mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-08 10:27:46 +08:00
feat(openai): align GPT-5.6 and Codex request contracts
This commit is contained in:
@@ -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());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user