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
@@ -9,7 +9,10 @@ use crate::ai_serving::{
GatewayProviderTransportSnapshot,
};
use super::{enforce_provider_body_stream_policy, request_requires_body_stream_field};
use super::{
enforce_provider_body_stream_policy, request_requires_body_stream_field,
validate_final_openai_provider_request,
};
pub(crate) fn build_local_openai_chat_request_body(
body_json: &Value,
@@ -39,6 +42,12 @@ pub(crate) fn build_local_openai_chat_request_body(
upstream_is_stream,
request_requires_body_stream_field(body_json, force_body_stream_field),
);
validate_final_openai_provider_request(
"openai:chat",
mapped_model,
body_json,
&provider_request_body,
)?;
Some(provider_request_body)
}
@@ -92,6 +101,12 @@ pub(crate) fn build_cross_format_openai_chat_request_body(
upstream_is_stream,
request_requires_body_stream_field(body_json, force_body_stream_field),
);
validate_final_openai_provider_request(
provider_api_format,
mapped_model,
body_json,
&provider_request_body,
)?;
Some(provider_request_body)
}
@@ -2,14 +2,16 @@ use serde_json::Value;
use crate::ai_serving::transport::apply_standard_provider_request_body_rules_with_request_headers;
use crate::ai_serving::{
apply_codex_openai_responses_special_body_edits,
apply_openai_responses_compact_special_body_edits,
build_cross_format_openai_responses_request_body_with_model_directives as surface_build_cross_format_openai_responses_request_body,
build_local_openai_responses_request_body_with_model_directives as surface_build_local_openai_responses_request_body,
GatewayProviderTransportSnapshot,
};
use super::{enforce_provider_body_stream_policy, request_requires_body_stream_field};
use super::{
enforce_provider_body_stream_policy, request_requires_body_stream_field,
validate_final_openai_provider_request,
};
pub(crate) fn build_local_openai_responses_request_body(
body_json: &Value,
@@ -19,9 +21,35 @@ pub(crate) fn build_local_openai_responses_request_body(
provider_type: &str,
provider_api_format: &str,
body_rules: Option<&Value>,
user_api_key_id: Option<&str>,
_user_api_key_id: Option<&str>,
request_headers: &http::HeaderMap,
enable_model_directives: bool,
) -> Option<Value> {
build_local_openai_responses_request_body_with_codex_model_capabilities(
body_json,
mapped_model,
require_streaming,
force_body_stream_field,
provider_type,
provider_api_format,
body_rules,
request_headers,
None,
enable_model_directives,
)
}
pub(crate) fn build_local_openai_responses_request_body_with_codex_model_capabilities(
body_json: &Value,
mapped_model: &str,
require_streaming: bool,
force_body_stream_field: bool,
provider_type: &str,
provider_api_format: &str,
body_rules: Option<&Value>,
request_headers: &http::HeaderMap,
model_capabilities: Option<&crate::ai_serving::CodexResponsesModelCapabilities>,
enable_model_directives: bool,
) -> Option<Value> {
let provider_request_body = surface_build_local_openai_responses_request_body(
body_json,
@@ -36,12 +64,18 @@ pub(crate) fn build_local_openai_responses_request_body(
body_json,
request_headers,
)?;
apply_codex_openai_responses_special_body_edits(
let source_model = body_json
.get("model")
.and_then(Value::as_str)
.unwrap_or(mapped_model);
crate::ai_serving::apply_codex_openai_responses_special_body_edits_with_source_model_and_capabilities(
&mut provider_request_body,
provider_type,
provider_api_format,
mapped_model,
source_model,
model_capabilities,
body_rules,
user_api_key_id,
);
apply_openai_responses_compact_special_body_edits(
&mut provider_request_body,
@@ -53,6 +87,12 @@ pub(crate) fn build_local_openai_responses_request_body(
require_streaming,
request_requires_body_stream_field(body_json, force_body_stream_field),
);
validate_final_openai_provider_request(
provider_api_format,
mapped_model,
body_json,
&provider_request_body,
)?;
Some(provider_request_body)
}
@@ -65,9 +105,37 @@ pub(crate) fn build_cross_format_openai_responses_request_body(
force_body_stream_field: bool,
provider_type: &str,
body_rules: Option<&Value>,
user_api_key_id: Option<&str>,
_user_api_key_id: Option<&str>,
request_headers: &http::HeaderMap,
enable_model_directives: bool,
) -> Option<Value> {
build_cross_format_openai_responses_request_body_with_codex_model_capabilities(
body_json,
mapped_model,
client_api_format,
provider_api_format,
upstream_is_stream,
force_body_stream_field,
provider_type,
body_rules,
request_headers,
None,
enable_model_directives,
)
}
pub(crate) fn build_cross_format_openai_responses_request_body_with_codex_model_capabilities(
body_json: &Value,
mapped_model: &str,
client_api_format: &str,
provider_api_format: &str,
upstream_is_stream: bool,
force_body_stream_field: bool,
provider_type: &str,
body_rules: Option<&Value>,
request_headers: &http::HeaderMap,
model_capabilities: Option<&crate::ai_serving::CodexResponsesModelCapabilities>,
enable_model_directives: bool,
) -> Option<Value> {
let provider_request_body = surface_build_cross_format_openai_responses_request_body(
body_json,
@@ -84,12 +152,18 @@ pub(crate) fn build_cross_format_openai_responses_request_body(
body_json,
request_headers,
)?;
apply_codex_openai_responses_special_body_edits(
let source_model = body_json
.get("model")
.and_then(Value::as_str)
.unwrap_or(mapped_model);
crate::ai_serving::apply_codex_openai_responses_special_body_edits_with_source_model_and_capabilities(
&mut provider_request_body,
provider_type,
provider_api_format,
mapped_model,
source_model,
model_capabilities,
body_rules,
user_api_key_id,
);
apply_openai_responses_compact_special_body_edits(
&mut provider_request_body,
@@ -101,6 +175,12 @@ pub(crate) fn build_cross_format_openai_responses_request_body(
upstream_is_stream,
request_requires_body_stream_field(body_json, force_body_stream_field),
);
validate_final_openai_provider_request(
provider_api_format,
mapped_model,
body_json,
&provider_request_body,
)?;
Some(provider_request_body)
}
@@ -6,8 +6,8 @@ use http::Request;
use serde_json::{json, Value};
use super::{
build_cross_format_openai_responses_request_body, build_local_openai_responses_request_body,
build_local_openai_responses_upstream_url,
build_cross_format_openai_responses_request_body, build_local_openai_chat_request_body,
build_local_openai_responses_request_body, build_local_openai_responses_upstream_url,
};
fn object_keys(value: &Value) -> Vec<&str> {
@@ -146,12 +146,10 @@ fn local_openai_responses_wrapper_preserves_body_order_after_edits() {
"reasoning",
"tool_choice",
"parallel_tool_calls",
"instructions",
"prompt_cache_key",
]
);
assert_eq!(provider_request_body["parallel_tool_calls"], json!(true));
assert_eq!(provider_request_body["instructions"], json!(""));
assert!(provider_request_body.get("instructions").is_none());
}
#[test]
@@ -181,18 +179,49 @@ fn local_openai_responses_compact_wrapper_strips_store_for_same_format_requests(
}
#[test]
fn local_openai_responses_compact_wrapper_strips_include_for_codex_requests() {
fn local_codex_compact_wrapper_applies_the_complete_request_projection() {
let body_json = json!({
"model": "gpt-5.4",
"input": [],
"model": "gpt-5.6-sol",
"input": [{
"type": "message",
"role": "user",
"content": [{"type": "input_text", "text": "hello"}]
}],
"instructions": "Work carefully",
"client_metadata": {"origin": "codex"},
"include": ["reasoning.encrypted_content"],
"store": true,
"stream": true
"stream": true,
"stream_options": {"reasoning_summary_delivery": "sequential_cutoff"},
"tool_choice": "auto",
"parallel_tool_calls": true,
"reasoning": {"effort": "max", "context": "all_turns"},
"text": {"verbosity": "medium"},
"tools": [{
"type": "function",
"name": "lookup",
"parameters": {"type": "object", "properties": {}}
}],
"service_tier": "priority",
"prompt_cache_key": "thread-compact"
});
let provider_request_body = build_local_openai_responses_request_body(
let regular = build_local_openai_responses_request_body(
&body_json,
"gpt-5.4",
"gpt-5.6-sol",
true,
false,
"codex",
"openai:responses",
None,
Some("key-123"),
&http::HeaderMap::new(),
false,
)
.expect("local Codex Responses body should build");
let compact = build_local_openai_responses_request_body(
&body_json,
"gpt-5.6-sol",
false,
false,
"codex",
@@ -202,22 +231,44 @@ fn local_openai_responses_compact_wrapper_strips_include_for_codex_requests() {
&http::HeaderMap::new(),
false,
)
.expect("local codex compact body should build");
.expect("local Codex Compact body should build");
assert!(provider_request_body.get("include").is_none());
assert!(provider_request_body.get("store").is_none());
assert!(provider_request_body.get("stream").is_none());
assert_eq!(provider_request_body["instructions"], "");
assert_eq!(
provider_request_body["prompt_cache_key"],
"3d2e2842-74cb-55dd-803a-b8940b3500c2"
);
for field in [
"client_metadata",
"include",
"store",
"stream",
"stream_options",
"tool_choice",
] {
assert!(
regular.get(field).is_some(),
"Responses should contain {field}"
);
assert!(compact.get(field).is_none(), "Compact should omit {field}");
}
for field in [
"model",
"input",
"instructions",
"parallel_tool_calls",
"reasoning",
"text",
"tools",
"service_tier",
"prompt_cache_key",
] {
assert_eq!(
compact[field], regular[field],
"Compact should preserve {field}"
);
}
}
#[test]
fn local_openai_responses_wrapper_applies_model_directive_before_body_rules() {
let body_json = json!({
"model": "gpt-5.4-max",
"model": "gpt-5.6-sol-max",
"input": "hello",
"reasoning": {"effort": "low", "summary": "auto"}
});
@@ -227,7 +278,7 @@ fn local_openai_responses_wrapper_applies_model_directive_before_body_rules() {
let provider_request_body = build_local_openai_responses_request_body(
&body_json,
"gpt-5.4",
"gpt-5.6-sol",
false,
false,
"openai",
@@ -244,6 +295,132 @@ fn local_openai_responses_wrapper_applies_model_directive_before_body_rules() {
assert_eq!(provider_request_body["metadata"]["override_seen"], true);
}
#[test]
fn final_openai_provider_contract_uses_the_mapped_model_for_reasoning() {
let alias = json!({
"model": "deployment-alias",
"input": "hello",
"reasoning": {"effort": "max"}
});
assert!(build_local_openai_responses_request_body(
&alias,
"gpt-5.6-sol",
false,
false,
"openai",
"openai:responses",
None,
None,
&http::HeaderMap::new(),
false,
)
.is_some());
assert!(build_local_openai_responses_request_body(
&alias,
"gpt-5.4",
false,
false,
"openai",
"openai:responses",
None,
None,
&http::HeaderMap::new(),
false,
)
.is_none());
let minimal = json!({
"model": "deployment-alias",
"messages": [{"role": "user", "content": "hello"}],
"reasoning_effort": "minimal"
});
assert!(build_local_openai_chat_request_body(
&minimal,
"gpt-5.6-terra",
false,
false,
None,
&http::HeaderMap::new(),
false,
)
.is_none());
let opaque_mapping = json!({
"model": "gpt-5.6-sol-max",
"input": "hello",
"reasoning": {"effort": "max", "mode": "pro"},
"prompt_cache_options": {"mode": "explicit", "ttl": "30m"}
});
assert!(build_local_openai_responses_request_body(
&opaque_mapping,
"azure-production",
false,
false,
"openai",
"openai:responses",
None,
None,
&http::HeaderMap::new(),
false,
)
.is_some());
assert!(build_local_openai_responses_request_body(
&opaque_mapping,
"gpt-5.4",
false,
false,
"openai",
"openai:responses",
None,
None,
&http::HeaderMap::new(),
false,
)
.is_none());
}
#[test]
fn final_openai_provider_contract_validates_body_rule_output() {
let body = json!({
"model": "gpt-5.6-sol",
"input": "hello",
"reasoning": {"effort": "max"}
});
let model_override = json!([
{"action":"set","path":"model","value":"gpt-5.4"}
]);
assert!(build_local_openai_responses_request_body(
&body,
"gpt-5.6-sol",
false,
false,
"openai",
"openai:responses",
Some(&model_override),
None,
&http::HeaderMap::new(),
false,
)
.is_none());
let cache_override = json!([
{"action":"set","path":"prompt_cache_options.ttl","value":"1h"}
]);
assert!(build_local_openai_responses_request_body(
&json!({"model":"gpt-5.6-sol","input":"hello"}),
"gpt-5.6-sol",
false,
false,
"openai",
"openai:responses",
Some(&cache_override),
None,
&http::HeaderMap::new(),
false,
)
.is_none());
}
#[test]
fn local_openai_responses_upstream_url_preserves_codex_base_path() {
let request = Request::builder()
@@ -371,7 +548,7 @@ fn applies_codex_defaults_unless_body_rules_handle_the_field() {
}
#[test]
fn injects_codex_prompt_cache_key_for_openai_responses_cross_format_requests() {
fn omits_codex_prompt_cache_key_for_openai_responses_cross_format_requests() {
let body_json = json!({
"model": "claude-sonnet-4-5",
"messages": [{
@@ -395,14 +572,11 @@ fn injects_codex_prompt_cache_key_for_openai_responses_cross_format_requests() {
)
.expect("claude cli to codex request should build");
assert_eq!(
provider_request_body["prompt_cache_key"],
"4ee6ea6e-3ac6-5a18-8cb8-1f8b956419e5"
);
assert!(provider_request_body.get("prompt_cache_key").is_none());
}
#[test]
fn injects_codex_prompt_cache_key_for_openai_chat_cross_format_requests() {
fn omits_codex_prompt_cache_key_for_openai_chat_cross_format_requests() {
let body_json = json!({
"model": "gpt-5",
"messages": [{
@@ -425,8 +599,5 @@ fn injects_codex_prompt_cache_key_for_openai_chat_cross_format_requests() {
)
.expect("openai chat to codex request should build");
assert_eq!(
provider_request_body["prompt_cache_key"],
"4ee6ea6e-3ac6-5a18-8cb8-1f8b956419e5"
);
assert!(provider_request_body.get("prompt_cache_key").is_none());
}