fix: adapt gemini cli to v1internal endpoint

This commit is contained in:
MMEXA
2026-05-28 00:24:56 +08:00
parent 14ad6e9b75
commit adca142d1e
19 changed files with 1072 additions and 87 deletions
@@ -91,6 +91,11 @@ pub(crate) async fn maybe_build_local_same_format_provider_decision_payload_for_
"envelope_name".to_string(),
json!(crate::ai_serving::transport::kiro::KIRO_ENVELOPE_NAME),
);
} else if resolved.is_gemini_cli {
extra_fields.insert(
"envelope_name".to_string(),
json!(aether_ai_formats::api::GEMINI_CLI_V1INTERNAL_ENVELOPE_NAME),
);
} else if resolved.is_antigravity {
extra_fields.insert(
"envelope_name".to_string(),
@@ -144,7 +149,7 @@ pub(crate) async fn maybe_build_local_same_format_provider_decision_payload_for_
.and_then(serde_json::Value::as_bool)
.unwrap_or(false),
upstream_is_stream: resolved.upstream_is_stream,
has_envelope: resolved.is_kiro || resolved.is_antigravity,
has_envelope: resolved.is_kiro || resolved.is_antigravity || resolved.is_gemini_cli,
needs_conversion: false,
extra_fields,
}),
@@ -159,6 +164,7 @@ pub(crate) async fn maybe_build_local_same_format_provider_decision_payload_for_
transport,
is_antigravity: _,
is_kiro: _,
is_gemini_cli: _,
auth_header,
auth_value,
provider_api_format,
@@ -15,6 +15,10 @@ use crate::ai_serving::transport::antigravity::{
classify_local_antigravity_request_support, AntigravityEnvelopeRequestType,
AntigravityRequestEnvelopeSupport, AntigravityRequestSideSupport,
};
use crate::ai_serving::transport::gemini_cli::{
build_gemini_cli_v1internal_request, resolve_local_gemini_cli_request_auth,
GeminiCliRequestAuthSupport, GeminiCliRequestEnvelopeSupport,
};
use crate::ai_serving::transport::{
build_grok_browser_headers, build_grok_upstream_url, build_same_format_provider_headers,
GrokHeaderInput, SameFormatProviderHeadersInput, GROK_CHAT_PATH,
@@ -69,6 +73,7 @@ pub(crate) fn resolve_same_format_provider_transport_unsupported_reason_for_trac
);
if !behavior.is_antigravity
&& !behavior.is_claude_code
&& !behavior.is_gemini_cli
&& !behavior.is_vertex
&& !behavior.is_kiro
{
@@ -92,6 +97,7 @@ pub(crate) struct LocalSameFormatProviderCandidatePayloadParts {
pub(super) transport: Arc<GatewayProviderTransportSnapshot>,
pub(super) is_antigravity: bool,
pub(super) is_kiro: bool,
pub(super) is_gemini_cli: bool,
pub(super) auth_header: Option<String>,
pub(super) auth_value: Option<String>,
pub(super) provider_api_format: String,
@@ -234,6 +240,32 @@ pub(crate) async fn resolve_local_same_format_provider_candidate_payload_parts(
} else {
None
};
let gemini_cli_auth = if prepared.is_gemini_cli {
match resolve_local_gemini_cli_request_auth(&prepared.transport) {
GeminiCliRequestAuthSupport::Supported(auth) => Some(auth),
GeminiCliRequestAuthSupport::Unsupported(_) => {
mark_skipped_local_same_format_provider_candidate_with_extra_data(
state,
input,
trace_id,
candidate,
attempt.candidate_index,
&attempt.candidate_id,
"provider_request_body_missing",
same_format_provider_request_body_failure_extra_data(
body_json,
attempt.eligible.provider_api_format.as_str(),
prepared.transport.endpoint.body_rules.as_ref(),
"gemini_cli_envelope",
),
)
.await;
return Ok(None);
}
}
} else {
None
};
let provider_request_body = if let Some(antigravity_auth) = antigravity_auth.as_ref() {
match build_antigravity_safe_v1internal_request(
antigravity_auth,
@@ -263,6 +295,34 @@ pub(crate) async fn resolve_local_same_format_provider_candidate_payload_parts(
return Ok(None);
}
}
} else if let Some(gemini_cli_auth) = gemini_cli_auth.as_ref() {
match build_gemini_cli_v1internal_request(
gemini_cli_auth,
trace_id,
&prepared.mapped_model,
&base_provider_request_body,
) {
GeminiCliRequestEnvelopeSupport::Supported(envelope) => envelope,
GeminiCliRequestEnvelopeSupport::Unsupported(_) => {
mark_skipped_local_same_format_provider_candidate_with_extra_data(
state,
input,
trace_id,
candidate,
attempt.candidate_index,
&attempt.candidate_id,
"provider_request_body_missing",
same_format_provider_request_body_failure_extra_data(
body_json,
attempt.eligible.provider_api_format.as_str(),
prepared.transport.endpoint.body_rules.as_ref(),
"gemini_cli_envelope",
),
)
.await;
return Ok(None);
}
}
} else {
base_provider_request_body
};
@@ -367,6 +427,7 @@ pub(crate) async fn resolve_local_same_format_provider_candidate_payload_parts(
transport: prepared.transport,
is_antigravity: prepared.is_antigravity,
is_kiro: prepared.is_kiro,
is_gemini_cli: prepared.is_gemini_cli,
auth_header: prepared.auth_header,
auth_value: prepared.auth_value,
provider_api_format: prepared.provider_api_format,
@@ -28,6 +28,7 @@ pub(super) struct PreparedSameFormatProviderCandidate {
pub(super) behavior: SameFormatProviderRequestBehavior,
pub(super) is_antigravity: bool,
pub(super) is_claude_code: bool,
pub(super) is_gemini_cli: bool,
pub(super) is_vertex: bool,
pub(super) is_kiro: bool,
pub(super) kiro_auth: Option<KiroRequestAuth>,
@@ -175,6 +176,7 @@ pub(super) async fn prepare_local_same_format_provider_candidate(
behavior,
is_antigravity: behavior.is_antigravity,
is_claude_code: behavior.is_claude_code,
is_gemini_cli: behavior.is_gemini_cli,
is_vertex: behavior.is_vertex,
is_kiro: behavior.is_kiro,
kiro_auth,
@@ -23,6 +23,11 @@ use crate::ai_serving::planner::standard::{
build_local_openai_chat_upstream_url, request_body_build_failure_extra_data,
};
use crate::ai_serving::transport::auth::resolve_local_openai_bearer_auth;
use crate::ai_serving::transport::gemini_cli::{
build_gemini_cli_v1internal_request, is_gemini_cli_provider_transport,
resolve_local_gemini_cli_request_auth, GeminiCliRequestAuthSupport,
GeminiCliRequestEnvelopeSupport,
};
use crate::ai_serving::transport::kiro::{
build_kiro_provider_headers, build_kiro_provider_request_body,
is_kiro_claude_messages_transport, KiroProviderHeadersInput, KiroRequestAuth,
@@ -126,6 +131,7 @@ pub(crate) async fn resolve_local_openai_chat_candidate_payload_parts(
.provider_type
.trim()
.eq_ignore_ascii_case("grok");
let is_gemini_cli = is_gemini_cli_provider_transport(transport);
if is_grok && is_grok_text_provider_api_format(provider_api_format) {
let prepared_candidate = match prepare_header_authenticated_candidate(
@@ -440,6 +446,8 @@ pub(crate) async fn resolve_local_openai_chat_candidate_payload_parts(
};
let provider_api_format = provider_api_format.trim().to_ascii_lowercase();
let normalized_provider_api_format =
crate::ai_serving::normalize_api_format_alias(provider_api_format.as_str());
if provider_api_format == "openai:image" {
return resolve_openai_chat_to_openai_image_payload_parts(
state,
@@ -474,17 +482,19 @@ pub(crate) async fn resolve_local_openai_chat_candidate_payload_parts(
transport,
conversion_kind,
) {
mark_skipped_local_openai_chat_candidate(
state,
input,
trace_id,
candidate,
candidate_index,
candidate_id,
skip_reason,
)
.await;
return Ok(None);
if !(is_gemini_cli && normalized_provider_api_format == "gemini:generate_content") {
mark_skipped_local_openai_chat_candidate(
state,
input,
trace_id,
candidate,
candidate_index,
candidate_id,
skip_reason,
)
.await;
return Ok(None);
}
}
let is_kiro_claude_cli =
is_kiro_claude_messages_transport(transport, provider_api_format.as_str());
@@ -629,6 +639,64 @@ pub(crate) async fn resolve_local_openai_chat_candidate_payload_parts(
Some(body_json),
);
let gemini_cli_auth = if is_gemini_cli_provider_transport(transport)
&& normalized_provider_api_format == "gemini:generate_content"
{
match resolve_local_gemini_cli_request_auth(transport) {
GeminiCliRequestAuthSupport::Supported(auth) => Some(auth),
GeminiCliRequestAuthSupport::Unsupported(_) => {
mark_skipped_local_openai_chat_candidate_with_failure_diagnostic(
state,
input,
trace_id,
candidate,
candidate_index,
candidate_id,
"provider_request_body_build_failed",
CandidateFailureDiagnostic::envelope_build_failed(
"openai:chat",
provider_api_format.as_str(),
"openai_chat_gemini_cli_envelope",
),
)
.await;
return Ok(None);
}
}
} else {
None
};
let provider_request_body = if let Some(gemini_cli_auth) = gemini_cli_auth.as_ref() {
match build_gemini_cli_v1internal_request(
gemini_cli_auth,
trace_id,
&prepared_candidate.mapped_model,
&provider_request_body,
) {
GeminiCliRequestEnvelopeSupport::Supported(envelope) => envelope,
GeminiCliRequestEnvelopeSupport::Unsupported(_) => {
mark_skipped_local_openai_chat_candidate_with_failure_diagnostic(
state,
input,
trace_id,
candidate,
candidate_index,
candidate_id,
"provider_request_body_build_failed",
CandidateFailureDiagnostic::envelope_build_failed(
"openai:chat",
provider_api_format.as_str(),
"openai_chat_gemini_cli_envelope",
),
)
.await;
return Ok(None);
}
}
} else {
provider_request_body
};
if let Some(kiro_auth) = kiro_auth.as_ref() {
return Ok(build_kiro_openai_chat_cross_format_payload_parts(
state,
@@ -744,7 +812,9 @@ pub(crate) async fn resolve_local_openai_chat_candidate_payload_parts(
execution_strategy,
conversion_mode,
report_kind: resolved_report_kind,
envelope_name: None,
envelope_name: gemini_cli_auth
.as_ref()
.map(|_| aether_ai_formats::api::GEMINI_CLI_V1INTERNAL_ENVELOPE_NAME),
transport: Arc::clone(transport),
request_redacted: redaction.redacted,
transport_profile: None,
@@ -33,6 +33,11 @@ use crate::ai_serving::transport::antigravity::{
use crate::ai_serving::transport::auth::{
resolve_local_gemini_auth, resolve_local_openai_bearer_auth, resolve_local_standard_auth,
};
use crate::ai_serving::transport::gemini_cli::{
build_gemini_cli_v1internal_request, is_gemini_cli_provider_transport,
resolve_local_gemini_cli_request_auth, GeminiCliRequestAuthSupport,
GeminiCliRequestEnvelopeSupport,
};
use crate::ai_serving::transport::kiro::{
build_kiro_provider_headers, build_kiro_provider_request_body,
is_kiro_claude_messages_transport,
@@ -111,9 +116,12 @@ pub(crate) async fn resolve_local_openai_responses_candidate_payload_parts(
let planner_state = PlannerAppState::new(state);
let candidate = &eligible.candidate;
let provider_api_format = eligible.provider_api_format.as_str();
let normalized_provider_api_format =
crate::ai_serving::normalize_api_format_alias(provider_api_format);
let transport = &eligible.transport;
let transport_profile = crate::ai_serving::transport::resolve_transport_profile(transport);
let is_antigravity = is_antigravity_provider_transport(transport);
let is_gemini_cli = is_gemini_cli_provider_transport(transport);
let is_kiro_claude_cli = is_kiro_claude_messages_transport(transport, provider_api_format);
let is_grok = transport
.provider
@@ -140,25 +148,29 @@ pub(crate) async fn resolve_local_openai_responses_candidate_payload_parts(
let same_format = api_format_alias_matches(provider_api_format, &client_api_format);
let conversion_kind = request_conversion_kind(spec_metadata.api_format, provider_api_format);
let transport_unsupported_reason = if is_grok
&& is_grok_text_provider_api_format(provider_api_format)
{
None
} else if same_format && is_kiro_claude_cli {
local_kiro_request_transport_unsupported_reason_with_network(transport)
} else if same_format {
local_standard_transport_unsupported_reason_with_network(transport, provider_api_format)
} else if is_windsurf_cascade {
local_windsurf_request_transport_unsupported_reason_with_network(transport)
} else {
match conversion_kind {
Some(_) if is_antigravity && provider_api_format == "gemini:generate_content" => None,
Some(kind) => {
crate::ai_serving::request_conversion_transport_unsupported_reason(transport, kind)
let transport_unsupported_reason =
if is_grok && is_grok_text_provider_api_format(provider_api_format) {
None
} else if same_format && is_kiro_claude_cli {
local_kiro_request_transport_unsupported_reason_with_network(transport)
} else if same_format {
local_standard_transport_unsupported_reason_with_network(transport, provider_api_format)
} else if is_windsurf_cascade {
local_windsurf_request_transport_unsupported_reason_with_network(transport)
} else {
match conversion_kind {
Some(_)
if (is_antigravity || is_gemini_cli)
&& normalized_provider_api_format == "gemini:generate_content" =>
{
None
}
Some(kind) => crate::ai_serving::request_conversion_transport_unsupported_reason(
transport, kind,
),
None => Some("transport_api_format_unsupported"),
}
None => Some("transport_api_format_unsupported"),
}
};
};
if let Some(skip_reason) = transport_unsupported_reason {
mark_skipped_local_openai_responses_candidate(
state,
@@ -420,6 +432,32 @@ pub(crate) async fn resolve_local_openai_responses_candidate_payload_parts(
} else {
None
};
let gemini_cli_auth =
if is_gemini_cli && normalized_provider_api_format == "gemini:generate_content" {
match resolve_local_gemini_cli_request_auth(transport) {
GeminiCliRequestAuthSupport::Supported(auth) => Some(auth),
GeminiCliRequestAuthSupport::Unsupported(_) => {
mark_skipped_local_openai_responses_candidate_with_failure_diagnostic(
state,
input,
trace_id,
candidate,
candidate_index,
candidate_id,
"provider_request_body_build_failed",
CandidateFailureDiagnostic::envelope_build_failed(
spec_metadata.api_format,
provider_api_format,
"openai_responses_gemini_cli_envelope",
),
)
.await;
return Ok(None);
}
}
} else {
None
};
let provider_request_body = if let Some(antigravity_auth) = antigravity_auth.as_ref() {
match build_antigravity_safe_v1internal_request(
antigravity_auth,
@@ -448,6 +486,33 @@ pub(crate) async fn resolve_local_openai_responses_candidate_payload_parts(
return Ok(None);
}
}
} else if let Some(gemini_cli_auth) = gemini_cli_auth.as_ref() {
match build_gemini_cli_v1internal_request(
gemini_cli_auth,
trace_id,
&mapped_model,
&base_provider_request_body,
) {
GeminiCliRequestEnvelopeSupport::Supported(envelope) => envelope,
GeminiCliRequestEnvelopeSupport::Unsupported(_) => {
mark_skipped_local_openai_responses_candidate_with_failure_diagnostic(
state,
input,
trace_id,
candidate,
candidate_index,
candidate_id,
"provider_request_body_build_failed",
CandidateFailureDiagnostic::envelope_build_failed(
spec_metadata.api_format,
provider_api_format,
"openai_responses_gemini_cli_envelope",
),
)
.await;
return Ok(None);
}
}
} else {
base_provider_request_body
};
@@ -664,6 +729,8 @@ pub(crate) async fn resolve_local_openai_responses_candidate_payload_parts(
|| antigravity_auth.is_some() && ANTIGRAVITY_ENVELOPE_NAME == "antigravity:v1internal",
envelope_name: if is_antigravity || antigravity_auth.is_some() {
Some(ANTIGRAVITY_ENVELOPE_NAME)
} else if gemini_cli_auth.is_some() {
Some(aether_ai_formats::api::GEMINI_CLI_V1INTERNAL_ENVELOPE_NAME)
} else {
None
},
@@ -18,6 +18,10 @@ pub(crate) mod grok {
pub(crate) use aether_provider_transport::grok::*;
}
pub(crate) mod gemini_cli {
pub(crate) use aether_provider_transport::gemini_cli::*;
}
pub(crate) mod oauth_refresh {
pub(crate) use aether_provider_transport::oauth_refresh::*;
}
@@ -452,7 +452,9 @@ async fn gateway_executes_gemini_cli_stream_via_local_decision_gate_after_oauth_
struct SeenExecutionRuntimeStreamRequest {
trace_id: String,
url: String,
has_model_field: bool,
outer_model: String,
user_prompt_id: String,
inner_model_present: bool,
accept: String,
authorization: String,
exact_temperature: f64,
@@ -574,7 +576,7 @@ async fn gateway_executes_gemini_cli_stream_via_local_decision_gate_after_oauth_
)
.expect("endpoint should build")
.with_transport_fields(
"https://generativelanguage.googleapis.com".to_string(),
"https://cloudcode-pa.googleapis.com".to_string(),
Some(serde_json::json!([
{"action":"set","key":"x-endpoint-tag","value":"gemini-cli-oauth-local"}
])),
@@ -584,7 +586,7 @@ async fn gateway_executes_gemini_cli_stream_via_local_decision_gate_after_oauth_
{"action":"drop","path":"toolConfig"}
])),
Some(2),
Some("/custom/v1beta/models/gemini-cli-upstream:streamGenerateContent".to_string()),
None,
None,
None,
None,
@@ -730,10 +732,25 @@ async fn gateway_executes_gemini_cli_stream_via_local_decision_gate_after_oauth_
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
has_model_field: payload
outer_model: payload
.get("body")
.and_then(|value| value.get("json_body"))
.and_then(|value| value.get("model"))
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
user_prompt_id: payload
.get("body")
.and_then(|value| value.get("json_body"))
.and_then(|value| value.get("user_prompt_id"))
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
inner_model_present: payload
.get("body")
.and_then(|value| value.get("json_body"))
.and_then(|value| value.get("request"))
.and_then(|value| value.get("model"))
.is_some(),
accept: payload
.get("headers")
@@ -750,6 +767,7 @@ async fn gateway_executes_gemini_cli_stream_via_local_decision_gate_after_oauth_
exact_temperature: payload
.get("body")
.and_then(|value| value.get("json_body"))
.and_then(|value| value.get("request"))
.and_then(|value| value.get("generationConfig"))
.and_then(|value| value.get("temperature"))
.and_then(|value| value.as_f64())
@@ -763,6 +781,7 @@ async fn gateway_executes_gemini_cli_stream_via_local_decision_gate_after_oauth_
metadata_mode: payload
.get("body")
.and_then(|value| value.get("json_body"))
.and_then(|value| value.get("request"))
.and_then(|value| value.get("metadata"))
.and_then(|value| value.get("mode"))
.and_then(|value| value.as_str())
@@ -771,6 +790,7 @@ async fn gateway_executes_gemini_cli_stream_via_local_decision_gate_after_oauth_
metadata_source: payload
.get("body")
.and_then(|value| value.get("json_body"))
.and_then(|value| value.get("request"))
.and_then(|value| value.get("metadata"))
.and_then(|value| value.get("source"))
.and_then(|value| value.as_str())
@@ -779,6 +799,7 @@ async fn gateway_executes_gemini_cli_stream_via_local_decision_gate_after_oauth_
tool_config_present: payload
.get("body")
.and_then(|value| value.get("json_body"))
.and_then(|value| value.get("request"))
.and_then(|value| value.get("toolConfig"))
.is_some(),
proxy_node_id: payload
@@ -909,9 +930,17 @@ async fn gateway_executes_gemini_cli_stream_via_local_decision_gate_after_oauth_
);
assert_eq!(
seen_execution_runtime_request.url,
"https://generativelanguage.googleapis.com/custom/v1beta/models/gemini-cli-upstream:streamGenerateContent?alt=sse"
"https://cloudcode-pa.googleapis.com/v1internal:streamGenerateContent?alt=sse"
);
assert!(!seen_execution_runtime_request.has_model_field);
assert_eq!(
seen_execution_runtime_request.outer_model,
"gemini-cli-upstream"
);
assert_eq!(
seen_execution_runtime_request.user_prompt_id,
"trace-gemini-cli-oauth-local-stream-123"
);
assert!(!seen_execution_runtime_request.inner_model_present);
assert_eq!(seen_execution_runtime_request.accept, "text/event-stream");
assert_eq!(
seen_execution_runtime_request.authorization,
@@ -2010,7 +2010,9 @@ async fn gateway_returns_openai_chat_error_for_local_cross_format_gemini_cli_syn
struct SeenExecutionRuntimeSyncRequest {
trace_id: String,
url: String,
model: String,
outer_model: String,
user_prompt_id: String,
inner_model_present: bool,
auth_header_value: String,
endpoint_tag: String,
has_contents: bool,
@@ -2031,7 +2033,7 @@ async fn gateway_returns_openai_chat_error_for_local_cross_format_gemini_cli_syn
"local".to_string(),
true,
false,
Some(serde_json::json!(["openai", "gemini"])),
Some(serde_json::json!(["openai", "gemini", "gemini_cli"])),
Some(serde_json::json!(["openai:chat"])),
Some(serde_json::json!(["gpt-5"])),
api_key_id.to_string(),
@@ -2042,7 +2044,7 @@ async fn gateway_returns_openai_chat_error_for_local_cross_format_gemini_cli_syn
Some(60),
Some(5),
Some(4_102_444_800),
Some(serde_json::json!(["openai", "gemini"])),
Some(serde_json::json!(["openai", "gemini", "gemini_cli"])),
Some(serde_json::json!(["openai:chat"])),
Some(serde_json::json!(["gpt-5"])),
)
@@ -2052,8 +2054,8 @@ async fn gateway_returns_openai_chat_error_for_local_cross_format_gemini_cli_syn
fn sample_candidate_row() -> StoredMinimalCandidateSelectionRow {
StoredMinimalCandidateSelectionRow {
provider_id: "provider-openai-chat-gemini-cli-local-1".to_string(),
provider_name: "gemini".to_string(),
provider_type: "custom".to_string(),
provider_name: "gemini_cli".to_string(),
provider_type: "gemini_cli".to_string(),
provider_priority: 10,
provider_is_active: true,
endpoint_id: "endpoint-openai-chat-gemini-cli-local-1".to_string(),
@@ -2062,8 +2064,8 @@ async fn gateway_returns_openai_chat_error_for_local_cross_format_gemini_cli_syn
endpoint_kind: Some("cli".to_string()),
endpoint_is_active: true,
key_id: "key-openai-chat-gemini-cli-local-1".to_string(),
key_name: "prod".to_string(),
key_auth_type: "bearer".to_string(),
key_name: "oauth".to_string(),
key_auth_type: "oauth".to_string(),
key_is_active: true,
key_api_formats: Some(vec!["gemini:generate_content".to_string()]),
key_allowed_models: None,
@@ -2091,9 +2093,9 @@ async fn gateway_returns_openai_chat_error_for_local_cross_format_gemini_cli_syn
fn sample_provider_catalog_provider() -> StoredProviderCatalogProvider {
StoredProviderCatalogProvider::new(
"provider-openai-chat-gemini-cli-local-1".to_string(),
"gemini".to_string(),
"gemini_cli".to_string(),
Some("https://example.com".to_string()),
"custom".to_string(),
"gemini_cli".to_string(),
)
.expect("provider should build")
.with_transport_fields(
@@ -2120,13 +2122,13 @@ async fn gateway_returns_openai_chat_error_for_local_cross_format_gemini_cli_syn
)
.expect("endpoint should build")
.with_transport_fields(
"https://generativelanguage.googleapis.com".to_string(),
"https://cloudcode-pa.googleapis.com".to_string(),
Some(serde_json::json!([
{"action":"set","key":"x-endpoint-tag","value":"openai-chat-gemini-cli-cross-format"}
])),
None,
Some(2),
Some("/custom/v1beta/models/gemini-cli-upstream:generateContent".to_string()),
None,
None,
None,
None,
@@ -2138,8 +2140,8 @@ async fn gateway_returns_openai_chat_error_for_local_cross_format_gemini_cli_syn
StoredProviderCatalogKey::new(
"key-openai-chat-gemini-cli-local-1".to_string(),
"provider-openai-chat-gemini-cli-local-1".to_string(),
"prod".to_string(),
"bearer".to_string(),
"oauth".to_string(),
"oauth".to_string(),
None,
true,
)
@@ -2241,13 +2243,26 @@ async fn gateway_returns_openai_chat_error_for_local_cross_format_gemini_cli_syn
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
model: payload
outer_model: payload
.get("body")
.and_then(|value| value.get("json_body"))
.and_then(|value| value.get("model"))
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
user_prompt_id: payload
.get("body")
.and_then(|value| value.get("json_body"))
.and_then(|value| value.get("user_prompt_id"))
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
inner_model_present: payload
.get("body")
.and_then(|value| value.get("json_body"))
.and_then(|value| value.get("request"))
.and_then(|value| value.get("model"))
.is_some(),
auth_header_value: payload
.get("headers")
.and_then(|value| value.get("authorization"))
@@ -2263,6 +2278,7 @@ async fn gateway_returns_openai_chat_error_for_local_cross_format_gemini_cli_syn
has_contents: payload
.get("body")
.and_then(|value| value.get("json_body"))
.and_then(|value| value.get("request"))
.and_then(|value| value.get("contents"))
.is_some(),
});
@@ -2336,15 +2352,20 @@ async fn gateway_returns_openai_chat_error_for_local_cross_format_gemini_cli_syn
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::TOO_MANY_REQUESTS);
let response_status = response.status();
let execution_path = response
.headers()
.get(EXECUTION_PATH_HEADER)
.and_then(|value| value.to_str().ok())
.map(str::to_owned);
let response_body = response.text().await.expect("body should read");
assert_eq!(
response
.headers()
.get(EXECUTION_PATH_HEADER)
.and_then(|value| value.to_str().ok()),
execution_path.as_deref(),
Some(EXECUTION_PATH_EXECUTION_RUNTIME_SYNC)
);
let response_json: serde_json::Value = response.json().await.expect("body should parse");
assert_eq!(response_status, StatusCode::TOO_MANY_REQUESTS);
let response_json: serde_json::Value =
serde_json::from_str(&response_body).expect("body should parse");
assert_eq!(
response_json,
json!({
@@ -2367,9 +2388,17 @@ async fn gateway_returns_openai_chat_error_for_local_cross_format_gemini_cli_syn
);
assert_eq!(
seen_execution_runtime_request.url,
"https://generativelanguage.googleapis.com/custom/v1beta/models/gemini-cli-upstream:generateContent"
"https://cloudcode-pa.googleapis.com/v1internal:generateContent"
);
assert_eq!(seen_execution_runtime_request.model, "gemini-cli-upstream");
assert_eq!(
seen_execution_runtime_request.outer_model,
"gemini-cli-upstream"
);
assert_eq!(
seen_execution_runtime_request.user_prompt_id,
"trace-openai-chat-gemini-cli-local-error-123"
);
assert!(!seen_execution_runtime_request.inner_model_present);
assert_eq!(
seen_execution_runtime_request.auth_header_value,
"Bearer sk-upstream-openai-chat-gemini-cli"
@@ -1572,7 +1572,7 @@ async fn gateway_returns_openai_responses_error_for_local_sync_failure() {
}
#[tokio::test]
async fn gateway_returns_openai_responses_error_for_local_cross_format_gemini_sync_failure() {
async fn gateway_returns_openai_responses_error_for_local_cross_format_gemini_cli_sync_failure() {
#[derive(Debug, Clone)]
struct SeenExecutionRuntimeSyncRequest {
trace_id: String,
@@ -1580,7 +1580,9 @@ async fn gateway_returns_openai_responses_error_for_local_cross_format_gemini_sy
authorization: String,
endpoint_tag: String,
has_contents: bool,
model: String,
outer_model: String,
user_prompt_id: String,
inner_model_present: bool,
}
fn hash_api_key(value: &str) -> String {
@@ -1598,7 +1600,7 @@ async fn gateway_returns_openai_responses_error_for_local_cross_format_gemini_sy
"local".to_string(),
true,
false,
Some(serde_json::json!(["openai", "gemini"])),
Some(serde_json::json!(["openai", "gemini", "gemini_cli"])),
Some(serde_json::json!(["openai:responses"])),
Some(serde_json::json!(["gpt-5"])),
api_key_id.to_string(),
@@ -1609,7 +1611,7 @@ async fn gateway_returns_openai_responses_error_for_local_cross_format_gemini_sy
Some(60),
Some(5),
Some(4_102_444_800_i64),
Some(serde_json::json!(["openai", "gemini"])),
Some(serde_json::json!(["openai", "gemini", "gemini_cli"])),
Some(serde_json::json!(["openai:responses"])),
Some(serde_json::json!(["gpt-5"])),
)
@@ -1619,8 +1621,8 @@ async fn gateway_returns_openai_responses_error_for_local_cross_format_gemini_sy
fn sample_candidate_row() -> StoredMinimalCandidateSelectionRow {
StoredMinimalCandidateSelectionRow {
provider_id: "provider-openai-cli-gemini-local-1".to_string(),
provider_name: "gemini".to_string(),
provider_type: "custom".to_string(),
provider_name: "gemini_cli".to_string(),
provider_type: "gemini_cli".to_string(),
provider_priority: 10,
provider_is_active: true,
endpoint_id: "endpoint-openai-cli-gemini-local-1".to_string(),
@@ -1629,8 +1631,8 @@ async fn gateway_returns_openai_responses_error_for_local_cross_format_gemini_sy
endpoint_kind: Some("cli".to_string()),
endpoint_is_active: true,
key_id: "key-openai-cli-gemini-local-1".to_string(),
key_name: "prod".to_string(),
key_auth_type: "bearer".to_string(),
key_name: "oauth".to_string(),
key_auth_type: "oauth".to_string(),
key_is_active: true,
key_api_formats: Some(vec!["gemini:generate_content".to_string()]),
key_allowed_models: None,
@@ -1658,9 +1660,9 @@ async fn gateway_returns_openai_responses_error_for_local_cross_format_gemini_sy
fn sample_provider_catalog_provider() -> StoredProviderCatalogProvider {
StoredProviderCatalogProvider::new(
"provider-openai-cli-gemini-local-1".to_string(),
"gemini".to_string(),
"gemini_cli".to_string(),
Some("https://example.com".to_string()),
"custom".to_string(),
"gemini_cli".to_string(),
)
.expect("provider should build")
.with_transport_fields(
@@ -1687,13 +1689,13 @@ async fn gateway_returns_openai_responses_error_for_local_cross_format_gemini_sy
)
.expect("endpoint should build")
.with_transport_fields(
"https://generativelanguage.googleapis.com".to_string(),
"https://cloudcode-pa.googleapis.com".to_string(),
Some(serde_json::json!([
{"action":"set","key":"x-endpoint-tag","value":"openai-cli-gemini-cross-format"}
])),
None,
Some(2),
Some("/custom/v1beta/models/gemini-cli-upstream:generateContent".to_string()),
None,
None,
None,
None,
@@ -1705,8 +1707,8 @@ async fn gateway_returns_openai_responses_error_for_local_cross_format_gemini_sy
StoredProviderCatalogKey::new(
"key-openai-cli-gemini-local-1".to_string(),
"provider-openai-cli-gemini-local-1".to_string(),
"prod".to_string(),
"bearer".to_string(),
"oauth".to_string(),
"oauth".to_string(),
None,
true,
)
@@ -1806,15 +1808,29 @@ async fn gateway_returns_openai_responses_error_for_local_cross_format_gemini_sy
has_contents: payload
.get("body")
.and_then(|value| value.get("json_body"))
.and_then(|value| value.get("request"))
.and_then(|value| value.get("contents"))
.is_some(),
model: payload
outer_model: payload
.get("body")
.and_then(|value| value.get("json_body"))
.and_then(|value| value.get("model"))
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
user_prompt_id: payload
.get("body")
.and_then(|value| value.get("json_body"))
.and_then(|value| value.get("user_prompt_id"))
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
inner_model_present: payload
.get("body")
.and_then(|value| value.get("json_body"))
.and_then(|value| value.get("request"))
.and_then(|value| value.get("model"))
.is_some(),
});
Json(json!({
"request_id": "trace-openai-cli-gemini-local-error-123",
@@ -1917,7 +1933,7 @@ async fn gateway_returns_openai_responses_error_for_local_cross_format_gemini_sy
);
assert_eq!(
seen_execution_runtime_request.url,
"https://generativelanguage.googleapis.com/custom/v1beta/models/gemini-cli-upstream:generateContent"
"https://cloudcode-pa.googleapis.com/v1internal:generateContent"
);
assert_eq!(
seen_execution_runtime_request.authorization,
@@ -1928,7 +1944,15 @@ async fn gateway_returns_openai_responses_error_for_local_cross_format_gemini_sy
"openai-cli-gemini-cross-format"
);
assert!(seen_execution_runtime_request.has_contents);
assert_eq!(seen_execution_runtime_request.model, "gemini-cli-upstream");
assert_eq!(
seen_execution_runtime_request.outer_model,
"gemini-cli-upstream"
);
assert_eq!(
seen_execution_runtime_request.user_prompt_id,
"trace-openai-cli-gemini-local-error-123"
);
assert!(!seen_execution_runtime_request.inner_model_present);
let stored_candidates = request_candidate_repository
.list_by_request_id("trace-openai-cli-gemini-local-error-123")
@@ -779,7 +779,9 @@ async fn gateway_executes_gemini_cli_sync_via_local_decision_gate_after_oauth_re
struct SeenExecutionRuntimeSyncRequest {
trace_id: String,
url: String,
has_model_field: bool,
outer_model: String,
user_prompt_id: String,
inner_model_present: bool,
authorization: String,
exact_temperature: f64,
endpoint_tag: String,
@@ -900,7 +902,7 @@ async fn gateway_executes_gemini_cli_sync_via_local_decision_gate_after_oauth_re
)
.expect("endpoint should build")
.with_transport_fields(
"https://generativelanguage.googleapis.com".to_string(),
"https://cloudcode-pa.googleapis.com".to_string(),
Some(serde_json::json!([
{"action":"set","key":"x-endpoint-tag","value":"gemini-cli-oauth-local"}
])),
@@ -910,7 +912,7 @@ async fn gateway_executes_gemini_cli_sync_via_local_decision_gate_after_oauth_re
{"action":"drop","path":"toolConfig"}
])),
Some(2),
Some("/custom/v1beta/models/gemini-cli-upstream:generateContent".to_string()),
None,
None,
None,
None,
@@ -1057,10 +1059,25 @@ async fn gateway_executes_gemini_cli_sync_via_local_decision_gate_after_oauth_re
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
has_model_field: payload
outer_model: payload
.get("body")
.and_then(|value| value.get("json_body"))
.and_then(|value| value.get("model"))
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
user_prompt_id: payload
.get("body")
.and_then(|value| value.get("json_body"))
.and_then(|value| value.get("user_prompt_id"))
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
inner_model_present: payload
.get("body")
.and_then(|value| value.get("json_body"))
.and_then(|value| value.get("request"))
.and_then(|value| value.get("model"))
.is_some(),
authorization: payload
.get("headers")
@@ -1071,6 +1088,7 @@ async fn gateway_executes_gemini_cli_sync_via_local_decision_gate_after_oauth_re
exact_temperature: payload
.get("body")
.and_then(|value| value.get("json_body"))
.and_then(|value| value.get("request"))
.and_then(|value| value.get("generationConfig"))
.and_then(|value| value.get("temperature"))
.and_then(|value| value.as_f64())
@@ -1084,6 +1102,7 @@ async fn gateway_executes_gemini_cli_sync_via_local_decision_gate_after_oauth_re
metadata_mode: payload
.get("body")
.and_then(|value| value.get("json_body"))
.and_then(|value| value.get("request"))
.and_then(|value| value.get("metadata"))
.and_then(|value| value.get("mode"))
.and_then(|value| value.as_str())
@@ -1092,6 +1111,7 @@ async fn gateway_executes_gemini_cli_sync_via_local_decision_gate_after_oauth_re
metadata_source: payload
.get("body")
.and_then(|value| value.get("json_body"))
.and_then(|value| value.get("request"))
.and_then(|value| value.get("metadata"))
.and_then(|value| value.get("source"))
.and_then(|value| value.as_str())
@@ -1100,6 +1120,7 @@ async fn gateway_executes_gemini_cli_sync_via_local_decision_gate_after_oauth_re
tool_config_present: payload
.get("body")
.and_then(|value| value.get("json_body"))
.and_then(|value| value.get("request"))
.and_then(|value| value.get("toolConfig"))
.is_some(),
proxy_node_id: payload
@@ -1238,9 +1259,17 @@ async fn gateway_executes_gemini_cli_sync_via_local_decision_gate_after_oauth_re
);
assert_eq!(
seen_execution_runtime_request.url,
"https://generativelanguage.googleapis.com/custom/v1beta/models/gemini-cli-upstream:generateContent"
"https://cloudcode-pa.googleapis.com/v1internal:generateContent"
);
assert!(!seen_execution_runtime_request.has_model_field);
assert_eq!(
seen_execution_runtime_request.outer_model,
"gemini-cli-upstream"
);
assert_eq!(
seen_execution_runtime_request.user_prompt_id,
"trace-gemini-cli-oauth-local-sync-123"
);
assert!(!seen_execution_runtime_request.inner_model_present);
assert_eq!(
seen_execution_runtime_request.authorization,
"Bearer refreshed-gemini-cli-access-token"
@@ -5027,7 +5027,7 @@ async fn gateway_handles_gemini_cli_test_model_with_oauth_header_fallback() {
assert_eq!(plan.provider_api_format, "gemini:generate_content");
assert_eq!(
plan.url,
"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:generateContent"
"https://cloudcode-pa.googleapis.com/v1internal:generateContent"
);
assert_eq!(
plan.headers.get("authorization").map(String::as_str),
@@ -5082,7 +5082,7 @@ async fn gateway_handles_gemini_cli_test_model_with_oauth_header_fallback() {
"endpoint-gemini-cli",
"provider-gemini",
"gemini:generate_content",
"https://generativelanguage.googleapis.com",
"https://cloudcode-pa.googleapis.com",
)],
vec![key],
));