From a18f0a2e1118d847196cdc941cf65636817e32e2 Mon Sep 17 00:00:00 2001 From: helen <50317361+helen-frank@users.noreply.github.com> Date: Sat, 19 Sep 2026 22:43:49 +0800 Subject: [PATCH] fix(openai): normalize system messages for Codex (#172) Normalize explicit system messages to developer at the shared Codex request boundary while preserving message content and other fields. Cover HTTP and WebSocket request bodies and document the compatibility behavior. Co-authored-by: Codex --- .../openai/src/transport/downstream/body.rs | 17 +++- .../openai/tests/transport/downstream/body.rs | 87 +++++++++++++++++-- docs/api.md | 4 +- 3 files changed, 101 insertions(+), 7 deletions(-) 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` 内的同名业务字段;其他未知字段继续透传。