mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-10 13:10:21 +08:00
fix: adapt gemini cli to v1internal endpoint
This commit is contained in:
@@ -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,
|
||||
|
||||
+2
@@ -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,
|
||||
|
||||
+82
-12
@@ -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,
|
||||
|
||||
+85
-18
@@ -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],
|
||||
));
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pub const ANTIGRAVITY_PROVIDER_TYPE: &str = "antigravity";
|
||||
pub const GEMINI_CLI_PROVIDER_TYPE: &str = "gemini_cli";
|
||||
pub const KIRO_PROVIDER_TYPE: &str = "kiro";
|
||||
pub const WINDSURF_PROVIDER_TYPE: &str = "windsurf";
|
||||
pub const KIRO_ENVELOPE_NAME: &str = "kiro:generateAssistantResponse";
|
||||
@@ -53,10 +54,10 @@ const PROVIDER_ADAPTATION_SURFACES: &[ProviderAdaptationDescriptor] = &[
|
||||
},
|
||||
ProviderAdaptationDescriptor {
|
||||
surface: ProviderAdaptationSurface::GeminiCliV1Internal,
|
||||
provider_type: None,
|
||||
provider_type: Some(GEMINI_CLI_PROVIDER_TYPE),
|
||||
envelope_name: GEMINI_CLI_V1INTERNAL_ENVELOPE_NAME,
|
||||
anchor_api_format: "gemini:generate_content",
|
||||
supports_request_bridge: false,
|
||||
supports_request_bridge: true,
|
||||
supports_sync_finalize_bridge: true,
|
||||
supports_stream_bridge: true,
|
||||
requires_eventstream_accept: false,
|
||||
@@ -153,9 +154,11 @@ pub fn provider_adaptation_should_unwrap_stream_envelope(
|
||||
mod tests {
|
||||
use super::{
|
||||
provider_adaptation_allows_sync_finalize_envelope, provider_adaptation_anchor_api_format,
|
||||
provider_adaptation_descriptor_for_provider_type,
|
||||
provider_adaptation_requires_eventstream_accept,
|
||||
provider_adaptation_should_unwrap_stream_envelope, ANTIGRAVITY_V1INTERNAL_ENVELOPE_NAME,
|
||||
GEMINI_CLI_V1INTERNAL_ENVELOPE_NAME, KIRO_ENVELOPE_NAME, WINDSURF_ENVELOPE_NAME,
|
||||
GEMINI_CLI_PROVIDER_TYPE, GEMINI_CLI_V1INTERNAL_ENVELOPE_NAME, KIRO_ENVELOPE_NAME,
|
||||
WINDSURF_ENVELOPE_NAME,
|
||||
};
|
||||
|
||||
#[test]
|
||||
@@ -194,6 +197,12 @@ mod tests {
|
||||
GEMINI_CLI_V1INTERNAL_ENVELOPE_NAME,
|
||||
"gemini:generate_content"
|
||||
));
|
||||
let gemini_cli_descriptor = provider_adaptation_descriptor_for_provider_type(
|
||||
GEMINI_CLI_PROVIDER_TYPE,
|
||||
"gemini:generate_content",
|
||||
)
|
||||
.expect("gemini cli descriptor should resolve by provider type");
|
||||
assert!(gemini_cli_descriptor.supports_request_bridge);
|
||||
assert!(provider_adaptation_requires_eventstream_accept(
|
||||
Some(KIRO_ENVELOPE_NAME),
|
||||
"claude:messages"
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
use serde_json::Value;
|
||||
|
||||
use super::super::snapshot::GatewayProviderTransportSnapshot;
|
||||
|
||||
pub const GEMINI_CLI_PROVIDER_TYPE: &str = "gemini_cli";
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct GeminiCliRequestAuth {
|
||||
pub project_id: Option<String>,
|
||||
pub session_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum GeminiCliRequestAuthSupport {
|
||||
Supported(GeminiCliRequestAuth),
|
||||
Unsupported(GeminiCliRequestAuthUnsupportedReason),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum GeminiCliRequestAuthUnsupportedReason {
|
||||
WrongProviderType,
|
||||
InvalidAuthConfigJson,
|
||||
}
|
||||
|
||||
pub fn is_gemini_cli_provider_transport(transport: &GatewayProviderTransportSnapshot) -> bool {
|
||||
transport
|
||||
.provider
|
||||
.provider_type
|
||||
.trim()
|
||||
.eq_ignore_ascii_case(GEMINI_CLI_PROVIDER_TYPE)
|
||||
}
|
||||
|
||||
pub fn resolve_local_gemini_cli_request_auth(
|
||||
transport: &GatewayProviderTransportSnapshot,
|
||||
) -> GeminiCliRequestAuthSupport {
|
||||
if !is_gemini_cli_provider_transport(transport) {
|
||||
return GeminiCliRequestAuthSupport::Unsupported(
|
||||
GeminiCliRequestAuthUnsupportedReason::WrongProviderType,
|
||||
);
|
||||
}
|
||||
|
||||
let Some(raw_auth_config) = transport
|
||||
.key
|
||||
.decrypted_auth_config
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
else {
|
||||
return GeminiCliRequestAuthSupport::Supported(GeminiCliRequestAuth::default());
|
||||
};
|
||||
|
||||
let Ok(auth_config) = serde_json::from_str::<Value>(raw_auth_config) else {
|
||||
return GeminiCliRequestAuthSupport::Unsupported(
|
||||
GeminiCliRequestAuthUnsupportedReason::InvalidAuthConfigJson,
|
||||
);
|
||||
};
|
||||
|
||||
GeminiCliRequestAuthSupport::Supported(GeminiCliRequestAuth {
|
||||
project_id: find_string_by_paths(
|
||||
&auth_config,
|
||||
&[
|
||||
&["project"],
|
||||
&["project_id"],
|
||||
&["projectId"],
|
||||
&["project", "id"],
|
||||
&["project", "project_id"],
|
||||
&["project", "projectId"],
|
||||
&["cloudaicompanionProject"],
|
||||
&["cloudaicompanionProject", "id"],
|
||||
&["cloudaicompanion_project"],
|
||||
&["cloudaicompanion_project", "id"],
|
||||
&["gemini_cli", "project"],
|
||||
&["gemini_cli", "project_id"],
|
||||
&["gemini_cli", "projectId"],
|
||||
&["geminiCli", "project"],
|
||||
&["geminiCli", "projectId"],
|
||||
&["metadata", "project"],
|
||||
&["metadata", "project_id"],
|
||||
&["metadata", "projectId"],
|
||||
],
|
||||
),
|
||||
session_id: find_string_by_paths(
|
||||
&auth_config,
|
||||
&[
|
||||
&["session_id"],
|
||||
&["sessionId"],
|
||||
&["gemini_cli", "session_id"],
|
||||
&["gemini_cli", "sessionId"],
|
||||
&["geminiCli", "sessionId"],
|
||||
&["metadata", "session_id"],
|
||||
&["metadata", "sessionId"],
|
||||
],
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
fn find_string_by_paths(value: &Value, paths: &[&[&str]]) -> Option<String> {
|
||||
for path in paths {
|
||||
let mut current = value;
|
||||
let mut matched = true;
|
||||
for segment in *path {
|
||||
let Some(next) = current.get(*segment) else {
|
||||
matched = false;
|
||||
break;
|
||||
};
|
||||
current = next;
|
||||
}
|
||||
if !matched {
|
||||
continue;
|
||||
}
|
||||
if let Some(string) = current
|
||||
.as_str()
|
||||
.map(str::trim)
|
||||
.filter(|item| !item.is_empty())
|
||||
{
|
||||
return Some(string.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
resolve_local_gemini_cli_request_auth, GeminiCliRequestAuth, GeminiCliRequestAuthSupport,
|
||||
};
|
||||
use crate::snapshot::{
|
||||
GatewayProviderTransportEndpoint, GatewayProviderTransportKey,
|
||||
GatewayProviderTransportProvider, GatewayProviderTransportSnapshot,
|
||||
};
|
||||
|
||||
fn sample_transport() -> GatewayProviderTransportSnapshot {
|
||||
GatewayProviderTransportSnapshot {
|
||||
provider: GatewayProviderTransportProvider {
|
||||
id: "provider-1".to_string(),
|
||||
name: "Gemini CLI".to_string(),
|
||||
provider_type: "gemini_cli".to_string(),
|
||||
website: None,
|
||||
is_active: true,
|
||||
keep_priority_on_conversion: false,
|
||||
enable_format_conversion: true,
|
||||
concurrent_limit: None,
|
||||
max_retries: None,
|
||||
proxy: None,
|
||||
request_timeout_secs: None,
|
||||
stream_first_byte_timeout_secs: None,
|
||||
config: None,
|
||||
},
|
||||
endpoint: GatewayProviderTransportEndpoint {
|
||||
id: "endpoint-1".to_string(),
|
||||
provider_id: "provider-1".to_string(),
|
||||
api_format: "gemini:generate_content".to_string(),
|
||||
api_family: None,
|
||||
endpoint_kind: None,
|
||||
is_active: true,
|
||||
base_url: "https://cloudcode-pa.googleapis.com".to_string(),
|
||||
header_rules: None,
|
||||
body_rules: None,
|
||||
max_retries: None,
|
||||
custom_path: None,
|
||||
config: None,
|
||||
format_acceptance_config: None,
|
||||
proxy: None,
|
||||
},
|
||||
key: GatewayProviderTransportKey {
|
||||
id: "key-1".to_string(),
|
||||
provider_id: "provider-1".to_string(),
|
||||
name: "key".to_string(),
|
||||
auth_type: "oauth".to_string(),
|
||||
is_active: true,
|
||||
api_formats: None,
|
||||
auth_type_by_format: None,
|
||||
allow_auth_channel_mismatch_formats: None,
|
||||
allowed_models: None,
|
||||
capabilities: None,
|
||||
rate_multipliers: None,
|
||||
global_priority_by_format: None,
|
||||
expires_at_unix_secs: None,
|
||||
proxy: None,
|
||||
fingerprint: None,
|
||||
decrypted_api_key: "__oauth__".to_string(),
|
||||
decrypted_auth_config: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extracts_project_and_session_metadata_when_available() {
|
||||
let mut transport = sample_transport();
|
||||
transport.key.decrypted_auth_config = Some(
|
||||
r#"{
|
||||
"cloudaicompanionProject": {"id": "project-123"},
|
||||
"metadata": {"sessionId": "session-123"}
|
||||
}"#
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
resolve_local_gemini_cli_request_auth(&transport),
|
||||
GeminiCliRequestAuthSupport::Supported(GeminiCliRequestAuth {
|
||||
project_id: Some("project-123".to_string()),
|
||||
session_id: Some("session-123".to_string()),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_metadata_still_supports_request_envelope() {
|
||||
let transport = sample_transport();
|
||||
|
||||
assert_eq!(
|
||||
resolve_local_gemini_cli_request_auth(&transport),
|
||||
GeminiCliRequestAuthSupport::Supported(GeminiCliRequestAuth {
|
||||
project_id: None,
|
||||
session_id: None,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
mod auth;
|
||||
mod request;
|
||||
mod url;
|
||||
|
||||
pub use auth::{
|
||||
is_gemini_cli_provider_transport, resolve_local_gemini_cli_request_auth, GeminiCliRequestAuth,
|
||||
GeminiCliRequestAuthSupport, GeminiCliRequestAuthUnsupportedReason, GEMINI_CLI_PROVIDER_TYPE,
|
||||
};
|
||||
pub use request::{
|
||||
build_gemini_cli_v1internal_request, classify_gemini_cli_v1internal_request_body,
|
||||
GeminiCliRequestEnvelopeSupport, GeminiCliRequestEnvelopeUnsupportedReason,
|
||||
};
|
||||
pub use url::{
|
||||
build_gemini_cli_v1internal_url, GeminiCliRequestUrlAction, GEMINI_CLI_V1INTERNAL_PATH_TEMPLATE,
|
||||
};
|
||||
@@ -0,0 +1,242 @@
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
use super::auth::GeminiCliRequestAuth;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum GeminiCliRequestEnvelopeSupport {
|
||||
Supported(Value),
|
||||
Unsupported(GeminiCliRequestEnvelopeUnsupportedReason),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum GeminiCliRequestEnvelopeUnsupportedReason {
|
||||
NonObjectBody,
|
||||
MissingContents,
|
||||
MissingUserPromptId,
|
||||
MissingModel,
|
||||
}
|
||||
|
||||
pub fn classify_gemini_cli_v1internal_request_body(
|
||||
request_body: &Value,
|
||||
) -> Result<(), GeminiCliRequestEnvelopeUnsupportedReason> {
|
||||
let Value::Object(map) = request_body else {
|
||||
return Err(GeminiCliRequestEnvelopeUnsupportedReason::NonObjectBody);
|
||||
};
|
||||
if !map.contains_key("contents") && existing_v1internal_request_object(map).is_none() {
|
||||
return Err(GeminiCliRequestEnvelopeUnsupportedReason::MissingContents);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn build_gemini_cli_v1internal_request(
|
||||
auth: &GeminiCliRequestAuth,
|
||||
user_prompt_id: &str,
|
||||
model: &str,
|
||||
request_body: &Value,
|
||||
) -> GeminiCliRequestEnvelopeSupport {
|
||||
if user_prompt_id.trim().is_empty() {
|
||||
return GeminiCliRequestEnvelopeSupport::Unsupported(
|
||||
GeminiCliRequestEnvelopeUnsupportedReason::MissingUserPromptId,
|
||||
);
|
||||
}
|
||||
if model.trim().is_empty() {
|
||||
return GeminiCliRequestEnvelopeSupport::Unsupported(
|
||||
GeminiCliRequestEnvelopeUnsupportedReason::MissingModel,
|
||||
);
|
||||
}
|
||||
if let Err(reason) = classify_gemini_cli_v1internal_request_body(request_body) {
|
||||
return GeminiCliRequestEnvelopeSupport::Unsupported(reason);
|
||||
}
|
||||
|
||||
let Value::Object(source) = request_body else {
|
||||
return GeminiCliRequestEnvelopeSupport::Unsupported(
|
||||
GeminiCliRequestEnvelopeUnsupportedReason::NonObjectBody,
|
||||
);
|
||||
};
|
||||
|
||||
let existing_request = existing_v1internal_request_object(source);
|
||||
let mut inner_request: Map<String, Value> =
|
||||
existing_request.cloned().unwrap_or_else(|| source.clone());
|
||||
sanitize_inner_request(&mut inner_request);
|
||||
maybe_insert_session_id(&mut inner_request, auth.session_id.as_deref());
|
||||
|
||||
let project = non_empty_string_field(source, "project")
|
||||
.map(ToOwned::to_owned)
|
||||
.or_else(|| {
|
||||
auth.project_id
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
});
|
||||
let user_prompt_id = non_empty_string_field(source, "user_prompt_id")
|
||||
.or_else(|| non_empty_string_field(source, "userPromptId"))
|
||||
.unwrap_or(user_prompt_id)
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
let mut envelope = Map::new();
|
||||
envelope.insert("model".to_string(), Value::String(model.trim().to_string()));
|
||||
if let Some(project) = project {
|
||||
envelope.insert("project".to_string(), Value::String(project));
|
||||
}
|
||||
envelope.insert("user_prompt_id".to_string(), Value::String(user_prompt_id));
|
||||
envelope.insert("request".to_string(), Value::Object(inner_request));
|
||||
|
||||
GeminiCliRequestEnvelopeSupport::Supported(Value::Object(envelope))
|
||||
}
|
||||
|
||||
fn sanitize_inner_request(inner_request: &mut Map<String, Value>) {
|
||||
inner_request.remove("model");
|
||||
inner_request.remove("stream");
|
||||
}
|
||||
|
||||
fn maybe_insert_session_id(inner_request: &mut Map<String, Value>, session_id: Option<&str>) {
|
||||
let Some(session_id) = session_id.map(str::trim).filter(|value| !value.is_empty()) else {
|
||||
return;
|
||||
};
|
||||
if inner_request.contains_key("session_id") || inner_request.contains_key("sessionId") {
|
||||
return;
|
||||
}
|
||||
inner_request.insert(
|
||||
"session_id".to_string(),
|
||||
Value::String(session_id.to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
fn existing_v1internal_request_object(source: &Map<String, Value>) -> Option<&Map<String, Value>> {
|
||||
source
|
||||
.get("request")
|
||||
.and_then(Value::as_object)
|
||||
.filter(|request| request.contains_key("contents"))
|
||||
}
|
||||
|
||||
fn non_empty_string_field<'a>(source: &'a Map<String, Value>, key: &str) -> Option<&'a str> {
|
||||
source
|
||||
.get(key)
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
|
||||
use super::{
|
||||
build_gemini_cli_v1internal_request, classify_gemini_cli_v1internal_request_body,
|
||||
GeminiCliRequestAuth, GeminiCliRequestEnvelopeSupport,
|
||||
};
|
||||
|
||||
fn sample_auth() -> GeminiCliRequestAuth {
|
||||
GeminiCliRequestAuth {
|
||||
project_id: Some("project-123".to_string()),
|
||||
session_id: Some("session-123".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wraps_generate_content_body_in_gemini_cli_v1internal_envelope() {
|
||||
let request_body = json!({
|
||||
"model": "client-model",
|
||||
"contents": [
|
||||
{"role": "user", "parts": [{"text": "hello"}]}
|
||||
],
|
||||
"stream": true,
|
||||
"generationConfig": {"temperature": 0.2}
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
classify_gemini_cli_v1internal_request_body(&request_body),
|
||||
Ok(())
|
||||
);
|
||||
assert_eq!(
|
||||
build_gemini_cli_v1internal_request(
|
||||
&sample_auth(),
|
||||
"trace-123",
|
||||
"gemini-2.5-pro",
|
||||
&request_body,
|
||||
),
|
||||
GeminiCliRequestEnvelopeSupport::Supported(json!({
|
||||
"model": "gemini-2.5-pro",
|
||||
"project": "project-123",
|
||||
"user_prompt_id": "trace-123",
|
||||
"request": {
|
||||
"contents": [
|
||||
{"role": "user", "parts": [{"text": "hello"}]}
|
||||
],
|
||||
"generationConfig": {"temperature": 0.2},
|
||||
"session_id": "session-123"
|
||||
}
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preserves_existing_v1internal_request_shape_without_antigravity_fields() {
|
||||
let request_body = json!({
|
||||
"model": "old-model",
|
||||
"project": "project-from-body",
|
||||
"user_prompt_id": "prompt-from-body",
|
||||
"request": {
|
||||
"model": "nested-client-model",
|
||||
"contents": [
|
||||
{"role": "user", "parts": [{"text": "hello"}]}
|
||||
],
|
||||
"stream": false,
|
||||
"labels": {"source": "test"}
|
||||
},
|
||||
"userAgent": "antigravity",
|
||||
"requestType": "agent"
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
build_gemini_cli_v1internal_request(
|
||||
&sample_auth(),
|
||||
"trace-123",
|
||||
"gemini-2.5-pro",
|
||||
&request_body,
|
||||
),
|
||||
GeminiCliRequestEnvelopeSupport::Supported(json!({
|
||||
"model": "gemini-2.5-pro",
|
||||
"project": "project-from-body",
|
||||
"user_prompt_id": "prompt-from-body",
|
||||
"request": {
|
||||
"contents": [
|
||||
{"role": "user", "parts": [{"text": "hello"}]}
|
||||
],
|
||||
"labels": {"source": "test"},
|
||||
"session_id": "session-123"
|
||||
}
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn omits_optional_project_and_session_when_metadata_is_absent() {
|
||||
let request_body = json!({
|
||||
"contents": [
|
||||
{"role": "user", "parts": [{"text": "hello"}]}
|
||||
]
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
build_gemini_cli_v1internal_request(
|
||||
&GeminiCliRequestAuth::default(),
|
||||
"trace-123",
|
||||
"gemini-2.5-pro",
|
||||
&request_body,
|
||||
),
|
||||
GeminiCliRequestEnvelopeSupport::Supported(json!({
|
||||
"model": "gemini-2.5-pro",
|
||||
"user_prompt_id": "trace-123",
|
||||
"request": {
|
||||
"contents": [
|
||||
{"role": "user", "parts": [{"text": "hello"}]}
|
||||
]
|
||||
}
|
||||
}))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use url::form_urlencoded;
|
||||
|
||||
pub const GEMINI_CLI_V1INTERNAL_PATH_TEMPLATE: &str = "/v1internal:{action}";
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum GeminiCliRequestUrlAction {
|
||||
GenerateContent,
|
||||
StreamGenerateContent,
|
||||
}
|
||||
|
||||
impl GeminiCliRequestUrlAction {
|
||||
fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::GenerateContent => "generateContent",
|
||||
Self::StreamGenerateContent => "streamGenerateContent",
|
||||
}
|
||||
}
|
||||
|
||||
fn is_stream(self) -> bool {
|
||||
matches!(self, Self::StreamGenerateContent)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_gemini_cli_v1internal_url(
|
||||
base_url: &str,
|
||||
action: GeminiCliRequestUrlAction,
|
||||
query: Option<&BTreeMap<String, String>>,
|
||||
) -> Option<String> {
|
||||
let trimmed_base = base_url.trim();
|
||||
if trimmed_base.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let path = GEMINI_CLI_V1INTERNAL_PATH_TEMPLATE.replace("{action}", action.as_str());
|
||||
let mut url = format!("{}{}", trimmed_base.trim_end_matches('/'), path);
|
||||
|
||||
let mut params = BTreeMap::new();
|
||||
if let Some(query) = query {
|
||||
for (key, value) in query {
|
||||
let key = key.trim();
|
||||
let value = value.trim();
|
||||
if key.is_empty()
|
||||
|| value.is_empty()
|
||||
|| key.eq_ignore_ascii_case("beta")
|
||||
|| key.eq_ignore_ascii_case("key")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
params.insert(key.to_string(), value.to_string());
|
||||
}
|
||||
}
|
||||
if action.is_stream() {
|
||||
params
|
||||
.entry(String::from("alt"))
|
||||
.or_insert_with(|| String::from("sse"));
|
||||
}
|
||||
|
||||
if !params.is_empty() {
|
||||
let mut serializer = form_urlencoded::Serializer::new(String::new());
|
||||
for (key, value) in params {
|
||||
serializer.append_pair(key.as_str(), value.as_str());
|
||||
}
|
||||
let query_string = serializer.finish();
|
||||
if !query_string.is_empty() {
|
||||
url.push('?');
|
||||
url.push_str(&query_string);
|
||||
}
|
||||
}
|
||||
|
||||
Some(url)
|
||||
}
|
||||
@@ -5,6 +5,7 @@ mod cache;
|
||||
pub mod claude_code;
|
||||
pub mod conversion;
|
||||
mod diagnostics;
|
||||
pub mod gemini_cli;
|
||||
mod gemini_files;
|
||||
mod generic_oauth;
|
||||
pub mod grok;
|
||||
@@ -40,6 +41,14 @@ pub use diagnostics::{
|
||||
append_transport_diagnostics_to_value, build_request_trace_proxy_value,
|
||||
build_transport_diagnostics,
|
||||
};
|
||||
pub use gemini_cli::{
|
||||
build_gemini_cli_v1internal_request, build_gemini_cli_v1internal_url,
|
||||
classify_gemini_cli_v1internal_request_body, is_gemini_cli_provider_transport,
|
||||
resolve_local_gemini_cli_request_auth, GeminiCliRequestAuth, GeminiCliRequestAuthSupport,
|
||||
GeminiCliRequestAuthUnsupportedReason, GeminiCliRequestEnvelopeSupport,
|
||||
GeminiCliRequestEnvelopeUnsupportedReason, GeminiCliRequestUrlAction, GEMINI_CLI_PROVIDER_TYPE,
|
||||
GEMINI_CLI_V1INTERNAL_PATH_TEMPLATE,
|
||||
};
|
||||
pub use gemini_files::{
|
||||
build_gemini_files_headers, build_gemini_files_request_body, build_gemini_files_upstream_url,
|
||||
gemini_files_transport_unsupported_reason, resolve_gemini_files_auth, GeminiFilesHeadersInput,
|
||||
|
||||
@@ -10,6 +10,9 @@ use crate::antigravity::{
|
||||
AntigravityRequestUrlAction,
|
||||
};
|
||||
use crate::claude_code::build_claude_code_messages_url;
|
||||
use crate::gemini_cli::{
|
||||
build_gemini_cli_v1internal_url, is_gemini_cli_provider_transport, GeminiCliRequestUrlAction,
|
||||
};
|
||||
use crate::snapshot::GatewayProviderTransportSnapshot;
|
||||
use crate::url::{
|
||||
build_claude_messages_url, build_gemini_content_url, build_openai_chat_url,
|
||||
@@ -285,6 +288,22 @@ fn build_transport_hook_url(
|
||||
|
||||
match aether_ai_formats::normalize_api_format_alias(params.provider_api_format).as_str() {
|
||||
"gemini:generate_content" => {
|
||||
if is_gemini_cli_provider_transport(transport) {
|
||||
let query = params.request_query.map(|raw| {
|
||||
form_urlencoded::parse(raw.as_bytes())
|
||||
.into_owned()
|
||||
.collect::<BTreeMap<String, String>>()
|
||||
});
|
||||
return build_gemini_cli_v1internal_url(
|
||||
&transport.endpoint.base_url,
|
||||
if params.upstream_is_stream {
|
||||
GeminiCliRequestUrlAction::StreamGenerateContent
|
||||
} else {
|
||||
GeminiCliRequestUrlAction::GenerateContent
|
||||
},
|
||||
query.as_ref(),
|
||||
);
|
||||
}
|
||||
if let Some(auth) = resolve_local_vertex_api_key_query_auth(transport) {
|
||||
return build_vertex_api_key_gemini_content_url(
|
||||
params.mapped_model?,
|
||||
@@ -692,6 +711,58 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gemini_cli_generate_content_uses_v1internal_code_assist_url() {
|
||||
let transport = sample_transport(
|
||||
"gemini_cli",
|
||||
"gemini:generate_content",
|
||||
"https://cloudcode-pa.googleapis.com",
|
||||
None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
build_transport_request_url(
|
||||
&transport,
|
||||
TransportRequestUrlParams {
|
||||
provider_api_format: "gemini:generate_content",
|
||||
mapped_model: Some("gemini-2.5-pro"),
|
||||
upstream_is_stream: false,
|
||||
request_query: Some("key=blocked&beta=true&foo=bar"),
|
||||
kiro_api_region: None,
|
||||
},
|
||||
)
|
||||
.as_deref(),
|
||||
Some("https://cloudcode-pa.googleapis.com/v1internal:generateContent?foo=bar")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gemini_cli_stream_generate_content_uses_v1internal_code_assist_url() {
|
||||
let transport = sample_transport(
|
||||
"gemini_cli",
|
||||
"gemini:generate_content",
|
||||
"https://cloudcode-pa.googleapis.com",
|
||||
None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
build_transport_request_url(
|
||||
&transport,
|
||||
TransportRequestUrlParams {
|
||||
provider_api_format: "gemini:generate_content",
|
||||
mapped_model: Some("gemini-2.5-pro"),
|
||||
upstream_is_stream: true,
|
||||
request_query: Some("foo=bar"),
|
||||
kiro_api_region: None,
|
||||
},
|
||||
)
|
||||
.as_deref(),
|
||||
Some(
|
||||
"https://cloudcode-pa.googleapis.com/v1internal:streamGenerateContent?alt=sse&foo=bar"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vertex_gemini_embedding_batch_request_uses_vertex_predict_endpoint() {
|
||||
let mut transport = sample_transport(
|
||||
|
||||
@@ -9,6 +9,7 @@ use crate::auth::{
|
||||
};
|
||||
use crate::claude_code::build_claude_code_passthrough_headers;
|
||||
use crate::claude_code::local_claude_code_transport_unsupported_reason_with_network;
|
||||
use crate::gemini_cli::is_gemini_cli_provider_transport;
|
||||
use crate::grok::{is_grok_provider_transport, resolve_grok_session_auth};
|
||||
use crate::kiro::{
|
||||
build_kiro_provider_headers, build_kiro_provider_request_body, is_kiro_provider_transport,
|
||||
@@ -49,6 +50,7 @@ pub struct SameFormatProviderRequestBehaviorParams<'a> {
|
||||
pub struct SameFormatProviderRequestBehavior {
|
||||
pub is_antigravity: bool,
|
||||
pub is_claude_code: bool,
|
||||
pub is_gemini_cli: bool,
|
||||
pub is_vertex: bool,
|
||||
pub is_kiro: bool,
|
||||
pub upstream_is_stream: bool,
|
||||
@@ -103,6 +105,7 @@ pub fn classify_same_format_provider_request_behavior(
|
||||
params: SameFormatProviderRequestBehaviorParams<'_>,
|
||||
) -> SameFormatProviderRequestBehavior {
|
||||
let is_antigravity = is_antigravity_provider_transport(transport);
|
||||
let is_gemini_cli = is_gemini_cli_provider_transport(transport);
|
||||
let is_claude_code = transport
|
||||
.provider
|
||||
.provider_type
|
||||
@@ -125,6 +128,12 @@ pub fn classify_same_format_provider_request_behavior(
|
||||
);
|
||||
let report_kind = if is_kiro && !params.require_streaming {
|
||||
"claude_cli_sync_finalize"
|
||||
} else if is_gemini_cli && !params.require_streaming {
|
||||
match params.report_kind {
|
||||
"gemini_chat_sync_success" => "gemini_chat_sync_finalize",
|
||||
"gemini_cli_sync_success" => "gemini_cli_sync_finalize",
|
||||
_ => params.report_kind,
|
||||
}
|
||||
} else if is_antigravity && !params.require_streaming {
|
||||
match params.report_kind {
|
||||
"gemini_chat_sync_success" => "gemini_chat_sync_finalize",
|
||||
@@ -138,6 +147,7 @@ pub fn classify_same_format_provider_request_behavior(
|
||||
SameFormatProviderRequestBehavior {
|
||||
is_antigravity,
|
||||
is_claude_code,
|
||||
is_gemini_cli,
|
||||
is_vertex,
|
||||
is_kiro,
|
||||
upstream_is_stream,
|
||||
@@ -404,6 +414,7 @@ pub fn same_format_provider_transport_unsupported_reason_for_trace(
|
||||
);
|
||||
if !behavior.is_antigravity
|
||||
&& !behavior.is_claude_code
|
||||
&& !behavior.is_gemini_cli
|
||||
&& !behavior.is_vertex
|
||||
&& !behavior.is_kiro
|
||||
{
|
||||
@@ -553,6 +564,19 @@ mod tests {
|
||||
assert!(behavior.is_antigravity);
|
||||
assert!(behavior.upstream_is_stream);
|
||||
assert_eq!(behavior.report_kind, "gemini_chat_sync_finalize");
|
||||
|
||||
let gemini_cli = sample_transport("gemini_cli");
|
||||
let behavior = classify_same_format_provider_request_behavior(
|
||||
&gemini_cli,
|
||||
SameFormatProviderRequestBehaviorParams {
|
||||
require_streaming: false,
|
||||
provider_api_format: "gemini:generate_content",
|
||||
report_kind: "gemini_cli_sync_success",
|
||||
},
|
||||
);
|
||||
|
||||
assert!(!behavior.upstream_is_stream);
|
||||
assert_eq!(behavior.report_kind, "gemini_cli_sync_finalize");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -994,6 +1018,7 @@ mod tests {
|
||||
behavior: SameFormatProviderRequestBehavior {
|
||||
is_antigravity: false,
|
||||
is_claude_code: false,
|
||||
is_gemini_cli: false,
|
||||
is_vertex: false,
|
||||
is_kiro: false,
|
||||
upstream_is_stream: true,
|
||||
|
||||
Reference in New Issue
Block a user