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
@@ -463,8 +463,8 @@ async fn gateway_executes_openai_chat_stream_via_local_openai_responses_cross_fo
accept: String,
authorization: String,
x_client_request_id: String,
session_id: String,
conversation_id: String,
codex_session_id: String,
codex_thread_id: String,
instructions: String,
user_text: String,
prompt_cache_key: String,
@@ -742,15 +742,15 @@ async fn gateway_executes_openai_chat_stream_via_local_openai_responses_cross_fo
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
session_id: payload
codex_session_id: payload
.get("headers")
.and_then(|value| value.get("session_id"))
.and_then(|value| value.get("session-id"))
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
conversation_id: payload
codex_thread_id: payload
.get("headers")
.and_then(|value| value.get("conversation_id"))
.and_then(|value| value.get("thread-id"))
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
@@ -889,19 +889,16 @@ async fn gateway_executes_openai_chat_stream_via_local_openai_responses_cross_fo
);
assert_eq!(
seen_execution_runtime_request.x_client_request_id,
"trace-openai-chat-cli-local-123"
seen_execution_runtime_request.codex_thread_id
);
assert_eq!(
seen_execution_runtime_request.prompt_cache_key,
"bc749eb7-a9e2-5793-8d14-abd659c700b0"
seen_execution_runtime_request.codex_session_id,
seen_execution_runtime_request.codex_thread_id
);
assert_eq!(
seen_execution_runtime_request.session_id,
"d1e9b802644e1f52"
);
assert_eq!(
seen_execution_runtime_request.conversation_id,
"d1e9b802644e1f52"
assert!(seen_execution_runtime_request.prompt_cache_key.is_empty());
assert_ne!(
seen_execution_runtime_request.codex_thread_id,
seen_execution_runtime_request.trace_id
);
assert_eq!(
seen_execution_runtime_request.instructions,
@@ -2,7 +2,6 @@ use super::{
any, build_router_with_state, build_state_with_execution_runtime_override, json, start_server,
to_bytes, Arc, Body, Json, Mutex, Request, Router, StatusCode, TRACE_ID_HEADER,
};
use crate::ai_serving::CODEX_OPENAI_IMAGE_INTERNAL_MODEL;
use aether_crypto::{encrypt_python_fernet_plaintext, DEVELOPMENT_ENCRYPTION_KEY};
use aether_data::repository::auth::{
InMemoryAuthApiKeySnapshotRepository, StoredAuthApiKeySnapshot,
@@ -55,13 +54,9 @@ async fn gateway_executes_codex_image_stream_via_local_decision_gate_after_oauth
struct SeenExecutionRuntimeStreamRequest {
trace_id: String,
url: String,
model: String,
authorization: String,
x_client_request_id: String,
tool_type: String,
tool_action: String,
tool_partial_images: Option<u64>,
request_stream: bool,
headers: serde_json::Value,
body: serde_json::Value,
plan_stream: bool,
}
@@ -180,7 +175,7 @@ async fn gateway_executes_codex_image_stream_via_local_decision_gate_after_oauth
None,
Some(2),
None,
Some(serde_json::json!({"upstream_stream_policy":"force_stream"})),
None,
None,
None,
)
@@ -272,65 +267,26 @@ async fn gateway_executes_codex_image_stream_via_local_decision_gate_after_oauth
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
model: payload
.get("body")
.and_then(|value| value.get("json_body"))
.and_then(|value| value.get("model"))
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
authorization: payload
.get("headers")
.and_then(|value| value.get("authorization"))
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
x_client_request_id: payload
.get("headers")
.and_then(|value| value.get("x-client-request-id"))
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
tool_type: payload
headers: payload.get("headers").cloned().unwrap_or_default(),
body: payload
.get("body")
.and_then(|value| value.get("json_body"))
.and_then(|value| value.get("tools"))
.and_then(|value| value.get(0))
.and_then(|value| value.get("type"))
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
tool_action: payload
.get("body")
.and_then(|value| value.get("json_body"))
.and_then(|value| value.get("tools"))
.and_then(|value| value.get(0))
.and_then(|value| value.get("action"))
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
tool_partial_images: payload
.get("body")
.and_then(|value| value.get("json_body"))
.and_then(|value| value.get("tools"))
.and_then(|value| value.get(0))
.and_then(|value| value.get("partial_images"))
.and_then(|value| value.as_u64()),
request_stream: payload
.get("body")
.and_then(|value| value.get("json_body"))
.and_then(|value| value.get("stream"))
.and_then(|value| value.as_bool())
.unwrap_or(false),
.cloned()
.unwrap_or_default(),
plan_stream: payload
.get("stream")
.and_then(|value| value.as_bool())
.unwrap_or(false),
});
let frames = concat!(
"{\"type\":\"headers\",\"payload\":{\"kind\":\"headers\",\"status_code\":200,\"headers\":{\"content-type\":\"text/event-stream\"}}}\n",
"{\"type\":\"data\",\"payload\":{\"kind\":\"data\",\"text\":\"event: response.output_item.done\\ndata: {\\\"type\\\":\\\"response.output_item.done\\\",\\\"output_index\\\":0,\\\"item\\\":{\\\"id\\\":\\\"ig_123\\\",\\\"type\\\":\\\"image_generation_call\\\",\\\"result\\\":\\\"aGVsbG8=\\\"}}\\n\\n\"}}\n",
"{\"type\":\"data\",\"payload\":{\"kind\":\"data\",\"text\":\"event: response.completed\\ndata: {\\\"type\\\":\\\"response.completed\\\",\\\"response\\\":{\\\"tool_usage\\\":{\\\"image_gen\\\":{\\\"input_tokens\\\":11,\\\"output_tokens\\\":22,\\\"total_tokens\\\":33}}}}\\n\\n\"}}\n",
"{\"type\":\"headers\",\"payload\":{\"kind\":\"headers\",\"status_code\":200,\"headers\":{\"content-type\":\"application/json\"}}}\n",
"{\"type\":\"data\",\"payload\":{\"kind\":\"data\",\"text\":\"{\\\"created\\\":1776991097,\\\"data\\\":[{\\\"b64_json\\\":\\\"aGVsbG8=\\\",\\\"revised_prompt\\\":\\\"水墨视觉海报\\\"}],\\\"usage\\\":{\\\"input_tokens\\\":11,\\\"output_tokens\\\":22,\\\"total_tokens\\\":33}}\"}}\n",
"{\"type\":\"telemetry\",\"payload\":{\"kind\":\"telemetry\",\"telemetry\":{\"elapsed_ms\":41}}}\n",
"{\"type\":\"eof\",\"payload\":{\"kind\":\"eof\"}}\n"
);
@@ -397,26 +353,28 @@ async fn gateway_executes_codex_image_stream_via_local_decision_gate_after_oauth
)
.header(TRACE_ID_HEADER, "trace-codex-image-stream-local-123")
.body(
"{\"model\":\"gpt-image-2\",\"prompt\":\"生成一张中国历史视觉海报\",\"stream\":true,\"partial_images\":1}",
"{\"model\":\"gpt-image-2\",\"prompt\":\"生成一张水墨视觉海报\",\"background\":\"auto\",\"quality\":\"auto\",\"size\":\"auto\",\"stream\":true,\"response_format\":\"b64_json\"}",
)
.send()
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
response
.headers()
.get(http::header::CONTENT_TYPE)
.and_then(|value| value.to_str().ok()),
Some("text/event-stream")
);
let response_status = response.status();
let response_content_type = response
.headers()
.get(http::header::CONTENT_TYPE)
.and_then(|value| value.to_str().ok())
.map(str::to_string);
let response_text = response.text().await.expect("body should read");
assert!(response_text.contains("event: image_generation.partial_image"));
assert!(response_text.contains("\"type\":\"image_generation.partial_image\""));
assert!(response_text.contains("\"b64_json\":\"aGVsbG8=\""));
assert_eq!(response_status, StatusCode::OK, "{response_text}");
assert_eq!(
response_content_type.as_deref(),
Some("text/event-stream"),
"{response_text}"
);
assert!(response_text.contains("event: image_generation.completed"));
assert!(response_text.contains("\"type\":\"image_generation.completed\""));
assert!(response_text.contains("\"b64_json\":\"aGVsbG8=\""));
assert!(response_text.contains("\"total_tokens\":33"));
assert!(!response_text.contains("response.completed"));
@@ -444,25 +402,34 @@ async fn gateway_executes_codex_image_stream_via_local_decision_gate_after_oauth
);
assert_eq!(
seen_execution_runtime_request.url,
"https://chatgpt.com/backend-api/codex/responses"
);
assert_eq!(
seen_execution_runtime_request.model,
CODEX_OPENAI_IMAGE_INTERNAL_MODEL
"https://chatgpt.com/backend-api/codex/images/generations"
);
assert_eq!(
seen_execution_runtime_request.authorization,
"Bearer refreshed-codex-image-stream-access-token"
);
assert_eq!(
seen_execution_runtime_request.x_client_request_id,
"trace-codex-image-stream-local-123"
seen_execution_runtime_request.body,
json!({
"prompt": "生成一张水墨视觉海报",
"background": "auto",
"model": "gpt-image-2",
"quality": "auto",
"size": "auto"
})
);
assert_eq!(seen_execution_runtime_request.tool_type, "image_generation");
assert_eq!(seen_execution_runtime_request.tool_action, "generate");
assert_eq!(seen_execution_runtime_request.tool_partial_images, Some(1));
assert!(seen_execution_runtime_request.request_stream);
assert!(seen_execution_runtime_request.plan_stream);
assert_eq!(
seen_execution_runtime_request.headers["user-agent"],
"codex_cli_rs/0.144.1"
);
assert_eq!(
seen_execution_runtime_request.headers["originator"],
"codex_cli_rs"
);
for header in ["x-client-request-id", "session-id", "thread-id"] {
assert!(seen_execution_runtime_request.headers.get(header).is_none());
}
assert!(!seen_execution_runtime_request.plan_stream);
gateway_handle.abort();
execution_runtime_handle.abort();
@@ -594,7 +561,7 @@ async fn gateway_bridges_codex_image_sync_json_to_streaming_image_sse_impl() {
None,
Some(2),
None,
Some(serde_json::json!({"upstream_stream_policy":"force_stream"})),
None,
None,
None,
)
@@ -772,8 +739,8 @@ async fn gateway_bridges_codex_image_sync_json_to_streaming_image_sse_impl() {
seen_execution_runtime_request.trace_id,
"trace-codex-image-stream-json-123"
);
assert!(seen_execution_runtime_request.request_stream);
assert!(seen_execution_runtime_request.plan_stream);
assert!(!seen_execution_runtime_request.request_stream);
assert!(!seen_execution_runtime_request.plan_stream);
gateway_handle.abort();
execution_runtime_handle.abort();
@@ -1152,12 +1119,14 @@ async fn gateway_routes_openai_responses_stream_image_intent_to_openai_image_pla
seen_plan.url,
"https://images.example.com/v1/images/generations"
);
assert!(seen_plan.plan_stream);
assert!(!seen_plan.plan_stream);
assert_eq!(seen_plan.auth_header, "Bearer sk-upstream-image-bridge");
assert_eq!(seen_plan.body_json["stream"], true);
assert_eq!(seen_plan.body_json["input"], "Draw a mountain observatory");
assert_eq!(seen_plan.body_json["tools"][0]["type"], "image_generation");
assert_eq!(seen_plan.body_json["tools"][0]["size"], "1024x1024");
assert_eq!(seen_plan.body_json["model"], "gpt-image-2");
assert_eq!(seen_plan.body_json["prompt"], "Draw a mountain observatory");
assert_eq!(seen_plan.body_json["size"], "1024x1024");
assert!(seen_plan.body_json.get("stream").is_none());
assert!(seen_plan.body_json.get("input").is_none());
assert!(seen_plan.body_json.get("tools").is_none());
gateway_handle.abort();
execution_runtime_handle.abort();