Merge commit 'refs/pr/585'

# Conflicts:
#	apps/aether-gateway/src/ai_serving/planner/passthrough/provider/family/request.rs
#	apps/aether-gateway/src/tests/ai_execute/stream_provider_gemini/local_cli.rs
#	apps/aether-gateway/src/tests/ai_execute/sync/gemini/cli.rs
#	apps/aether-gateway/src/tests/control/admin/provider_query.rs
#	crates/aether-provider-transport/src/gemini_cli/mod.rs
#	crates/aether-provider-transport/src/gemini_cli/request.rs
#	crates/aether-provider-transport/src/gemini_cli/url.rs
#	crates/aether-provider-transport/src/lib.rs
This commit is contained in:
fawney19
2026-05-28 13:19:42 +08:00
21 changed files with 867 additions and 404 deletions
@@ -3,11 +3,11 @@ use std::sync::Arc;
use serde_json::Value;
use crate::ai_serving::transport::gemini_cli::resolve_gemini_cli_project_id;
use crate::ai_serving::transport::{
build_gemini_cli_v1internal_request, build_standard_provider_request_headers,
GatewayProviderTransportSnapshot, GeminiCliRequestEnvelopeSupport,
StandardProviderRequestHeaders, StandardProviderRequestHeadersInput, GEMINI_CLI_USER_AGENT,
GatewayProviderTransportSnapshot, GeminiCliRequestAuth, GeminiCliRequestAuthSupport,
GeminiCliRequestEnvelopeSupport, StandardProviderRequestHeaders,
StandardProviderRequestHeadersInput, GEMINI_CLI_USER_AGENT,
};
use crate::AppState;
@@ -101,24 +101,33 @@ async fn build_gemini_cli_v1internal_payload(
gemini_request_body: &Value,
) -> Result<GeminiCliV1InternalPayload, GeminiCliV1InternalRequestError> {
let mut resolved_transport = Arc::clone(transport);
let project_id = match resolve_gemini_cli_project_id(&resolved_transport) {
Some(project_id) => Some(project_id),
None => match state
let mut auth = match crate::ai_serving::transport::resolve_local_gemini_cli_request_auth(
&resolved_transport,
) {
GeminiCliRequestAuthSupport::Supported(auth) => auth,
GeminiCliRequestAuthSupport::Unsupported(_) => {
return Err(GeminiCliV1InternalRequestError::ProjectUnavailable);
}
};
if auth.project_id.is_none() {
auth = match state
.hydrate_gemini_cli_project_metadata_for_transport(&resolved_transport)
.await
{
Some(hydrated) => {
let project_id = resolve_gemini_cli_project_id(&hydrated);
resolved_transport = Arc::new(hydrated);
project_id
match crate::ai_serving::transport::resolve_local_gemini_cli_request_auth(
&resolved_transport,
) {
GeminiCliRequestAuthSupport::Supported(auth) => auth,
GeminiCliRequestAuthSupport::Unsupported(_) => GeminiCliRequestAuth::default(),
}
}
None => None,
},
None => GeminiCliRequestAuth::default(),
};
}
.ok_or(GeminiCliV1InternalRequestError::ProjectUnavailable)?;
let body = match build_gemini_cli_v1internal_request(
&project_id,
&auth,
trace_id,
mapped_model,
gemini_request_body,
@@ -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(),
@@ -15,11 +15,11 @@ use crate::ai_serving::transport::antigravity::{
classify_local_antigravity_request_support, AntigravityEnvelopeRequestType,
AntigravityRequestEnvelopeSupport, AntigravityRequestSideSupport,
};
use crate::ai_serving::transport::gemini_cli::resolve_gemini_cli_project_id;
use crate::ai_serving::transport::{
build_gemini_cli_v1internal_request, build_grok_browser_headers, build_grok_upstream_url,
build_same_format_provider_headers, GeminiCliRequestEnvelopeSupport, GrokHeaderInput,
SameFormatProviderHeadersInput, GEMINI_CLI_USER_AGENT, GROK_CHAT_PATH,
build_same_format_provider_headers, resolve_local_gemini_cli_request_auth,
GeminiCliRequestAuth, GeminiCliRequestAuthSupport, GeminiCliRequestEnvelopeSupport,
GrokHeaderInput, SameFormatProviderHeadersInput, GEMINI_CLI_USER_AGENT, GROK_CHAT_PATH,
};
use crate::ai_serving::{CandidateFailureDiagnostic, GatewayProviderTransportSnapshot};
use crate::{AppState, GatewayError};
@@ -71,6 +71,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
{
@@ -238,35 +239,41 @@ pub(crate) async fn resolve_local_same_format_provider_candidate_payload_parts(
} else {
None
};
let gemini_cli_project_id = if prepared.behavior.is_gemini_cli {
match resolve_gemini_cli_project_id(&transport) {
Some(project_id) => Some(project_id),
None => {
match state
.hydrate_gemini_cli_project_metadata_for_transport(&transport)
.await
{
Some(hydrated) => {
let project_id = resolve_gemini_cli_project_id(&hydrated);
transport = Arc::new(hydrated);
project_id
}
None => {
mark_skipped_local_same_format_provider_candidate(
state,
input,
trace_id,
candidate,
attempt.candidate_index,
&attempt.candidate_id,
"transport_auth_unavailable",
)
.await;
return Ok(None);
let gemini_cli_auth = if prepared.behavior.is_gemini_cli {
let mut auth = match resolve_local_gemini_cli_request_auth(&transport) {
GeminiCliRequestAuthSupport::Supported(auth) => auth,
GeminiCliRequestAuthSupport::Unsupported(_) => {
mark_skipped_local_same_format_provider_candidate(
state,
input,
trace_id,
candidate,
attempt.candidate_index,
&attempt.candidate_id,
"transport_auth_unavailable",
)
.await;
return Ok(None);
}
};
if auth.project_id.is_none() {
auth = match state
.hydrate_gemini_cli_project_metadata_for_transport(&transport)
.await
{
Some(hydrated) => {
transport = Arc::new(hydrated);
match resolve_local_gemini_cli_request_auth(&transport) {
GeminiCliRequestAuthSupport::Supported(auth) => auth,
GeminiCliRequestAuthSupport::Unsupported(_) => {
GeminiCliRequestAuth::default()
}
}
}
}
None => GeminiCliRequestAuth::default(),
};
}
Some(auth)
} else {
None
};
@@ -299,9 +306,9 @@ pub(crate) async fn resolve_local_same_format_provider_candidate_payload_parts(
return Ok(None);
}
}
} else if let Some(project_id) = gemini_cli_project_id.as_deref() {
} else if let Some(gemini_cli_auth) = gemini_cli_auth.as_ref() {
match build_gemini_cli_v1internal_request(
project_id,
gemini_cli_auth,
trace_id,
&prepared.mapped_model,
&base_provider_request_body,
@@ -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,
@@ -131,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(
@@ -445,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,
@@ -479,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());
@@ -116,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
@@ -145,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,
+21 -18
View File
@@ -90,25 +90,28 @@ pub(crate) use aether_provider_transport::{
openai_image_transport_unsupported_reason, request_conversion_direct_auth,
request_conversion_enabled_for_transport, request_conversion_transport_supported,
request_conversion_transport_unsupported_reason, request_pair_allowed_for_transport,
request_pair_direct_auth, request_pair_transport_unsupported_reason, resolve_gemini_files_auth,
resolve_grok_session_auth, resolve_openai_image_auth, resolve_same_format_provider_direct_auth,
resolve_transport_execution_timeouts, resolve_transport_profile,
resolve_transport_proxy_snapshot, resolve_transport_proxy_snapshot_with_tunnel_affinity,
resolve_video_create_auth, same_format_provider_transport_supported,
same_format_provider_transport_unsupported_reason, should_skip_upstream_passthrough_header,
should_try_same_format_provider_oauth_auth, supports_local_gemini_transport_with_network,
request_pair_direct_auth, request_pair_transport_unsupported_reason,
resolve_gemini_cli_project_id, resolve_gemini_files_auth, resolve_grok_session_auth,
resolve_local_gemini_cli_request_auth, resolve_openai_image_auth,
resolve_same_format_provider_direct_auth, resolve_transport_execution_timeouts,
resolve_transport_profile, resolve_transport_proxy_snapshot,
resolve_transport_proxy_snapshot_with_tunnel_affinity, resolve_video_create_auth,
same_format_provider_transport_supported, same_format_provider_transport_unsupported_reason,
should_skip_upstream_passthrough_header, should_try_same_format_provider_oauth_auth,
supports_local_gemini_transport_with_network,
supports_local_generic_oauth_request_auth_resolution,
supports_local_oauth_request_auth_resolution, transport_proxy_is_locally_supported,
video_create_transport_unsupported_reason, CandidateTransportPolicyFacts,
GatewayProviderTransportSnapshot, GeminiCliRequestEnvelopeSupport, GeminiFilesHeadersInput,
GeminiFilesRequestBodyError, GeminiFilesRequestBodyParts, GrokHeaderInput,
LocalResolvedOAuthRequestAuth, ProviderOpenAiImageHeadersInput, ProviderVideoCreateFamily,
ProviderVideoCreateHeadersInput, SameFormatProviderFamily, SameFormatProviderHeadersInput,
SameFormatProviderRequestBehavior, SameFormatProviderRequestBehaviorParams,
SameFormatProviderRequestBodyInput, SameFormatProviderUpstreamUrlParams,
StandardPlanFallbackAcceptPolicy, StandardPlanFallbackHeadersInput,
StandardProviderRequestHeaders, StandardProviderRequestHeadersInput,
TransportRequestBodySemanticsError, TransportRequestUrlParams, GEMINI_CLI_USER_AGENT,
GEMINI_CLI_V1INTERNAL_ENVELOPE_NAME, GROK_CHAT_PATH, GROK_INTERNAL_HEADER,
GROK_RATE_LIMITS_PATH, WINDSURF_ENVELOPE_NAME,
GatewayProviderTransportSnapshot, GeminiCliRequestAuth, GeminiCliRequestAuthSupport,
GeminiCliRequestAuthUnsupportedReason, GeminiCliRequestEnvelopeSupport,
GeminiFilesHeadersInput, GeminiFilesRequestBodyError, GeminiFilesRequestBodyParts,
GrokHeaderInput, LocalResolvedOAuthRequestAuth, ProviderOpenAiImageHeadersInput,
ProviderVideoCreateFamily, ProviderVideoCreateHeadersInput, SameFormatProviderFamily,
SameFormatProviderHeadersInput, SameFormatProviderRequestBehavior,
SameFormatProviderRequestBehaviorParams, SameFormatProviderRequestBodyInput,
SameFormatProviderUpstreamUrlParams, StandardPlanFallbackAcceptPolicy,
StandardPlanFallbackHeadersInput, StandardProviderRequestHeaders,
StandardProviderRequestHeadersInput, TransportRequestBodySemanticsError,
TransportRequestUrlParams, GEMINI_CLI_USER_AGENT, GEMINI_CLI_V1INTERNAL_ENVELOPE_NAME,
GROK_CHAT_PATH, GROK_INTERNAL_HEADER, GROK_RATE_LIMITS_PATH, WINDSURF_ENVELOPE_NAME,
};