fix(codex): 服务端压缩使用标准 Responses 合约

This commit is contained in:
ZheFox
2026-07-16 12:02:33 +08:00
parent 3976652942
commit 71fcb9c168
9 changed files with 139 additions and 24 deletions
@@ -122,8 +122,9 @@ pub(crate) fn apply_provider_request_routing_policy_to_decision(
input.requested_model.as_str(),
)
});
crate::ai_serving::apply_codex_openai_responses_lite_header_with_capabilities(
crate::ai_serving::apply_codex_openai_responses_lite_header_for_request_body_with_capabilities(
&mut decision.provider_request_headers,
decision.provider_request_body.as_ref(),
provider_type.as_str(),
provider_api_format.as_str(),
terminal_provider_model,
@@ -254,8 +255,9 @@ pub(crate) fn apply_provider_request_routing_policy_to_decision(
input.requested_model.as_str(),
)
});
crate::ai_serving::apply_codex_openai_responses_lite_header_with_capabilities(
crate::ai_serving::apply_codex_openai_responses_lite_header_for_request_body_with_capabilities(
&mut provider_request_headers,
Some(&provider_request_body),
provider_type.as_str(),
provider_api_format.as_str(),
provider_model,
@@ -531,8 +531,9 @@ pub(crate) async fn resolve_local_same_format_provider_candidate_payload_parts(
.get("model")
.and_then(Value::as_str)
.unwrap_or(prepared.mapped_model.as_str());
crate::ai_serving::apply_codex_openai_responses_lite_header_with_capabilities(
crate::ai_serving::apply_codex_openai_responses_lite_header_for_request_body_with_capabilities(
&mut provider_request_headers,
Some(&provider_request_body),
transport.provider.provider_type.as_str(),
prepared.provider_api_format.as_str(),
provider_model,
@@ -685,8 +685,9 @@ pub(crate) async fn resolve_local_openai_responses_candidate_payload_parts(
Some(trace_id),
transport.key.decrypted_auth_config.as_deref(),
);
crate::ai_serving::apply_codex_openai_responses_lite_header_with_capabilities(
crate::ai_serving::apply_codex_openai_responses_lite_header_for_request_body_with_capabilities(
&mut provider_request_headers,
Some(&provider_request_body),
transport.provider.provider_type.as_str(),
provider_api_format,
mapped_model.as_str(),
@@ -4,6 +4,7 @@ pub(crate) use aether_ai_formats::api::{
aggregate_standard_chat_stream_sync_response, aggregate_standard_cli_stream_sync_response,
api_format_alias_matches, api_format_storage_aliases,
apply_codex_openai_compact_terminal_headers, apply_codex_openai_responses_chat_body_edits,
apply_codex_openai_responses_lite_header_for_request_body_with_capabilities,
apply_codex_openai_responses_lite_header_with_capabilities,
apply_codex_openai_responses_special_body_edits,
apply_codex_openai_responses_special_body_edits_with_source_model_and_capabilities,
@@ -3464,8 +3464,9 @@ async fn provider_query_execute_standard_test_candidate(
.get("model")
.and_then(Value::as_str)
.unwrap_or(request_model);
crate::ai_serving::apply_codex_openai_responses_lite_header_with_capabilities(
crate::ai_serving::apply_codex_openai_responses_lite_header_for_request_body_with_capabilities(
&mut request_headers,
Some(&provider_request_body),
transport.provider.provider_type.as_str(),
provider_api_format,
final_provider_model,
@@ -705,18 +705,15 @@ async fn gateway_executes_codex_cli_stream_via_local_decision_gate_after_oauth_r
seen_execution_runtime_request.thread_id,
seen_execution_runtime_request.prompt_cache_key
);
assert_eq!(seen_execution_runtime_request.responses_lite, "true");
assert!(!seen_execution_runtime_request.has_top_level_tools);
assert!(!seen_execution_runtime_request.has_top_level_instructions);
assert!(seen_execution_runtime_request.has_additional_tools);
assert!(!seen_execution_runtime_request.parallel_tool_calls);
assert!(seen_execution_runtime_request.responses_lite.is_empty());
assert!(seen_execution_runtime_request.has_top_level_tools);
assert!(seen_execution_runtime_request.has_top_level_instructions);
assert!(!seen_execution_runtime_request.has_additional_tools);
assert!(seen_execution_runtime_request.parallel_tool_calls);
assert_eq!(seen_execution_runtime_request.reasoning_effort, "low");
assert_eq!(
seen_execution_runtime_request.reasoning_context,
"all_turns"
);
assert!(seen_execution_runtime_request.reasoning_context.is_empty());
assert!(seen_execution_runtime_request.has_compaction_trigger);
assert!(!seen_execution_runtime_request.has_context_management);
assert!(seen_execution_runtime_request.has_context_management);
let stored_candidates = request_candidate_repository
.list_by_request_id("trace-codex-cli-stream-local-123")
+1
View File
@@ -173,6 +173,7 @@ pub use crate::formats::{
codex::{
apply_codex_openai_compact_terminal_headers,
apply_codex_openai_responses_chat_body_edits,
apply_codex_openai_responses_lite_header_for_request_body_with_capabilities,
apply_codex_openai_responses_lite_header_with_capabilities,
apply_codex_openai_responses_special_body_edits,
apply_codex_openai_responses_special_body_edits_with_source_model_and_capabilities,
@@ -607,10 +607,6 @@ mod tests {
}
}
}],
"context_management": [{
"type": "compaction",
"compact_threshold": 128000
}],
"parallel_tool_calls": true
});
@@ -633,7 +629,6 @@ mod tests {
assert_eq!(body, first);
assert!(body.get("instructions").is_none());
assert!(body.get("tools").is_none());
assert!(body.get("context_management").is_none());
assert_eq!(body["input"][0]["type"], "additional_tools");
assert_eq!(body["input"][0]["role"], "developer");
assert_eq!(body["input"][0]["tools"][0]["name"], "lookup");
@@ -656,6 +651,51 @@ mod tests {
assert!(body["reasoning"].get("summary").is_none());
}
#[test]
fn gpt_5_6_sol_uses_standard_responses_contract_for_server_side_compaction() {
let mut body = json!({
"model": "gpt-5.6-sol",
"instructions": "Preserve the standard Responses request shape.",
"input": [{
"type": "message",
"role": "user",
"content": [{"type": "input_text", "text": "compact"}]
}],
"tools": [{"type": "function", "name": "lookup"}],
"context_management": [{
"type": "compaction",
"compact_threshold": 128000
}],
"parallel_tool_calls": true
});
let finalization = OpenAiProviderRequestFinalization {
source_api_format: "openai:responses",
provider_api_format: "openai:responses",
provider_type: "codex",
provider_model: "gpt-5.6-sol",
source_model: "gpt-5.6-sol",
body_rules: None,
upstream_is_stream: true,
require_body_stream_field: true,
};
finalize_openai_provider_request(&mut body, finalization)
.expect("server-side compaction should use the standard Responses contract");
let first = body.clone();
finalize_openai_provider_request(&mut body, finalization)
.expect("standard Responses finalization should be idempotent");
assert_eq!(body, first);
assert_eq!(body["context_management"][0]["compact_threshold"], 128000);
assert_eq!(
body["instructions"],
"Preserve the standard Responses request shape."
);
assert_eq!(body["tools"][0]["name"], "lookup");
assert_eq!(body["parallel_tool_calls"], true);
assert!(body["reasoning"].get("context").is_none());
}
#[test]
fn opaque_codex_deployments_use_the_exact_source_model_card() {
let mut body = json!({
@@ -1290,6 +1290,16 @@ fn strip_codex_responses_lite_image_details(item: &mut Value) {
}
}
fn codex_responses_lite_supports_context_management(context_management: Option<&Value>) -> bool {
context_management.is_none_or(Value::is_null)
}
fn codex_responses_lite_supports_request_body(provider_request_body: Option<&Value>) -> bool {
provider_request_body.is_none_or(|body| {
codex_responses_lite_supports_context_management(body.get("context_management"))
})
}
fn apply_codex_responses_lite_body_contract(
body_object: &mut serde_json::Map<String, Value>,
capabilities: &CodexResponsesModelCapabilities,
@@ -1298,8 +1308,6 @@ fn apply_codex_responses_lite_body_contract(
return;
}
body_object.remove("context_management");
let tools_are_valid = body_object
.get("tools")
.is_none_or(|tools| tools.is_null() || tools.is_array());
@@ -1573,6 +1581,17 @@ pub fn apply_codex_openai_responses_special_body_edits_with_source_model_and_cap
);
&bundled_capabilities
};
let standard_contract_capabilities = (capabilities.use_responses_lite
&& !codex_responses_lite_supports_context_management(
body_object.get("context_management"),
))
.then(|| CodexResponsesModelCapabilities {
use_responses_lite: false,
..capabilities.clone()
});
let capabilities = standard_contract_capabilities
.as_ref()
.unwrap_or(capabilities);
let supports_reasoning_mode =
crate::formats::shared::model_directives::openai_model_resolves_to_gpt_5_6(
effective_provider_model.as_str(),
@@ -1697,6 +1716,26 @@ pub fn apply_codex_openai_responses_lite_header_with_capabilities(
provider_model: &str,
source_model: &str,
model_capabilities: Option<&CodexResponsesModelCapabilities>,
) {
apply_codex_openai_responses_lite_header_for_request_body_with_capabilities(
provider_request_headers,
None,
provider_type,
provider_api_format,
provider_model,
source_model,
model_capabilities,
);
}
pub fn apply_codex_openai_responses_lite_header_for_request_body_with_capabilities(
provider_request_headers: &mut BTreeMap<String, String>,
provider_request_body: Option<&Value>,
provider_type: &str,
provider_api_format: &str,
provider_model: &str,
source_model: &str,
model_capabilities: Option<&CodexResponsesModelCapabilities>,
) {
if !is_codex_openai_responses_request(provider_type, provider_api_format) {
return;
@@ -1710,7 +1749,9 @@ pub fn apply_codex_openai_responses_lite_header_with_capabilities(
resolve_codex_responses_model_capabilities(provider_model, source_model, None);
&bundled_capabilities
};
if capabilities.use_responses_lite {
if capabilities.use_responses_lite
&& codex_responses_lite_supports_request_body(provider_request_body)
{
provider_request_headers
.insert(CODEX_RESPONSES_LITE_HEADER.to_string(), "true".to_string());
}
@@ -1770,12 +1811,14 @@ pub fn apply_codex_openai_special_headers(
.get("model")
.and_then(Value::as_str)
.unwrap_or_default();
apply_codex_openai_responses_lite_header(
apply_codex_openai_responses_lite_header_for_request_body_with_capabilities(
provider_request_headers,
Some(provider_request_body),
provider_type,
provider_api_format,
provider_model,
provider_model,
None,
);
apply_codex_openai_compact_terminal_headers(
@@ -1790,6 +1833,7 @@ mod tests {
use super::{
apply_codex_openai_responses_chat_body_edits,
apply_codex_openai_responses_compact_body_edits,
apply_codex_openai_responses_lite_header_for_request_body_with_capabilities,
apply_codex_openai_responses_lite_header_with_capabilities,
apply_codex_openai_responses_special_body_edits,
apply_codex_openai_responses_special_body_edits_with_source_model_and_capabilities,
@@ -1897,6 +1941,33 @@ mod tests {
);
}
#[test]
fn codex_responses_lite_header_is_omitted_for_server_side_compaction() {
let mut headers = std::collections::BTreeMap::from([(
CODEX_RESPONSES_LITE_HEADER.to_string(),
"true".to_string(),
)]);
let body = json!({
"model": "gpt-5.6-sol",
"context_management": [{
"type": "compaction",
"compact_threshold": 128000
}]
});
apply_codex_openai_responses_lite_header_for_request_body_with_capabilities(
&mut headers,
Some(&body),
"codex",
"openai:responses",
"gpt-5.6-sol",
"gpt-5.6-sol",
None,
);
assert!(!headers.contains_key(CODEX_RESPONSES_LITE_HEADER));
}
#[test]
fn model_card_without_summary_parameter_support_keeps_reasoning_and_ids() {
let metadata = build_codex_model_catalog_metadata(&[json!({