diff --git a/backend/crates/providers/openai/src/transport/downstream/body.rs b/backend/crates/providers/openai/src/transport/downstream/body.rs index 135a6aa84..14c82d174 100644 --- a/backend/crates/providers/openai/src/transport/downstream/body.rs +++ b/backend/crates/providers/openai/src/transport/downstream/body.rs @@ -2,7 +2,7 @@ use serde_json::{Map, Value}; -/// 补齐 Codex 请求缺省字段并移除已确认不适用的顶层参数,不递归清洗业务正文。 +/// 补齐 Codex 请求缺省字段并适配已确认不兼容的请求形状,不递归清洗业务正文。 /// /// 兼容基准是 Codex Core/Desktop 的模型请求,不是公开 OpenAI Responses API。 /// 未知字段继续透传,不能因官方请求结构中没有某个字段就将其列入过滤规则。 @@ -10,6 +10,21 @@ pub(in crate::transport) fn normalize_codex_request_body(body: &mut Map(&body).expect("HTTP request JSON") }); - let mut request = encode_downstream_request(json!({"model": "gpt-test", "input": "hello"})); + let mut request = encode_downstream_request(json!({ + "model": "gpt-test", + "input": [{"type": "message", "role": "system", "content": "Be concise."}] + })); request.force_http_sse = true; let client = CodexBackendClient::new( reqwest::Client::builder() @@ -127,13 +194,16 @@ async fn backend_http_should_send_default_store_for_downstream_request() { assert_eq!( server.await.expect("HTTP server task"), json!({ - "model": "gpt-test", "input": "hello", "store": false, "stream": true + "model": "gpt-test", + "input": [{"type": "message", "role": "developer", "content": "Be concise."}], + "store": false, + "stream": true }) ); } #[tokio::test] -async fn backend_websocket_should_send_default_store_for_downstream_request() { +async fn backend_websocket_should_send_default_store_and_normalized_system_role() { let listener = TcpListener::bind("127.0.0.1:0") .await .expect("bind WebSocket server"); @@ -156,7 +226,10 @@ async fn backend_websocket_should_send_default_store_for_downstream_request() { .expect("send terminal event"); body }); - let mut request = encode_downstream_request(json!({"model": "gpt-test", "input": "hello"})); + let mut request = encode_downstream_request(json!({ + "model": "gpt-test", + "input": [{"type": "message", "role": "system", "content": "Be concise."}] + })); request.use_websocket = true; let client = CodexBackendClient::new( reqwest::Client::builder() @@ -178,6 +251,10 @@ async fn backend_websocket_should_send_default_store_for_downstream_request() { let body = server.await.expect("WebSocket server task"); assert_eq!(body.get("store"), Some(&json!(false))); + assert_eq!( + body["input"], + json!([{"type": "message", "role": "developer", "content": "Be concise."}]) + ); } fn encode_downstream_request(body: Value) -> CodexResponsesRequest { diff --git a/docs/api.md b/docs/api.md index 08faf50ec..4f56f72ea 100644 --- a/docs/api.md +++ b/docs/api.md @@ -190,7 +190,9 @@ Responses 也不透传 `x-stainless-*`、`Origin`、`Referer`、`sec-ch-ua*` 和 Responses 上游编码会移除 Codex 不接受的顶层 `temperature`、`max_output_tokens` 和 `prompt_cache_retention`。缺少顶层 `store` 时补齐 `false`,与官方 Codex 客户端一致; -显式提供的值保持原样。HTTP/SSE 与 WebSocket 共用这条正文兼容规则。 +显式提供的值保持原样。`input` 数组中显式指定 `type: "message"` 且 `role: "system"` 的消息, +其角色转换为 Codex 接受的 `developer`;消息内容、顺序、其他字段和顶层 `instructions` 保持不变。 +HTTP/SSE 与 WebSocket 共用这条正文兼容规则。 `prompt_cache_key`、`reasoning`、`include` 等 Codex 参数继续保留。过滤只作用于顶层, 不删除工具参数 schema、输入内容或 `client_metadata` 内的同名业务字段;其他未知字段继续透传。