Fix Gemini CLI OpenAI conversion envelope

This commit is contained in:
Mas0nShi
2026-05-22 18:40:11 +08:00
parent c67818ee86
commit 8cc6888c5b
5 changed files with 697 additions and 49 deletions
@@ -0,0 +1,62 @@
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, GatewayProviderTransportSnapshot,
GeminiCliRequestEnvelopeSupport,
};
use crate::AppState;
pub(crate) struct GeminiCliV1InternalPayload {
pub(crate) transport: Arc<GatewayProviderTransportSnapshot>,
pub(crate) body: Value,
}
pub(crate) enum GeminiCliV1InternalPayloadError {
ProjectUnavailable,
EnvelopeUnsupported,
}
pub(crate) async fn build_gemini_cli_v1internal_payload(
state: &AppState,
transport: &Arc<GatewayProviderTransportSnapshot>,
trace_id: &str,
mapped_model: &str,
gemini_request_body: &Value,
) -> Result<GeminiCliV1InternalPayload, GeminiCliV1InternalPayloadError> {
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
.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
}
None => None,
},
}
.ok_or(GeminiCliV1InternalPayloadError::ProjectUnavailable)?;
let body = match build_gemini_cli_v1internal_request(
&project_id,
trace_id,
mapped_model,
gemini_request_body,
) {
GeminiCliRequestEnvelopeSupport::Supported(envelope) => envelope,
GeminiCliRequestEnvelopeSupport::Unsupported(_) => {
return Err(GeminiCliV1InternalPayloadError::EnvelopeUnsupported);
}
};
Ok(GeminiCliV1InternalPayload {
transport: resolved_transport,
body,
})
}
@@ -12,6 +12,7 @@ mod candidate_transport_ranking_facts;
mod common;
mod decision;
mod decision_input;
mod gemini_cli;
mod materialization_policy;
mod passthrough;
mod plan_builders;
@@ -12,27 +12,28 @@ use crate::ai_serving::planner::common::{
endpoint_config_forces_body_stream_field, enforce_provider_body_stream_policy,
request_requires_body_stream_field, resolve_upstream_is_stream_for_provider,
};
use crate::ai_serving::planner::gemini_cli::{
build_gemini_cli_v1internal_payload, GeminiCliV1InternalPayloadError,
};
use crate::ai_serving::planner::spec_metadata::local_standard_spec_metadata;
use crate::ai_serving::planner::standard::{
apply_codex_openai_responses_special_headers, request_body_build_failure_extra_data,
};
use crate::ai_serving::transport::gemini_cli::resolve_gemini_cli_project_id;
use crate::ai_serving::transport::kiro::{
build_kiro_provider_headers, build_kiro_provider_request_body,
is_kiro_claude_messages_transport, KiroProviderHeadersInput, KiroRequestAuth,
KIRO_ENVELOPE_NAME,
};
use crate::ai_serving::transport::{
build_gemini_cli_v1internal_request, build_grok_browser_headers, build_grok_upstream_url,
build_kiro_cross_format_upstream_url, build_openai_image_headers,
build_openai_image_upstream_url, build_standard_provider_request_headers,
build_windsurf_cascade_headers, build_windsurf_cascade_request_body,
build_windsurf_cascade_upstream_url, is_gemini_cli_provider_transport,
is_windsurf_provider_transport,
build_grok_browser_headers, build_grok_upstream_url, build_kiro_cross_format_upstream_url,
build_openai_image_headers, build_openai_image_upstream_url,
build_standard_provider_request_headers, build_windsurf_cascade_headers,
build_windsurf_cascade_request_body, build_windsurf_cascade_upstream_url,
is_gemini_cli_provider_transport, is_windsurf_provider_transport,
local_windsurf_request_transport_unsupported_reason_with_network,
openai_image_transport_unsupported_reason, resolve_grok_session_auth,
resolve_openai_image_auth, GeminiCliRequestEnvelopeSupport, GrokHeaderInput,
ProviderOpenAiImageHeadersInput, StandardProviderRequestHeadersInput, GEMINI_CLI_USER_AGENT,
resolve_openai_image_auth, GrokHeaderInput, ProviderOpenAiImageHeadersInput,
StandardProviderRequestHeadersInput, GEMINI_CLI_USER_AGENT,
GEMINI_CLI_V1INTERNAL_ENVELOPE_NAME, GROK_CHAT_PATH, WINDSURF_ENVELOPE_NAME,
};
use crate::ai_serving::{
@@ -755,43 +756,30 @@ async fn build_gemini_cli_cross_format_payload_parts(
) -> Option<LocalStandardCandidatePayloadParts> {
let candidate = &attempt.eligible.candidate;
let effective_headers = input.effective_headers(&parts.headers);
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
.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
}
None => None,
},
};
let Some(project_id) = project_id else {
mark_skipped_local_standard_candidate(
state,
input,
trace_id,
candidate,
attempt.candidate_index,
&attempt.candidate_id,
"transport_auth_unavailable",
)
.await;
return None;
};
let provider_request_body = match build_gemini_cli_v1internal_request(
&project_id,
let resolved = match build_gemini_cli_v1internal_payload(
state,
transport,
trace_id,
&mapped_model,
&gemini_request_body,
) {
GeminiCliRequestEnvelopeSupport::Supported(envelope) => envelope,
GeminiCliRequestEnvelopeSupport::Unsupported(_) => {
)
.await
{
Ok(resolved) => resolved,
Err(GeminiCliV1InternalPayloadError::ProjectUnavailable) => {
mark_skipped_local_standard_candidate(
state,
input,
trace_id,
candidate,
attempt.candidate_index,
&attempt.candidate_id,
"transport_auth_unavailable",
)
.await;
return None;
}
Err(GeminiCliV1InternalPayloadError::EnvelopeUnsupported) => {
mark_skipped_local_standard_candidate_with_extra_data(
state,
input,
@@ -810,6 +798,8 @@ async fn build_gemini_cli_cross_format_payload_parts(
return None;
}
};
let provider_request_body = resolved.body;
let resolved_transport = resolved.transport;
let upstream_url = match crate::ai_serving::planner::standard::build_standard_upstream_url(
parts,
@@ -15,6 +15,9 @@ use crate::ai_serving::planner::common::{
endpoint_config_forces_body_stream_field, enforce_provider_body_stream_policy,
request_requires_body_stream_field, OPENAI_CHAT_STREAM_PLAN_KIND,
};
use crate::ai_serving::planner::gemini_cli::{
build_gemini_cli_v1internal_payload, GeminiCliV1InternalPayloadError,
};
use crate::ai_serving::planner::standard::{
apply_codex_openai_responses_special_body_edits, apply_codex_openai_responses_special_headers,
build_cross_format_openai_chat_request_body, build_cross_format_openai_chat_upstream_url,
@@ -37,9 +40,10 @@ use crate::ai_serving::transport::windsurf::{
use crate::ai_serving::transport::{
build_grok_browser_headers, build_grok_upstream_url, build_kiro_cross_format_upstream_url,
build_openai_image_headers, build_openai_image_upstream_url,
build_standard_provider_request_headers, openai_image_transport_unsupported_reason,
resolve_openai_image_auth, GrokHeaderInput, ProviderOpenAiImageHeadersInput,
StandardProviderRequestHeadersInput, GROK_CHAT_PATH,
build_standard_provider_request_headers, is_gemini_cli_provider_transport,
openai_image_transport_unsupported_reason, resolve_openai_image_auth, GrokHeaderInput,
ProviderOpenAiImageHeadersInput, StandardProviderRequestHeadersInput, GEMINI_CLI_USER_AGENT,
GEMINI_CLI_V1INTERNAL_ENVELOPE_NAME, GROK_CHAT_PATH,
};
use crate::ai_serving::{
ai_local_execution_contract_for_formats, request_conversion_direct_auth,
@@ -731,6 +735,30 @@ pub(crate) async fn resolve_local_openai_chat_candidate_payload_parts(
)
.await);
}
if provider_api_format == "gemini:generate_content"
&& is_gemini_cli_provider_transport(transport)
{
return Ok(build_gemini_cli_openai_chat_cross_format_payload_parts(
state,
parts,
trace_id,
body_json,
input,
eligible,
candidate_index,
candidate_id,
decision_kind,
transport,
provider_api_format.as_str(),
prepared_candidate.mapped_model,
prepared_candidate.auth_header,
prepared_candidate.auth_value,
provider_request_body,
upstream_is_stream,
redaction.redacted,
)
.await);
}
let Some(upstream_url) = build_cross_format_openai_chat_upstream_url(
parts,
@@ -831,6 +859,175 @@ pub(crate) async fn resolve_local_openai_chat_candidate_payload_parts(
}))
}
#[allow(clippy::too_many_arguments)]
async fn build_gemini_cli_openai_chat_cross_format_payload_parts(
state: &AppState,
parts: &http::request::Parts,
trace_id: &str,
original_body_json: &serde_json::Value,
input: &LocalOpenAiChatDecisionInput,
eligible: &EligibleLocalExecutionCandidate,
candidate_index: u32,
candidate_id: &str,
decision_kind: &str,
transport: &Arc<GatewayProviderTransportSnapshot>,
provider_api_format: &str,
mapped_model: String,
auth_header: String,
auth_value: String,
gemini_request_body: Value,
upstream_is_stream: bool,
request_redacted: bool,
) -> Option<LocalOpenAiChatCandidatePayloadParts> {
let candidate = &eligible.candidate;
let effective_headers = input.effective_headers(&parts.headers);
let resolved = match build_gemini_cli_v1internal_payload(
state,
transport,
trace_id,
&mapped_model,
&gemini_request_body,
)
.await
{
Ok(resolved) => resolved,
Err(GeminiCliV1InternalPayloadError::ProjectUnavailable) => {
mark_skipped_local_openai_chat_candidate(
state,
input,
trace_id,
candidate,
candidate_index,
candidate_id,
"transport_auth_unavailable",
)
.await;
return None;
}
Err(GeminiCliV1InternalPayloadError::EnvelopeUnsupported) => {
mark_skipped_local_openai_chat_candidate_with_extra_data(
state,
input,
trace_id,
candidate,
candidate_index,
candidate_id,
"provider_request_body_build_failed",
request_body_build_failure_extra_data(
original_body_json,
"openai:chat",
provider_api_format,
),
)
.await;
return None;
}
};
let provider_request_body = resolved.body;
let resolved_transport = resolved.transport;
let Some(upstream_url) = build_cross_format_openai_chat_upstream_url(
parts,
&resolved_transport,
&mapped_model,
provider_api_format,
upstream_is_stream,
) else {
mark_skipped_local_openai_chat_candidate_with_failure_diagnostic(
state,
input,
trace_id,
candidate,
candidate_index,
candidate_id,
"upstream_url_missing",
CandidateFailureDiagnostic::upstream_url_missing(
"openai:chat",
provider_api_format,
"openai_chat_gemini_cli_url",
),
)
.await;
return None;
};
let extra_headers =
BTreeMap::from([("user-agent".to_string(), GEMINI_CLI_USER_AGENT.to_string())]);
let Some(resolved_headers) =
build_standard_provider_request_headers(StandardProviderRequestHeadersInput {
transport: &resolved_transport,
provider_api_format,
same_format: false,
headers: effective_headers,
auth_header: &auth_header,
auth_value: &auth_value,
extra_headers: &extra_headers,
header_rules: resolved_transport.endpoint.header_rules.as_ref(),
provider_request_body: &provider_request_body,
original_request_body: original_body_json,
upstream_is_stream,
})
else {
mark_skipped_local_openai_chat_candidate_with_failure_diagnostic(
state,
input,
trace_id,
candidate,
candidate_index,
candidate_id,
"transport_header_rules_apply_failed",
CandidateFailureDiagnostic::header_rules_apply_failed(
"openai:chat",
provider_api_format,
"openai_chat_gemini_cli_headers",
),
)
.await;
return None;
};
let mut provider_request_headers = resolved_headers.headers;
apply_codex_openai_responses_special_headers(
&mut provider_request_headers,
&provider_request_body,
effective_headers,
resolved_transport.provider.provider_type.as_str(),
provider_api_format,
Some(trace_id),
resolved_transport.key.decrypted_auth_config.as_deref(),
);
request_identity_response_encoding_when_redacted(
&mut provider_request_headers,
request_redacted,
);
let resolved_report_kind = if decision_kind == OPENAI_CHAT_STREAM_PLAN_KIND {
"openai_chat_stream_success".to_string()
} else {
"openai_chat_sync_finalize".to_string()
};
let (execution_strategy, conversion_mode) =
ai_local_execution_contract_for_formats("openai:chat", provider_api_format);
Some(LocalOpenAiChatCandidatePayloadParts {
client_api_format: "openai:chat".to_string(),
auth_header: resolved_headers.auth_header,
auth_value: resolved_headers.auth_value,
mapped_model,
provider_api_format: provider_api_format.to_string(),
provider_request_body,
provider_request_headers,
upstream_url,
execution_strategy,
conversion_mode,
report_kind: resolved_report_kind,
envelope_name: Some(GEMINI_CLI_V1INTERNAL_ENVELOPE_NAME),
transport: resolved_transport,
request_redacted,
transport_profile: None,
image_request_summary: None,
})
}
#[allow(clippy::too_many_arguments)]
async fn resolve_openai_chat_to_openai_image_payload_parts(
state: &AppState,
@@ -1680,6 +1877,218 @@ fn redaction_mask_error_to_gateway_error(error: RedactionMaskError) -> GatewayEr
#[cfg(test)]
mod tests {
use super::*;
use aether_provider_transport::snapshot::{
GatewayProviderTransportEndpoint, GatewayProviderTransportKey,
GatewayProviderTransportProvider, GatewayProviderTransportSnapshot,
};
use aether_scheduler_core::SchedulerMinimalCandidateSelectionCandidate;
fn sample_auth_snapshot() -> crate::ai_serving::GatewayAuthApiKeySnapshot {
crate::ai_serving::GatewayAuthApiKeySnapshot {
user_id: "user-1".to_string(),
username: "alice".to_string(),
email: None,
user_role: "user".to_string(),
user_auth_source: "local".to_string(),
user_is_active: true,
user_is_deleted: false,
user_rate_limit: None,
user_allowed_providers: None,
user_allowed_api_formats: None,
user_allowed_models: None,
api_key_id: "api-key-1".to_string(),
api_key_name: Some("default".to_string()),
api_key_is_active: true,
api_key_is_locked: false,
api_key_is_standalone: false,
api_key_rate_limit: None,
api_key_concurrent_limit: None,
api_key_expires_at_unix_secs: None,
api_key_allowed_providers: None,
api_key_allowed_api_formats: None,
api_key_allowed_models: None,
api_key_ip_rules: None,
currently_usable: true,
}
}
fn sample_input() -> LocalOpenAiChatDecisionInput {
LocalOpenAiChatDecisionInput {
auth_context: crate::ai_serving::ExecutionRuntimeAuthContext {
user_id: "user-1".to_string(),
api_key_id: "api-key-1".to_string(),
username: Some("alice".to_string()),
api_key_name: Some("default".to_string()),
balance_remaining: Some(10.0),
access_allowed: true,
api_key_is_standalone: false,
},
requested_model: "gemini-2.5-pro".to_string(),
auth_snapshot: sample_auth_snapshot(),
required_capabilities: None,
request_auth_channel: None,
client_session_affinity: None,
routing_policy: None,
routing_trace_seed: None,
routing_context: None,
}
}
fn sample_gemini_cli_transport() -> GatewayProviderTransportSnapshot {
GatewayProviderTransportSnapshot {
provider: GatewayProviderTransportProvider {
id: "provider-1".to_string(),
name: "gemini".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: Some("gemini".to_string()),
endpoint_kind: Some("generate_content".to_string()),
is_active: true,
base_url: "https://cloudcode-pa.googleapis.com".to_string(),
header_rules: None,
body_rules: None,
max_retries: None,
custom_path: Some("/v1internal:{action}".to_string()),
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: "bearer".to_string(),
is_active: true,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
auth_type_by_format: None,
allow_auth_channel_mismatch_formats: None,
allowed_models: None,
capabilities: None,
rate_multipliers: None,
global_priority_by_format: Some(json!({
"gemini:generate_content": 1,
})),
expires_at_unix_secs: None,
proxy: None,
fingerprint: None,
upstream_metadata: Some(json!({
"gemini_cli": {
"project_id": "test-project"
}
})),
decrypted_api_key: "oauth-access-token".to_string(),
decrypted_auth_config: None,
},
}
}
fn sample_gemini_cli_eligible() -> EligibleLocalExecutionCandidate {
EligibleLocalExecutionCandidate {
kind: crate::ai_serving::planner::candidate_resolution::LocalExecutionCandidateKind::SingleKey,
candidate: SchedulerMinimalCandidateSelectionCandidate {
provider_id: "provider-1".to_string(),
provider_name: "gemini".to_string(),
provider_type: "gemini_cli".to_string(),
provider_priority: 1,
endpoint_id: "endpoint-1".to_string(),
endpoint_api_format: "gemini:generate_content".to_string(),
key_id: "key-1".to_string(),
key_name: "key".to_string(),
key_auth_type: "bearer".to_string(),
key_internal_priority: 1,
key_global_priority_for_format: Some(1),
key_capabilities: None,
model_id: "model-1".to_string(),
global_model_id: "global-model-1".to_string(),
global_model_name: "gemini-2.5-pro".to_string(),
selected_provider_model_name: "gemini-2.5-pro".to_string(),
mapping_matched_model: None,
},
transport: Arc::new(sample_gemini_cli_transport()),
provider_api_format: "gemini:generate_content".to_string(),
orchestration: crate::orchestration::LocalExecutionCandidateMetadata::default(),
ranking: None,
}
}
#[tokio::test]
async fn openai_chat_to_gemini_cli_wraps_cross_format_body_in_v1internal_envelope() {
let state = AppState::new().expect("state should build");
let request = http::Request::builder()
.method("POST")
.uri("/v1/chat/completions")
.header(http::header::CONTENT_TYPE, "application/json")
.body(())
.expect("request should build");
let (parts, _) = request.into_parts();
let body_json = json!({
"model": "gemini-2.5-pro",
"messages": [{"role": "user", "content": "hello"}],
"generationConfig": {"temperature": 0.2},
"stream": true
});
let payload = resolve_local_openai_chat_candidate_payload_parts(
&state,
&parts,
"trace-openai-chat-gemini-cli",
&body_json,
&sample_input(),
&sample_gemini_cli_eligible(),
0,
"candidate-0",
OPENAI_CHAT_STREAM_PLAN_KIND,
"openai_chat_stream_success",
true,
)
.await
.expect("candidate resolution should not fail")
.expect("gemini_cli candidate should build a payload");
assert_eq!(
payload.upstream_url,
"https://cloudcode-pa.googleapis.com/v1internal:streamGenerateContent?alt=sse"
);
assert_eq!(
payload.envelope_name,
Some(GEMINI_CLI_V1INTERNAL_ENVELOPE_NAME)
);
assert_eq!(
payload
.provider_request_headers
.get("user-agent")
.map(String::as_str),
Some(GEMINI_CLI_USER_AGENT)
);
assert_eq!(payload.provider_request_body["model"], "gemini-2.5-pro");
assert_eq!(payload.provider_request_body["project"], "test-project");
assert_eq!(
payload.provider_request_body["user_prompt_id"],
"trace-openai-chat-gemini-cli"
);
assert!(payload.provider_request_body.get("contents").is_none());
assert!(payload
.provider_request_body
.get("generationConfig")
.is_none());
assert!(payload.provider_request_body["request"]
.get("contents")
.is_some());
}
#[test]
fn chatgpt_web_chat_image_bridge_body_uses_internal_web_shape() {
@@ -14,6 +14,9 @@ use crate::ai_serving::planner::common::{
endpoint_config_forces_body_stream_field, enforce_provider_body_stream_policy,
request_requires_body_stream_field, resolve_upstream_is_stream_for_provider,
};
use crate::ai_serving::planner::gemini_cli::{
build_gemini_cli_v1internal_payload, GeminiCliV1InternalPayloadError,
};
use crate::ai_serving::planner::spec_metadata::local_openai_responses_spec_metadata;
use crate::ai_serving::planner::standard::{
apply_codex_openai_responses_special_body_edits, apply_codex_openai_responses_special_headers,
@@ -41,11 +44,12 @@ use crate::ai_serving::transport::{
build_openai_image_headers, build_openai_image_upstream_url,
build_standard_provider_request_headers, build_windsurf_cascade_headers,
build_windsurf_cascade_request_body, build_windsurf_cascade_upstream_url,
is_windsurf_provider_transport, local_standard_transport_unsupported_reason_with_network,
is_gemini_cli_provider_transport, is_windsurf_provider_transport,
local_standard_transport_unsupported_reason_with_network,
local_windsurf_request_transport_unsupported_reason_with_network,
openai_image_transport_unsupported_reason, resolve_openai_image_auth, GrokHeaderInput,
ProviderOpenAiImageHeadersInput, StandardProviderRequestHeadersInput, GROK_CHAT_PATH,
WINDSURF_ENVELOPE_NAME,
ProviderOpenAiImageHeadersInput, StandardProviderRequestHeadersInput, GEMINI_CLI_USER_AGENT,
GEMINI_CLI_V1INTERNAL_ENVELOPE_NAME, GROK_CHAT_PATH, WINDSURF_ENVELOPE_NAME,
};
use crate::ai_serving::{
ai_local_execution_contract_for_formats, request_conversion_direct_auth,
@@ -475,6 +479,29 @@ pub(crate) async fn resolve_local_openai_responses_candidate_payload_parts(
)
.await;
}
if provider_api_format == "gemini:generate_content"
&& is_gemini_cli_provider_transport(transport)
{
return build_gemini_cli_openai_responses_payload_parts(
state,
parts,
trace_id,
body_json,
input,
eligible,
candidate_index,
candidate_id,
spec_metadata.api_format,
transport,
provider_api_format,
mapped_model,
auth_header,
auth_value,
provider_request_body,
upstream_is_stream,
)
.await;
}
let Some(upstream_url) = (if is_grok && is_grok_text_provider_api_format(provider_api_format) {
Some(build_grok_upstream_url(transport, GROK_CHAT_PATH))
@@ -647,6 +674,165 @@ pub(crate) async fn resolve_local_openai_responses_candidate_payload_parts(
})
}
#[allow(clippy::too_many_arguments)]
async fn build_gemini_cli_openai_responses_payload_parts(
state: &AppState,
parts: &http::request::Parts,
trace_id: &str,
original_body_json: &serde_json::Value,
input: &LocalOpenAiResponsesDecisionInput,
eligible: &EligibleLocalExecutionCandidate,
candidate_index: u32,
candidate_id: &str,
client_api_format: &str,
transport: &Arc<GatewayProviderTransportSnapshot>,
provider_api_format: &str,
mapped_model: String,
auth_header: String,
auth_value: String,
gemini_request_body: Value,
upstream_is_stream: bool,
) -> Option<LocalOpenAiResponsesCandidatePayloadParts> {
let candidate = &eligible.candidate;
let effective_headers = input.effective_headers(&parts.headers);
let resolved = match build_gemini_cli_v1internal_payload(
state,
transport,
trace_id,
&mapped_model,
&gemini_request_body,
)
.await
{
Ok(resolved) => resolved,
Err(GeminiCliV1InternalPayloadError::ProjectUnavailable) => {
mark_skipped_local_openai_responses_candidate(
state,
input,
trace_id,
candidate,
candidate_index,
candidate_id,
"transport_auth_unavailable",
)
.await;
return None;
}
Err(GeminiCliV1InternalPayloadError::EnvelopeUnsupported) => {
mark_skipped_local_openai_responses_candidate_with_extra_data(
state,
input,
trace_id,
candidate,
candidate_index,
candidate_id,
"provider_request_body_build_failed",
request_body_build_failure_extra_data(
original_body_json,
client_api_format,
provider_api_format,
),
)
.await;
return None;
}
};
let provider_request_body = resolved.body;
let resolved_transport = resolved.transport;
let Some(upstream_url) = build_cross_format_openai_responses_upstream_url(
parts,
&resolved_transport,
&mapped_model,
client_api_format,
provider_api_format,
upstream_is_stream,
) else {
mark_skipped_local_openai_responses_candidate_with_failure_diagnostic(
state,
input,
trace_id,
candidate,
candidate_index,
candidate_id,
"upstream_url_missing",
CandidateFailureDiagnostic::upstream_url_missing(
client_api_format,
provider_api_format,
"openai_responses_gemini_cli_url",
),
)
.await;
return None;
};
let extra_headers =
BTreeMap::from([("user-agent".to_string(), GEMINI_CLI_USER_AGENT.to_string())]);
let Some(resolved_headers) =
build_standard_provider_request_headers(StandardProviderRequestHeadersInput {
transport: &resolved_transport,
provider_api_format,
same_format: false,
headers: effective_headers,
auth_header: &auth_header,
auth_value: &auth_value,
extra_headers: &extra_headers,
header_rules: resolved_transport.endpoint.header_rules.as_ref(),
provider_request_body: &provider_request_body,
original_request_body: original_body_json,
upstream_is_stream,
})
else {
mark_skipped_local_openai_responses_candidate_with_failure_diagnostic(
state,
input,
trace_id,
candidate,
candidate_index,
candidate_id,
"transport_header_rules_apply_failed",
CandidateFailureDiagnostic::header_rules_apply_failed(
client_api_format,
provider_api_format,
"openai_responses_gemini_cli_headers",
),
)
.await;
return None;
};
let mut provider_request_headers = resolved_headers.headers;
apply_codex_openai_responses_special_headers(
&mut provider_request_headers,
&provider_request_body,
effective_headers,
resolved_transport.provider.provider_type.as_str(),
provider_api_format,
Some(trace_id),
resolved_transport.key.decrypted_auth_config.as_deref(),
);
let (execution_strategy, conversion_mode) =
ai_local_execution_contract_for_formats(client_api_format, provider_api_format);
Some(LocalOpenAiResponsesCandidatePayloadParts {
auth_header: resolved_headers.auth_header,
auth_value: resolved_headers.auth_value,
mapped_model,
provider_api_format: provider_api_format.to_string(),
provider_request_body,
provider_request_headers,
upstream_url,
execution_strategy,
conversion_mode,
is_antigravity: false,
envelope_name: Some(GEMINI_CLI_V1INTERNAL_ENVELOPE_NAME),
upstream_is_stream,
transport: resolved_transport,
transport_profile: None,
image_request_summary: None,
})
}
#[allow(clippy::too_many_arguments)]
async fn build_windsurf_openai_responses_payload_parts(
state: &AppState,