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
+1 -1
View File
@@ -275,7 +275,7 @@ pub fn resolve_local_gemini_auth(
}
}
pub(crate) fn resolve_local_auth_type_for_transport_format(
pub fn resolve_local_auth_type_for_transport_format(
transport: &GatewayProviderTransportSnapshot,
) -> String {
let default_auth_type = transport.key.auth_type.trim().to_ascii_lowercase();
@@ -45,6 +45,7 @@ const IGNORABLE_AUTH_CONFIG_METADATA_KEYS: &[&str] = &[
"access_token_import_temporary",
"email",
"expires_at",
"is_fedramp",
"model_regions",
"organizations",
"plan_type",
@@ -7,7 +7,7 @@ use crate::grok::{is_grok_provider_transport, resolve_grok_session_auth};
use crate::policy::local_standard_transport_unsupported_reason_with_network;
use crate::rules::apply_local_header_rules_with_request_headers;
use crate::snapshot::GatewayProviderTransportSnapshot;
use crate::url::{build_openai_image_url, build_openai_responses_url};
use crate::url::build_openai_image_url;
#[derive(Debug, Clone, Copy)]
pub struct ProviderOpenAiImageHeadersInput<'a> {
@@ -15,7 +15,7 @@ pub struct ProviderOpenAiImageHeadersInput<'a> {
pub headers: &'a http::HeaderMap,
pub auth_header: &'a str,
pub auth_value: &'a str,
pub accept: &'a str,
pub accept: Option<&'a str>,
pub header_rules: Option<&'a Value>,
pub provider_request_body: &'a Value,
pub original_request_body: &'a Value,
@@ -44,6 +44,11 @@ fn is_dedicated_openai_image_provider(transport: &GatewayProviderTransportSnapsh
.provider_type
.trim()
.eq_ignore_ascii_case("chatgpt_web")
|| transport
.provider
.provider_type
.trim()
.eq_ignore_ascii_case("codex")
|| is_grok_provider_transport(transport)
}
@@ -61,14 +66,6 @@ pub fn build_openai_image_upstream_url(
request_path: Option<&str>,
request_query: Option<&str>,
) -> String {
if transport
.provider
.provider_type
.trim()
.eq_ignore_ascii_case("codex")
{
return build_openai_responses_url(&transport.endpoint.base_url, request_query, false);
}
build_openai_image_url(&transport.endpoint.base_url, request_path, request_query)
}
@@ -82,7 +79,11 @@ pub fn build_openai_image_headers(
&BTreeMap::new(),
);
provider_request_headers.insert("content-type".to_string(), "application/json".to_string());
provider_request_headers.insert("accept".to_string(), input.accept.to_string());
if let Some(accept) = input.accept {
provider_request_headers.insert("accept".to_string(), accept.to_string());
} else {
provider_request_headers.remove("accept");
}
crate::apply_local_auth_config_header_overrides(
&mut provider_request_headers,
input.transport.key.decrypted_auth_config.as_deref(),
@@ -171,14 +172,25 @@ mod tests {
}
#[test]
fn codex_openai_image_url_stays_on_responses_surface() {
fn codex_openai_image_url_uses_images_surface() {
let url = build_openai_image_upstream_url(
&sample_transport(),
Some("/v1/images/generations"),
Some("trace=1"),
);
assert_eq!(url, "https://api.openai.com/v1/responses?trace=1");
assert_eq!(url, "https://api.openai.com/v1/images/generations?trace=1");
}
#[test]
fn codex_openai_image_edit_url_uses_images_edit_surface() {
let url = build_openai_image_upstream_url(
&sample_transport(),
Some("/v1/images/edits"),
Some("trace=1"),
);
assert_eq!(url, "https://api.openai.com/v1/images/edits?trace=1");
}
#[test]
@@ -228,6 +240,19 @@ mod tests {
);
}
#[test]
fn codex_is_supported_by_dedicated_openai_image_transport_policy() {
let mut transport = sample_transport();
transport.provider.provider_type = "codex".to_string();
transport.key.auth_type = "oauth".to_string();
transport.key.decrypted_auth_config = Some(json!({"access_token":"token"}).to_string());
assert_eq!(
openai_image_transport_unsupported_reason(&transport, "openai:image"),
None
);
}
#[test]
fn grok_oauth_session_is_supported_by_dedicated_openai_image_transport_policy() {
let mut transport = sample_transport();
@@ -264,7 +289,7 @@ mod tests {
headers: &HeaderMap::new(),
auth_header: "authorization",
auth_value: "Bearer secret",
accept: "text/event-stream",
accept: Some("text/event-stream"),
header_rules: Some(&json!([
{"action":"set","key":"x-image-route","value":"codex"}
])),
@@ -307,7 +332,7 @@ mod tests {
headers: &HeaderMap::new(),
auth_header: "authorization",
auth_value: "Bearer refreshed-access-token",
accept: "text/event-stream",
accept: Some("text/event-stream"),
header_rules: None,
provider_request_body: &json!({"model":"gpt-5.4-mini"}),
original_request_body: &json!({"prompt":"draw"}),
@@ -332,7 +357,7 @@ mod tests {
headers: &HeaderMap::new(),
auth_header: "authorization",
auth_value: "Bearer secret",
accept: "application/json",
accept: Some("application/json"),
header_rules: None,
provider_request_body: &json!({
"model": "upstream-image-model",
@@ -349,6 +374,31 @@ mod tests {
);
}
#[test]
fn codex_images_omits_explicit_accept_header() {
let transport = sample_transport();
let mut request_headers = HeaderMap::new();
request_headers.insert("accept", "text/event-stream".parse().expect("valid header"));
let headers = build_openai_image_headers(ProviderOpenAiImageHeadersInput {
transport: &transport,
headers: &request_headers,
auth_header: "authorization",
auth_value: "Bearer secret",
accept: None,
header_rules: Some(&json!([
{"action":"set","key":"accept","value":"application/json"}
])),
provider_request_body: &json!({
"model": "gpt-image-2",
"prompt": "draw a city",
}),
original_request_body: &json!({"prompt":"draw a city"}),
})
.expect("headers should build");
assert!(!headers.contains_key("accept"));
}
#[test]
fn standard_openai_compatible_image_url_supports_aether_api_root() {
let mut transport = sample_transport();
@@ -310,7 +310,7 @@ const CODEX_FIXED_PROVIDER_TEMPLATE: FixedProviderTemplate = FixedProviderTempla
item_key: "openai:image",
api_format: "openai:image",
custom_path: None,
config_defaults: FORCE_STREAM_ENDPOINT_CONFIG_DEFAULTS,
config_defaults: EMPTY_ENDPOINT_CONFIG_DEFAULTS,
},
],
runtime_policy: CODEX_RUNTIME_POLICY,
@@ -659,17 +659,7 @@ mod tests {
let image_template =
fixed_provider_endpoint_template_by_api_format("codex", "openai:image")
.expect("codex image endpoint should exist");
assert_eq!(
image_template
.config_defaults
.iter()
.map(|item| (item.key, item.value))
.collect::<Vec<_>>(),
vec![(
"upstream_stream_policy",
FixedProviderEndpointConfigValue::String("force_stream")
)]
);
assert!(image_template.config_defaults.is_empty());
}
#[test]
@@ -142,16 +142,12 @@ pub fn classify_same_format_provider_request_behavior(
params.provider_api_format,
params.require_streaming,
);
let upstream_is_stream = aether_ai_formats::resolve_upstream_is_stream_from_endpoint_config(
let upstream_is_stream = aether_ai_formats::resolve_upstream_is_stream_for_provider(
transport.endpoint.config.as_ref(),
transport.provider.provider_type.as_str(),
params.provider_api_format,
params.require_streaming,
is_kiro
|| is_antigravity
|| gemini_cli_requires_upstream_streaming
|| aether_ai_formats::api::force_upstream_streaming_for_provider(
transport.provider.provider_type.as_str(),
params.provider_api_format,
),
is_kiro || is_antigravity || gemini_cli_requires_upstream_streaming,
);
let force_body_stream_field = aether_ai_formats::endpoint_config_forces_upstream_stream_policy(
transport.endpoint.config.as_ref(),
@@ -238,10 +234,11 @@ fn build_same_format_provider_request_body_inner(
.map(|(key, value)| (key.clone(), value.clone())),
)
} else {
aether_ai_formats::convert_request_pure(
aether_ai_formats::convert_request_pure_with_context(
input.client_api_format,
input.provider_api_format,
input.body_json,
&aether_ai_formats::FormatContext::default().with_mapped_model(input.mapped_model),
)
.ok()?
.value
@@ -397,6 +394,30 @@ fn build_same_format_provider_request_body_inner(
),
);
}
let provider_model = provider_request_body
.get("model")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.unwrap_or(input.mapped_model)
.to_string();
if aether_ai_formats::finalize_openai_provider_request(
&mut provider_request_body,
aether_ai_formats::OpenAiProviderRequestFinalization {
source_api_format: input.provider_api_format,
provider_api_format: input.provider_api_format,
provider_type: "",
provider_model: &provider_model,
source_model: input.source_model.unwrap_or(input.mapped_model),
body_rules: input.body_rules,
upstream_is_stream: input.upstream_is_stream,
require_body_stream_field,
},
)
.is_err()
{
return None;
}
Some(provider_request_body)
}
@@ -851,6 +872,20 @@ mod tests {
},
);
assert!(!sync_behavior.upstream_is_stream);
let mut compact = sample_transport("codex");
compact.endpoint.config = Some(json!({
"upstream_stream_policy": "force_stream"
}));
let compact_behavior = classify_same_format_provider_request_behavior(
&compact,
SameFormatProviderRequestBehaviorParams {
require_streaming: true,
provider_api_format: "openai:responses:compact",
report_kind: "openai_responses_compact_sync_success",
},
);
assert!(!compact_behavior.upstream_is_stream);
}
#[test]
@@ -1081,6 +1116,227 @@ mod tests {
assert_eq!(body["unknown_vendor_field"]["keep"], true);
}
#[test]
fn same_format_responses_body_preserves_gpt_5_6_protocol_contract_verbatim() {
let input = json!([
{
"type": "message",
"role": "user",
"content": [
{
"type": "input_text",
"text": "before",
"prompt_cache_breakpoint": {"mode": "explicit"}
},
{"type": "input_text", "text": "after"}
]
},
{
"type": "program",
"call_id": "program-call-1",
"fingerprint": "fp-program-1"
},
{
"type": "program_output",
"call_id": "program-call-1",
"result": "hello",
"status": "completed"
},
{
"type": "multi_agent_call",
"call_id": "agent-call-1",
"action": "delegate",
"arguments": {"query": "release notes"},
"agent": "researcher"
},
{
"type": "agent_message",
"author": "researcher",
"recipient": "assistant",
"encrypted_content": "encrypted-agent-message"
},
{
"type": "function_call_output",
"call_id": "call-1",
"output": "ok",
"caller": {"type": "multi_agent", "id": "agent-call-1"}
}
]);
let prompt_cache_options = json!({
"mode": "explicit",
"ttl": "30m"
});
let body_json = json!({
"model": "gpt-5.6-sol",
"input": input,
"reasoning": {
"effort": "max",
"mode": "pro",
"context": "all_turns"
},
"prompt_cache_options": prompt_cache_options,
"multi_agent": {
"enabled": true,
"max_concurrent_subagents": 2
}
});
let body = build_same_format_provider_request_body(SameFormatProviderRequestBodyInput {
body_json: &body_json,
mapped_model: "gpt-5.6-sol",
client_api_format: "openai:responses",
provider_api_format: "openai:responses",
source_model: Some("gpt-5.6-sol"),
family: SameFormatProviderFamily::Standard,
body_rules: None,
request_headers: None,
upstream_is_stream: false,
force_body_stream_field: false,
kiro_auth_config: None,
is_claude_code: false,
enable_model_directives: false,
})
.expect("valid GPT-5.6 Responses request should use raw same-format transport");
assert_eq!(body["input"], input);
assert_eq!(body["reasoning"], body_json["reasoning"]);
assert_eq!(body["prompt_cache_options"], prompt_cache_options);
assert_eq!(body["multi_agent"], body_json["multi_agent"]);
}
#[test]
fn same_format_prompt_cache_capability_uses_mapped_provider_model() {
let body_json = json!({
"model": "gpt-5.5",
"messages": [{"role": "user", "content": "hello"}],
"prompt_cache_options": {"mode": "explicit", "ttl": "30m"}
});
let body = build_same_format_provider_request_body(SameFormatProviderRequestBodyInput {
body_json: &body_json,
mapped_model: "gpt-5.6-sol",
client_api_format: "openai:chat",
provider_api_format: "openai:chat",
source_model: Some("gpt-5.5"),
family: SameFormatProviderFamily::Standard,
body_rules: None,
request_headers: None,
upstream_is_stream: false,
force_body_stream_field: false,
kiro_auth_config: None,
is_claude_code: false,
enable_model_directives: false,
})
.expect("final GPT-5.6 provider model should accept prompt_cache_options");
assert_eq!(body["model"], "gpt-5.6-sol");
assert_eq!(
body["prompt_cache_options"],
body_json["prompt_cache_options"]
);
let rejected =
build_same_format_provider_request_body(SameFormatProviderRequestBodyInput {
body_json: &body_json,
mapped_model: "gpt-5.5",
client_api_format: "openai:chat",
provider_api_format: "openai:chat",
source_model: Some("gpt-5.5"),
family: SameFormatProviderFamily::Standard,
body_rules: None,
request_headers: None,
upstream_is_stream: false,
force_body_stream_field: false,
kiro_auth_config: None,
is_claude_code: false,
enable_model_directives: false,
});
assert!(rejected.is_none());
}
#[test]
fn same_format_reasoning_capability_uses_mapped_provider_model() {
let body_json = json!({
"model": "deployment-alias",
"input": "hello",
"reasoning": {"effort": "max"}
});
let build = |mapped_model: &str| {
build_same_format_provider_request_body(SameFormatProviderRequestBodyInput {
body_json: &body_json,
mapped_model,
client_api_format: "openai:responses",
provider_api_format: "openai:responses",
source_model: Some("deployment-alias"),
family: SameFormatProviderFamily::Standard,
body_rules: None,
request_headers: None,
upstream_is_stream: false,
force_body_stream_field: false,
kiro_auth_config: None,
is_claude_code: false,
enable_model_directives: false,
})
};
assert!(build("gpt-5.6-sol").is_some());
assert!(build("gpt-5.4").is_none());
}
#[test]
fn same_format_gpt_5_6_requires_prompt_cache_options() {
let body_json = json!({
"model": "client-alias",
"input": "hello",
"prompt_cache_retention": "24h"
});
let body = build_same_format_provider_request_body(SameFormatProviderRequestBodyInput {
body_json: &body_json,
mapped_model: "gpt-5.6-luna",
client_api_format: "openai:responses",
provider_api_format: "openai:responses",
source_model: Some("client-alias"),
family: SameFormatProviderFamily::Standard,
body_rules: None,
request_headers: None,
upstream_is_stream: false,
force_body_stream_field: false,
kiro_auth_config: None,
is_claude_code: false,
enable_model_directives: false,
});
assert!(body.is_none());
}
#[test]
fn same_format_responses_body_preserves_opaque_extension_fields() {
let opaque_option = json!({"mode": "custom"});
let body = build_same_format_provider_request_body(SameFormatProviderRequestBodyInput {
body_json: &json!({
"model": "gpt-5.6-sol",
"input": "hello",
"multi_agent": {"enabled": true},
"opaque_responses_option": opaque_option
}),
mapped_model: "gpt-5.6-sol",
client_api_format: "openai:responses",
provider_api_format: "openai:responses",
source_model: Some("gpt-5.6-sol"),
family: SameFormatProviderFamily::Standard,
body_rules: None,
request_headers: None,
upstream_is_stream: false,
force_body_stream_field: false,
kiro_auth_config: None,
is_claude_code: false,
enable_model_directives: false,
});
let body = body.expect("same-format Responses request should remain transparent");
assert_eq!(body["multi_agent"], json!({"enabled": true}));
assert_eq!(body["opaque_responses_option"], opaque_option);
}
#[test]
fn cross_format_standard_body_fails_closed_for_lossy_chat_fields() {
let body = build_same_format_provider_request_body(SameFormatProviderRequestBodyInput {