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
@@ -14,7 +14,10 @@ use serde_json::{json, Value};
use tracing::warn;
use crate::ai_serving::planner::common::extract_standard_requested_model;
use crate::ai_serving::{ExecutionRuntimeAuthContext, GatewayAuthApiKeySnapshot, PlannerAppState};
use crate::ai_serving::{
ExecutionRuntimeAuthContext, GatewayAuthApiKeySnapshot, GatewayProviderTransportSnapshot,
PlannerAppState,
};
use crate::client_session_affinity::client_session_affinity_from_request;
use crate::clock::current_unix_secs;
use crate::routing::{
@@ -27,12 +30,16 @@ use crate::stage_metrics::observe_gateway_stage_ms;
use crate::{AiExecutionDecision, AppState, GatewayError};
const ROUTING_GROUP_SELECTION_CACHE_TTL: Duration = Duration::from_secs(30);
const CODEX_ACCOUNT_ID_HEADER: &str = "chatgpt-account-id";
const CODEX_FEDRAMP_HEADER: &str = "x-openai-fedramp";
const CODEX_RESPONSES_LITE_HEADER: &str = "x-openai-internal-codex-responses-lite";
#[derive(Debug, Clone)]
pub(crate) struct ResolvedLocalDecisionAuthInput {
pub(crate) auth_context: ExecutionRuntimeAuthContext,
pub(crate) auth_snapshot: GatewayAuthApiKeySnapshot,
pub(crate) required_capabilities: Option<serde_json::Value>,
pub(crate) model_directive_policy: crate::system_features::ModelDirectivePolicySnapshot,
}
#[derive(Debug, Clone)]
@@ -46,6 +53,7 @@ pub(crate) struct LocalRequestedModelDecisionInput {
pub(crate) routing_policy: Option<ResolvedRoutingPolicy>,
pub(crate) routing_trace_seed: Option<RoutingDecisionTrace>,
pub(crate) routing_context: Option<LocalRoutingRequestContext>,
pub(crate) model_directive_policy: crate::system_features::ModelDirectivePolicySnapshot,
}
#[derive(Debug, Clone)]
@@ -86,14 +94,52 @@ impl LocalRequestedModelDecisionInput {
pub(crate) fn apply_provider_request_routing_policy_to_decision(
input: &LocalRequestedModelDecisionInput,
decision: &mut AiExecutionDecision,
transport: Option<&GatewayProviderTransportSnapshot>,
) -> Result<(), GatewayError> {
let provider_api_format = decision
.provider_api_format
.clone()
.or_else(|| {
input
.routing_context
.as_ref()
.map(|context| context.client_api_format.clone())
})
.unwrap_or_default();
let provider_type = decision.provider_type.clone().unwrap_or_default();
let terminal_provider_model = decision
.provider_request_body
.as_ref()
.and_then(|body| body.get("model"))
.and_then(Value::as_str)
.or(decision.mapped_model.as_deref())
.or(decision.model_name.as_deref())
.unwrap_or(input.requested_model.as_str());
let model_capabilities = transport.and_then(|transport| {
crate::ai_serving::codex_model_capabilities_for_transport(
transport,
provider_api_format.as_str(),
terminal_provider_model,
input.requested_model.as_str(),
)
});
crate::ai_serving::apply_codex_openai_responses_lite_header_with_capabilities(
&mut decision.provider_request_headers,
provider_type.as_str(),
provider_api_format.as_str(),
terminal_provider_model,
input.requested_model.as_str(),
model_capabilities.as_ref(),
);
let Some(context) = input.routing_context.as_ref() else {
return Ok(());
};
let provider_api_format = decision
.provider_api_format
.as_deref()
.unwrap_or(context.client_api_format.as_str());
let provider_body_rules = decision
.report_context
.as_ref()
.and_then(|context| context.get("body_rules"))
.cloned();
let resolved_model = decision
.mapped_model
.as_deref()
@@ -104,6 +150,21 @@ pub(crate) fn apply_provider_request_routing_policy_to_decision(
.clone()
.unwrap_or(serde_json::Value::Null);
let mut provider_headers = btree_headers_to_header_map(&decision.provider_request_headers)?;
let mut protected_codex_header_names = vec![CODEX_ACCOUNT_ID_HEADER, CODEX_FEDRAMP_HEADER];
if provider_type.eq_ignore_ascii_case("codex")
&& crate::ai_serving::is_openai_responses_family_format(provider_api_format.as_str())
{
protected_codex_header_names.extend([
"x-client-request-id",
"accept",
"content-encoding",
CODEX_RESPONSES_LITE_HEADER,
]);
}
let protected_codex_headers = protected_codex_header_names
.into_iter()
.map(|name| (name, provider_headers.get(name).cloned()))
.collect::<Vec<_>>();
let provider_headers_json = headers_to_routing_value(&provider_headers);
let policy = resolve_gateway_routing_policy(GatewayRoutingPolicyInput {
group_id: context.group_id.as_deref(),
@@ -112,7 +173,7 @@ pub(crate) fn apply_provider_request_routing_policy_to_decision(
selection_source: context.selection_source.as_str(),
requested_model: input.requested_model.as_str(),
resolved_model,
api_format: provider_api_format,
api_format: provider_api_format.as_str(),
user_id: Some(input.auth_context.user_id.as_str()),
api_key_id: Some(input.auth_context.api_key_id.as_str()),
headers: &provider_headers_json,
@@ -134,6 +195,80 @@ pub(crate) fn apply_provider_request_routing_policy_to_decision(
&mut provider_headers,
&policy.mutation_plan,
)?;
for (name, value) in protected_codex_headers {
provider_headers.remove(name);
if let Some(value) = value {
provider_headers.insert(HeaderName::from_static(name), value);
}
}
if original_provider_request_body.is_some() {
let provider_model = provider_request_body
.get("model")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.or(decision.mapped_model.as_deref())
.or(decision.model_name.as_deref())
.unwrap_or(input.requested_model.as_str())
.to_string();
let model_capabilities = transport.and_then(|transport| {
crate::ai_serving::codex_model_capabilities_for_transport(
transport,
provider_api_format.as_str(),
provider_model.as_str(),
input.requested_model.as_str(),
)
});
crate::ai_serving::finalize_openai_provider_request_with_codex_model_capabilities(
&mut provider_request_body,
crate::ai_serving::OpenAiProviderRequestFinalization {
source_api_format: context.client_api_format.as_str(),
provider_api_format: provider_api_format.as_str(),
provider_type: provider_type.as_str(),
provider_model: provider_model.as_str(),
source_model: input.requested_model.as_str(),
body_rules: provider_body_rules.as_ref(),
upstream_is_stream: decision.upstream_is_stream,
require_body_stream_field: original_provider_request_body
.as_ref()
.is_some_and(|body| body.get("stream").is_some()),
},
model_capabilities.as_ref(),
)
.map_err(|violation| GatewayError::Client {
status: StatusCode::BAD_REQUEST,
message: format!("routing provider_request violates provider contract: {violation:?}"),
})?;
}
let provider_model = provider_request_body
.get("model")
.and_then(Value::as_str)
.or(decision.mapped_model.as_deref())
.or(decision.model_name.as_deref())
.unwrap_or(input.requested_model.as_str());
let mut provider_request_headers = header_map_to_btree_headers(&provider_headers);
let model_capabilities = transport.and_then(|transport| {
crate::ai_serving::codex_model_capabilities_for_transport(
transport,
provider_api_format.as_str(),
provider_model,
input.requested_model.as_str(),
)
});
crate::ai_serving::apply_codex_openai_responses_lite_header_with_capabilities(
&mut provider_request_headers,
provider_type.as_str(),
provider_api_format.as_str(),
provider_model,
input.requested_model.as_str(),
model_capabilities.as_ref(),
);
crate::ai_serving::apply_codex_openai_compact_terminal_headers(
&mut provider_request_headers,
provider_type.as_str(),
provider_api_format.as_str(),
);
provider_headers = btree_headers_to_header_map(&provider_request_headers)?;
decision.provider_request_headers = header_map_to_btree_headers(&provider_headers);
if original_provider_request_body.is_some() {
decision.provider_request_body = Some(provider_request_body);
@@ -145,6 +280,8 @@ pub(crate) fn apply_provider_request_routing_policy_to_decision(
struct GatewayAuthenticatedDecisionInputPort<'a> {
state: PlannerAppState<'a>,
now_unix_secs: u64,
model_directive_policy: &'a crate::system_features::ModelDirectivePolicySnapshot,
model_directive_base_model: Option<String>,
}
#[async_trait]
@@ -181,6 +318,7 @@ impl AiAuthenticatedDecisionInputPort for GatewayAuthenticatedDecisionInputPort<
&auth_context.api_key_id,
requested_model,
explicit_required_capabilities,
self.model_directive_base_model.as_deref(),
)
.await)
}
@@ -195,6 +333,7 @@ impl AiAuthenticatedDecisionInputPort for GatewayAuthenticatedDecisionInputPort<
auth_context,
auth_snapshot,
required_capabilities,
model_directive_policy: self.model_directive_policy.clone(),
}
}
}
@@ -213,6 +352,7 @@ pub(crate) fn build_local_requested_model_decision_input(
routing_policy: None,
routing_trace_seed: None,
routing_context: None,
model_directive_policy: resolved_input.model_directive_policy,
}
}
@@ -373,12 +513,16 @@ pub(crate) async fn attach_routing_policy_to_local_requested_model_input(
}
}
if requested_model_changed {
let model_directive_resolution = input
.model_directive_policy
.resolve_reasoning(client_api_format, Some(input.requested_model.as_str()));
input.required_capabilities = PlannerAppState::new(state)
.resolve_request_candidate_required_capabilities(
&input.auth_context.user_id,
&input.auth_context.api_key_id,
Some(input.requested_model.as_str()),
input.required_capabilities.as_ref(),
model_directive_resolution.base_model(),
)
.await;
}
@@ -475,11 +619,22 @@ pub(crate) async fn resolve_local_authenticated_decision_input(
state: &AppState,
auth_context: ExecutionRuntimeAuthContext,
requested_model: Option<&str>,
requested_model_api_format: Option<&str>,
explicit_required_capabilities: Option<&serde_json::Value>,
model_directive_policy: &crate::system_features::ModelDirectivePolicySnapshot,
) -> Result<Option<ResolvedLocalDecisionAuthInput>, GatewayError> {
let model_directive_base_model = match (requested_model, requested_model_api_format) {
(Some(model), Some(api_format)) => model_directive_policy
.resolve_reasoning(api_format, Some(model))
.base_model()
.map(str::to_owned),
_ => None,
};
let port = GatewayAuthenticatedDecisionInputPort {
state: PlannerAppState::new(state),
now_unix_secs: current_unix_secs(),
model_directive_policy,
model_directive_base_model,
};
run_ai_authenticated_decision_input(
@@ -730,6 +885,10 @@ fn ensure_report_context_routing_trace(
#[cfg(test)]
mod tests {
use super::*;
use aether_provider_transport::snapshot::{
GatewayProviderTransportEndpoint, GatewayProviderTransportKey,
GatewayProviderTransportProvider,
};
fn sample_auth_context() -> ExecutionRuntimeAuthContext {
ExecutionRuntimeAuthContext {
@@ -782,6 +941,7 @@ mod tests {
client_session_affinity: None,
routing_policy: None,
routing_trace_seed: None,
model_directive_policy: Default::default(),
routing_context: Some(LocalRoutingRequestContext {
group_id: Some("group-1".to_string()),
group_version: Some(3),
@@ -830,6 +990,7 @@ mod tests {
request_id: Some("trace-1".to_string()),
candidate_id: Some("candidate-1".to_string()),
provider_name: Some("provider".to_string()),
provider_type: Some("openai".to_string()),
provider_id: Some("provider-1".to_string()),
endpoint_id: Some("endpoint-1".to_string()),
key_id: Some("key-1".to_string()),
@@ -869,9 +1030,80 @@ mod tests {
}
}
fn set_provider_request_rules(input: &mut LocalRequestedModelDecisionInput, actions: Value) {
fn sample_codex_transport_with_card() -> GatewayProviderTransportSnapshot {
let card = json!({
"id": "gpt-future-agent",
"slug": "gpt-future-agent",
"use_responses_lite": true,
"supports_reasoning_summaries": true,
"default_reasoning_level": "low",
"default_reasoning_summary": "none",
"supported_reasoning_levels": [{"effort": "low"}, {"effort": "high"}]
});
GatewayProviderTransportSnapshot {
provider: GatewayProviderTransportProvider {
id: "provider-codex".to_string(),
name: "Codex".to_string(),
provider_type: "codex".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-codex".to_string(),
provider_id: "provider-codex".to_string(),
api_format: "openai:responses:compact".to_string(),
api_family: Some("openai".to_string()),
endpoint_kind: Some("compact".to_string()),
is_active: true,
base_url: "https://chatgpt.com/backend-api/codex".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-codex".to_string(),
provider_id: "provider-codex".to_string(),
name: "Codex key".to_string(),
auth_type: "oauth".to_string(),
is_active: true,
api_formats: Some(vec!["openai:responses:compact".to_string()]),
auth_type_by_format: None,
allow_auth_channel_mismatch_formats: None,
allowed_models: Some(vec!["gpt-future-agent".to_string()]),
capabilities: None,
rate_multipliers: None,
global_priority_by_format: None,
expires_at_unix_secs: None,
proxy: None,
fingerprint: None,
upstream_metadata: Some(crate::ai_serving::build_codex_model_catalog_metadata(&[
card,
])),
decrypted_api_key: "access-token".to_string(),
decrypted_auth_config: None,
},
}
}
fn set_provider_request_rules(
input: &mut LocalRequestedModelDecisionInput,
allowed_models: &[&str],
actions: Value,
) {
let config = json!({
"allowed_models": ["gpt-5"],
"allowed_models": allowed_models,
"rules": [{
"id": "provider-patch",
"priority": 1,
@@ -904,6 +1136,7 @@ mod tests {
client_session_affinity: None,
routing_policy: None,
routing_trace_seed: None,
model_directive_policy: Default::default(),
routing_context: Some(LocalRoutingRequestContext {
group_id: Some("stale".to_string()),
group_version: Some(1),
@@ -971,6 +1204,7 @@ mod tests {
routing_policy: None,
routing_trace_seed: None,
routing_context: None,
model_directive_policy: Default::default(),
};
let group_config_json = json!({
"rules": [{
@@ -1006,7 +1240,7 @@ mod tests {
let input = sample_decision_input();
let mut decision = sample_decision();
apply_provider_request_routing_policy_to_decision(&input, &mut decision)
apply_provider_request_routing_policy_to_decision(&input, &mut decision, None)
.expect("provider routing mutation should apply");
assert_eq!(
@@ -1035,6 +1269,179 @@ mod tests {
);
}
#[test]
fn codex_compact_contract_is_terminal_after_routing_mutations() {
let mut input = sample_decision_input();
input
.routing_context
.as_mut()
.expect("routing context")
.client_api_format = "openai:responses:compact".to_string();
set_provider_request_rules(
&mut input,
&["gpt-5"],
json!([
{
"type": "json_patch_body",
"patch": [
{"op": "add", "path": "/store", "value": true},
{"op": "add", "path": "/top_logprobs", "value": 5},
{"op": "add", "path": "/custom_extension", "value": true},
{"op": "replace", "path": "/input", "value": "routed compact input"},
{"op": "replace", "path": "/tools", "value": [{
"type": "function",
"name": "lookup",
"cache_control": {"type": "ephemeral"}
}]}
]
},
{
"type": "patch_headers",
"patch": [
{"op": "set", "name": "chatgpt-account-id", "value": "spoofed"},
{"op": "set", "name": "x-openai-fedramp", "value": "false"},
{"op": "set", "name": "x-client-request-id", "value": "spoofed"},
{"op": "set", "name": "accept", "value": "text/event-stream"},
{"op": "set", "name": "content-encoding", "value": "zstd"}
]
}
]),
);
let mut decision = sample_decision();
decision.provider_type = Some("codex".to_string());
decision.provider_api_format = Some("openai:responses:compact".to_string());
decision.client_api_format = Some("openai:responses:compact".to_string());
decision.provider_request_body = Some(json!({
"model": "gpt-5",
"input": [],
"tools": [{"type": "function", "name": "lookup"}]
}));
decision
.provider_request_headers
.insert(CODEX_ACCOUNT_ID_HEADER.to_string(), "account-1".to_string());
decision
.provider_request_headers
.insert(CODEX_FEDRAMP_HEADER.to_string(), "true".to_string());
apply_provider_request_routing_policy_to_decision(&input, &mut decision, None)
.expect("terminal contract should accept the projected request");
let body = decision.provider_request_body.as_ref().expect("body");
assert_eq!(body["parallel_tool_calls"], false);
assert_eq!(body["input"][0]["type"], "message");
assert_eq!(
body["input"][0]["content"][0]["text"],
"routed compact input"
);
assert!(body["tools"][0].get("cache_control").is_none());
for field in ["store", "top_logprobs", "custom_extension"] {
assert!(
body.get(field).is_none(),
"unexpected Compact field: {field}"
);
}
assert_eq!(
decision
.provider_request_headers
.get(CODEX_ACCOUNT_ID_HEADER),
Some(&"account-1".to_string())
);
assert_eq!(
decision.provider_request_headers.get(CODEX_FEDRAMP_HEADER),
Some(&"true".to_string())
);
for header in ["x-client-request-id", "accept", "content-encoding"] {
assert!(
!decision.provider_request_headers.contains_key(header),
"unexpected Compact header: {header}"
);
}
}
#[test]
fn codex_responses_lite_contract_is_terminal_after_routing_mutations() {
let mut input = sample_decision_input();
input.requested_model = "gpt-future-agent".to_string();
input
.routing_context
.as_mut()
.expect("routing context")
.client_api_format = "openai:responses:compact".to_string();
set_provider_request_rules(
&mut input,
&["gpt-future-agent"],
json!([
{
"type": "json_patch_body",
"patch": [
{"op": "replace", "path": "/input", "value": "routed compact input"},
{"op": "add", "path": "/instructions", "value": "Routed instructions"},
{"op": "replace", "path": "/tools", "value": [{
"type": "function",
"name": "lookup",
"parameters": {},
"cache_control": {"type": "ephemeral"}
}]},
{"op": "add", "path": "/parallel_tool_calls", "value": true},
{"op": "add", "path": "/reasoning", "value": {
"effort": "high",
"context": "current_turn"
}}
]
},
{
"type": "patch_headers",
"patch": [{
"op": "set",
"name": "x-openai-internal-codex-responses-lite",
"value": "false"
}]
}
]),
);
let mut decision = sample_decision();
decision.provider_type = Some("codex".to_string());
decision.provider_api_format = Some("openai:responses:compact".to_string());
decision.client_api_format = Some("openai:responses:compact".to_string());
decision.mapped_model = Some("gpt-future-agent".to_string());
decision.provider_request_body = Some(json!({
"model": "gpt-future-agent",
"input": [],
"tools": []
}));
let transport = sample_codex_transport_with_card();
apply_provider_request_routing_policy_to_decision(&input, &mut decision, Some(&transport))
.expect("terminal Lite contract should accept the projected request");
let body = decision.provider_request_body.as_ref().expect("body");
assert_eq!(body["input"][0]["type"], "additional_tools");
assert_eq!(body["input"][0]["tools"][0]["name"], "lookup");
assert!(body["input"][0]["tools"][0].get("cache_control").is_none());
assert_eq!(body["input"][1]["role"], "developer");
assert_eq!(
body["input"][1]["content"][0]["text"],
"Routed instructions"
);
assert_eq!(body["input"][2]["role"], "user");
assert_eq!(
body["input"][2]["content"][0]["text"],
"routed compact input"
);
assert!(body.get("tools").is_none());
assert!(body.get("instructions").is_none());
assert_eq!(body["parallel_tool_calls"], false);
assert_eq!(body["reasoning"]["effort"], "high");
assert_eq!(body["reasoning"]["context"], "all_turns");
assert_eq!(
decision
.provider_request_headers
.get(CODEX_RESPONSES_LITE_HEADER)
.map(String::as_str),
Some("true")
);
}
#[test]
fn provider_request_routing_policy_rejects_body_patch_without_json_body() {
let input = sample_decision_input();
@@ -1042,7 +1449,7 @@ mod tests {
decision.provider_request_body = None;
decision.provider_request_body_base64 = Some("AA==".to_string());
let error = apply_provider_request_routing_policy_to_decision(&input, &mut decision)
let error = apply_provider_request_routing_policy_to_decision(&input, &mut decision, None)
.expect_err("provider body patch should reject binary upstream bodies");
match error {
@@ -1067,6 +1474,7 @@ mod tests {
let mut input = sample_decision_input();
set_provider_request_rules(
&mut input,
&["gpt-5"],
json!([{
"type": "patch_headers",
"patch": [{
@@ -1080,7 +1488,7 @@ mod tests {
decision.provider_request_body = None;
decision.provider_request_body_base64 = Some("AA==".to_string());
apply_provider_request_routing_policy_to_decision(&input, &mut decision)
apply_provider_request_routing_policy_to_decision(&input, &mut decision, None)
.expect("header-only provider routing mutation should apply without JSON body");
assert_eq!(decision.provider_request_body, None);
@@ -1112,7 +1520,7 @@ mod tests {
"priority_slot": 3
}));
apply_provider_request_routing_policy_to_decision(&input, &mut decision)
apply_provider_request_routing_policy_to_decision(&input, &mut decision, None)
.expect("provider routing mutation should seed pool trace");
let routing_trace = &decision.report_context.as_ref().unwrap()["routing_trace"];