feat(openai): align GPT-5.6 and Codex request contracts

This commit is contained in:
MMEXA
2026-07-11 07:40:12 +08:00
parent bc1da3bf3f
commit dfa121dd5b
178 changed files with 17947 additions and 3541 deletions
@@ -7,7 +7,7 @@ use crate::ai_serving::planner::report_context::{
};
use crate::ai_serving::planner::spec_metadata::local_gemini_files_spec_metadata;
use crate::ai_serving::planner::{
build_ai_execution_decision_response, resolve_transport_request_gzip_policy,
build_ai_execution_decision_response, resolve_transport_request_encoding_policy,
AiExecutionDecisionResponseParts,
};
use crate::ai_serving::transport::{
@@ -124,7 +124,7 @@ pub(super) async fn maybe_build_local_gemini_files_decision_payload_for_candidat
upstream_url,
file_name: _,
} = resolved;
let request_gzip = resolve_transport_request_gzip_policy(&transport);
let request_encoding = resolve_transport_request_encoding_policy(&transport);
let mut decision = build_ai_execution_decision_response(AiExecutionDecisionResponseParts {
decision_is_stream: spec_metadata.require_streaming,
@@ -134,6 +134,7 @@ pub(super) async fn maybe_build_local_gemini_files_decision_payload_for_candidat
request_id: trace_id.to_string(),
candidate_id: candidate_id.clone(),
provider_name: transport.provider.name.clone(),
provider_type: transport.provider.provider_type.clone(),
provider_id: candidate.provider_id.clone(),
endpoint_id: candidate.endpoint_id.clone(),
key_id: candidate.key_id.clone(),
@@ -156,8 +157,8 @@ pub(super) async fn maybe_build_local_gemini_files_decision_payload_for_candidat
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned),
content_encoding: None,
request_gzip,
content_encoding: request_encoding.content_encoding,
request_gzip: request_encoding.request_gzip,
proxy,
transport_profile,
timeouts: resolve_transport_execution_timeouts(&transport),
@@ -166,6 +167,10 @@ pub(super) async fn maybe_build_local_gemini_files_decision_payload_for_candidat
report_context: Some(report_context),
auth_context: input.auth_context.clone(),
});
apply_provider_request_routing_policy_to_decision(input, &mut decision)?;
apply_provider_request_routing_policy_to_decision(
input,
&mut decision,
Some(transport.as_ref()),
)?;
Ok(Some(decision))
}
@@ -53,7 +53,9 @@ pub(super) async fn resolve_local_gemini_files_decision_input(
state,
auth_context,
None,
decision.auth_endpoint_signature.as_deref(),
Some(&explicit_required_capabilities),
&decision.model_directive_policy,
)
.await
{
@@ -5,7 +5,7 @@ use crate::ai_serving::planner::report_context::{
};
use crate::ai_serving::planner::spec_metadata::local_openai_image_spec_metadata;
use crate::ai_serving::planner::{
build_ai_execution_decision_response, resolve_transport_request_gzip_policy,
build_ai_execution_decision_response, resolve_transport_request_encoding_policy,
AiExecutionDecisionResponseParts,
};
use crate::ai_serving::transport::{
@@ -84,11 +84,7 @@ pub(super) async fn maybe_build_local_openai_image_decision_payload_for_candidat
serde_json::Value::Bool(true),
);
}
let upstream_is_stream = resolved
.provider_request_body
.get("stream")
.and_then(serde_json::Value::as_bool)
.unwrap_or(spec_metadata.require_streaming);
let upstream_is_stream = resolved.upstream_is_stream;
let effective_headers = input.effective_headers(&parts.headers);
let report_context = append_execution_contract_fields_to_value(
build_local_execution_report_context(LocalExecutionReportContextParts {
@@ -135,7 +131,7 @@ pub(super) async fn maybe_build_local_openai_image_decision_payload_for_candidat
spec_metadata.api_format,
provider_api_format.as_str(),
);
let request_gzip = resolve_transport_request_gzip_policy(&transport);
let request_encoding = resolve_transport_request_encoding_policy(&transport);
let mut decision = build_ai_execution_decision_response(AiExecutionDecisionResponseParts {
decision_is_stream: spec_metadata.require_streaming,
@@ -145,6 +141,7 @@ pub(super) async fn maybe_build_local_openai_image_decision_payload_for_candidat
request_id: trace_id.to_string(),
candidate_id: candidate_id.clone(),
provider_name: transport.provider.name.clone(),
provider_type: transport.provider.provider_type.clone(),
provider_id: candidate.provider_id.clone(),
endpoint_id: candidate.endpoint_id.clone(),
key_id: candidate.key_id.clone(),
@@ -162,8 +159,8 @@ pub(super) async fn maybe_build_local_openai_image_decision_payload_for_candidat
provider_request_body: Some(resolved.provider_request_body),
provider_request_body_base64: None,
content_type: Some("application/json".to_string()),
content_encoding: None,
request_gzip,
content_encoding: request_encoding.content_encoding,
request_gzip: request_encoding.request_gzip,
proxy,
transport_profile,
timeouts: resolve_transport_execution_timeouts(&transport),
@@ -172,6 +169,10 @@ pub(super) async fn maybe_build_local_openai_image_decision_payload_for_candidat
report_context: Some(report_context),
auth_context: input.auth_context.clone(),
});
apply_provider_request_routing_policy_to_decision(input, &mut decision)?;
apply_provider_request_routing_policy_to_decision(
input,
&mut decision,
Some(transport.as_ref()),
)?;
Ok(Some(decision))
}
@@ -16,8 +16,8 @@ use crate::ai_serving::transport::{
ProviderOpenAiImageHeadersInput, StandardProviderRequestHeadersInput, GROK_CHAT_PATH,
};
use crate::ai_serving::{
apply_codex_openai_responses_special_body_edits, apply_codex_openai_responses_special_headers,
build_chatgpt_web_image_request_body,
apply_codex_openai_special_headers, build_chatgpt_web_image_request_body,
build_codex_openai_image_api_provider_request_body,
build_gemini_image_request_body_from_openai_image_request,
build_openai_image_api_provider_request_body, build_openai_image_provider_request_body,
default_model_for_openai_image_operation, normalize_openai_image_request,
@@ -48,6 +48,7 @@ pub(super) struct LocalOpenAiImageCandidatePayloadParts {
pub(super) upstream_url: String,
pub(super) input_summary: Value,
pub(super) transport_profile: Option<ResolvedTransportProfile>,
pub(super) upstream_is_stream: bool,
}
pub(super) async fn resolve_local_openai_image_candidate_payload_parts(
@@ -130,7 +131,10 @@ pub(super) async fn resolve_local_openai_image_candidate_payload_parts(
parts,
body_json,
body_base64,
openai_image_normalize_options_for_provider(&transport.provider.provider_type),
openai_image_normalize_options_for_provider(
&transport.provider.provider_type,
Some(prepared_candidate.mapped_model.as_str()),
),
);
let Some(normalized_request) = normalized_request else {
mark_skipped_local_openai_image_candidate_with_failure_diagnostic(
@@ -174,29 +178,56 @@ pub(super) async fn resolve_local_openai_image_candidate_payload_parts(
} else {
build_openai_image_upstream_url(transport, Some(parts.uri.path()), parts.uri.query())
};
let mut provider_request_body = if is_chatgpt_web {
match build_chatgpt_web_image_request_body(parts, body_json, body_base64) {
Ok(body) => body,
Err(err) => err.to_error_json(),
}
} else if is_codex || is_grok {
build_openai_image_provider_request_body(&normalized_request)
let upstream_is_stream =
crate::ai_serving::planner::common::resolve_upstream_is_stream_for_provider(
transport.endpoint.config.as_ref(),
transport.provider.provider_type.as_str(),
spec_metadata.api_format,
spec_metadata.require_streaming && candidate.supports_streaming,
false,
);
let provider_request_body = if is_chatgpt_web {
Some(
match build_chatgpt_web_image_request_body(parts, body_json, body_base64) {
Ok(body) => body,
Err(err) => err.to_error_json(),
},
)
} else if is_codex {
build_codex_openai_image_api_provider_request_body(
&normalized_request,
Some(prepared_candidate.mapped_model.as_str()),
upstream_is_stream,
)
} else if is_grok {
Some(build_openai_image_provider_request_body(
&normalized_request,
))
} else {
build_openai_image_api_provider_request_body(
&normalized_request,
Some(prepared_candidate.mapped_model.as_str()),
upstream_is_stream,
)
};
if !is_chatgpt_web {
apply_codex_openai_responses_special_body_edits(
&mut provider_request_body,
transport.provider.provider_type.as_str(),
spec_metadata.api_format,
transport.endpoint.body_rules.as_ref(),
Some(candidate.key_id.as_str()),
);
}
let Some(provider_request_body) = provider_request_body else {
mark_skipped_local_openai_image_candidate_with_failure_diagnostic(
state,
input,
trace_id,
candidate,
attempt.candidate_index,
&attempt.candidate_id,
"provider_request_body_missing",
CandidateFailureDiagnostic::provider_request_body_missing(
spec_metadata.api_format,
spec_metadata.api_format,
"codex_openai_images_request_contract",
),
)
.await;
return None;
};
let Some(mut provider_request_headers) = (if is_grok {
build_grok_browser_headers(GrokHeaderInput {
transport,
@@ -214,10 +245,12 @@ pub(super) async fn resolve_local_openai_image_candidate_payload_parts(
headers: effective_headers,
auth_header: &auth_header,
auth_value: &auth_value,
accept: if is_codex || is_chatgpt_web {
"text/event-stream"
accept: if is_codex {
None
} else if upstream_is_stream {
Some("text/event-stream")
} else {
"application/json"
Some("application/json")
},
header_rules: transport.endpoint.header_rules.as_ref(),
provider_request_body: &provider_request_body,
@@ -245,7 +278,7 @@ pub(super) async fn resolve_local_openai_image_candidate_payload_parts(
provider_request_headers.insert("x-aether-chatgpt-web-image".to_string(), "1".to_string());
} else if is_grok {
} else {
apply_codex_openai_responses_special_headers(
apply_codex_openai_special_headers(
&mut provider_request_headers,
&provider_request_body,
effective_headers,
@@ -287,6 +320,7 @@ pub(super) async fn resolve_local_openai_image_candidate_payload_parts(
upstream_url,
input_summary,
transport_profile,
upstream_is_stream,
})
}
@@ -403,7 +437,14 @@ async fn resolve_local_openai_image_to_gemini_candidate_payload_parts(
return None;
}
};
let upstream_is_stream = spec_metadata.require_streaming;
let upstream_is_stream =
crate::ai_serving::planner::common::resolve_upstream_is_stream_for_provider(
transport.endpoint.config.as_ref(),
transport.provider.provider_type.as_str(),
provider_api_format,
spec_metadata.require_streaming && candidate.supports_streaming,
false,
);
let Some(upstream_url) = crate::ai_serving::planner::standard::build_standard_upstream_url(
parts,
transport,
@@ -474,6 +515,7 @@ async fn resolve_local_openai_image_to_gemini_candidate_payload_parts(
upstream_url,
input_summary: converted.summary_json,
transport_profile: None,
upstream_is_stream,
})
}
@@ -58,7 +58,9 @@ pub(super) async fn resolve_local_openai_image_decision_input(
state,
auth_context,
Some(requested_model.as_str()),
decision.auth_endpoint_signature.as_deref(),
None,
&decision.model_directive_policy,
)
.await
{
@@ -124,6 +126,7 @@ pub(super) async fn list_local_openai_image_candidate_attempts(
matches_client_format.then_some(&input.auth_snapshot),
input.client_session_affinity.as_ref(),
current_unix_secs(),
false,
)
.await
{
@@ -144,8 +147,8 @@ pub(super) async fn list_local_openai_image_candidate_attempts(
auth_snapshot_allows_cross_format_candidate(
&input.auth_snapshot,
&input.requested_model,
None,
candidate,
false,
)
});
}
@@ -197,6 +200,7 @@ pub(super) async fn build_local_openai_image_candidate_attempt_source<'a>(
matches_client_format.then_some(&input.auth_snapshot),
input.client_session_affinity.as_ref(),
current_unix_secs(),
false,
)
.await
{
@@ -206,16 +210,16 @@ pub(super) async fn build_local_openai_image_candidate_attempt_source<'a>(
auth_snapshot_allows_cross_format_candidate(
&input.auth_snapshot,
&input.requested_model,
None,
candidate,
false,
)
});
format_skipped.retain(|candidate| {
auth_snapshot_allows_cross_format_candidate(
&input.auth_snapshot,
&input.requested_model,
None,
&candidate.candidate,
false,
)
});
}
@@ -5,7 +5,7 @@ use crate::ai_serving::planner::report_context::{
};
use crate::ai_serving::planner::spec_metadata::local_video_create_spec_metadata;
use crate::ai_serving::planner::{
build_ai_execution_decision_response, resolve_transport_request_gzip_policy,
build_ai_execution_decision_response, resolve_transport_request_encoding_policy,
AiExecutionDecisionResponseParts,
};
use crate::ai_serving::transport::{
@@ -104,7 +104,7 @@ pub(super) async fn maybe_build_local_video_create_decision_payload_for_candidat
provider_request_body,
upstream_url,
} = resolved;
let request_gzip = resolve_transport_request_gzip_policy(&transport);
let request_encoding = resolve_transport_request_encoding_policy(&transport);
let mut decision = build_ai_execution_decision_response(AiExecutionDecisionResponseParts {
decision_is_stream: false,
@@ -114,6 +114,7 @@ pub(super) async fn maybe_build_local_video_create_decision_payload_for_candidat
request_id: trace_id.to_string(),
candidate_id: candidate_id.clone(),
provider_name: transport.provider.name.clone(),
provider_type: transport.provider.provider_type.clone(),
provider_id: candidate.provider_id.clone(),
endpoint_id: candidate.endpoint_id.clone(),
key_id: candidate.key_id.clone(),
@@ -137,8 +138,8 @@ pub(super) async fn maybe_build_local_video_create_decision_payload_for_candidat
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned),
content_encoding: None,
request_gzip,
content_encoding: request_encoding.content_encoding,
request_gzip: request_encoding.request_gzip,
proxy,
transport_profile,
timeouts: resolve_transport_execution_timeouts(&transport),
@@ -147,6 +148,10 @@ pub(super) async fn maybe_build_local_video_create_decision_payload_for_candidat
report_context: Some(report_context),
auth_context: input.auth_context.clone(),
});
apply_provider_request_routing_policy_to_decision(input, &mut decision)?;
apply_provider_request_routing_policy_to_decision(
input,
&mut decision,
Some(transport.as_ref()),
)?;
Ok(Some(decision))
}
@@ -62,7 +62,9 @@ pub(super) async fn resolve_local_video_create_decision_input(
state,
auth_context,
Some(requested_model.as_str()),
decision.auth_endpoint_signature.as_deref(),
None,
&decision.model_directive_policy,
)
.await
{
@@ -130,6 +132,7 @@ pub(super) async fn list_local_video_create_candidate_attempts(
Some(&input.auth_snapshot),
input.client_session_affinity.as_ref(),
current_unix_secs(),
false,
)
.await
{
@@ -186,6 +189,7 @@ pub(super) async fn build_local_video_create_candidate_attempt_source<'a>(
Some(&input.auth_snapshot),
input.client_session_affinity.as_ref(),
current_unix_secs(),
false,
)
.await
{