Skip to content

Commit 7778116

Browse files
committed
fix(codex): strip invalid content from oauth response items
Root cause: the Codex OAuth passthrough normalizer preserved Desktop-provided input arrays verbatim. Some follow-up tool output items can carry message-style content, but the ChatGPT Codex backend schema for function_call_output, custom_tool_call_output, and tool_search_output does not allow that field, causing HTTP 400 such as Invalid input[3].content: array too long. Change: normalize official managed Codex OAuth passthrough input items before forwarding. Message and reasoning items keep content; non-message tool/call output items drop redundant content while preserving output, tools, call_id, status, and execution. Public Responses and Responses-to-Chat paths are not routed through this cleanup. Memory: recorded the bug signature, root-cause boundary, and regression-test location in memory.md. Verification: cargo fmt --manifest-path src-tauri/Cargo.toml --check; cargo test --manifest-path src-tauri/Cargo.toml codex_responses_request_normalizer --lib -- --nocapture; cargo test --manifest-path src-tauri/Cargo.toml responses_request_to_chat_ --lib -- --nocapture; git diff --check.
1 parent 6d00feb commit 7778116

2 files changed

Lines changed: 117 additions & 1 deletion

File tree

memory.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# CC Switch Repository Memory
22

3+
## 2026-07-01 Codex OAuth Responses Passthrough Content Shape
4+
5+
- 用户截图里的 `Invalid 'input[3].content': array too long. Expected an array with maximum length 0, but got an array with length 1 instead.` 发生在 Codex `/responses` 直透 ChatGPT Codex OAuth backend,日志特征是 `responses_to_chat=false``responses_to_messages=false``upstream_url=https://chatgpt.com/backend-api/codex/responses`。这不是第三方 Chat 转换问题,也不是模型容量问题。
6+
- 根因是 `openai_compat::normalize_codex_oauth_responses_request` 对 Codex Desktop 已经发来的 `input` 数组只做字段补齐,未清理非 message item 上的冗余 `content`。官方 Codex app-server protocol 的 `function_call_output``custom_tool_call_output``tool_search_output` 只允许 `output/tools/call_id/status/execution` 等字段,不允许携带 message-style `content`;ChatGPT backend 会把该 content 视为长度必须为 0 的数组并返回 400。
7+
- 修复边界:只在 official managed Codex OAuth passthrough normalizer 中清理 input item,保留 `message``reasoning``content`,删除 tool/call/web/image/compaction 等非 message item 的 `content`。公开 OpenAI Responses、第三方 Responses、Responses->Chat 转换路径不调用这条清理逻辑,避免扩大行为变更。
8+
- 回归测试落点:`openai_compat.rs::codex_responses_request_normalizer_strips_content_from_tool_output_items` 覆盖 function/custom/tool-search output item 带 `content` 时会被删除,同时保留 `output/tools` 和普通 user message content。
9+
310
## 2026-07-01 CCSwitchMulti v3.16.4-5 Formal Release
411

512
- CCSwitchMulti 正式发布远端是 `fork` (`BigStrongSun/ccswitchmulti`);`origin`/`upstream` 指向原版 `farion1231/cc-switch`,发布、tag、asset upload 都不能推到 `origin`

src-tauri/src/proxy/providers/openai_compat.rs

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,69 @@ fn normalize_codex_oauth_responses_input(body: &mut Map<String, Value>) {
146146
Some(other) => codex_oauth_input_text_message(other.to_string()),
147147
};
148148

149-
body.insert("input".to_string(), input);
149+
body.insert(
150+
"input".to_string(),
151+
normalize_codex_oauth_input_items(input),
152+
);
153+
}
154+
155+
/// 清理 Codex OAuth backend 不接受的 input item 冗余字段。
156+
///
157+
/// 参数:
158+
/// - `input`: 已经规整成 Responses `input` 数组或单条消息的 JSON。
159+
/// 返回:
160+
/// - 清理后的 `input` JSON,保留 message/reasoning 的 content,删除 tool/call item 上的 content。
161+
/// 副作用:
162+
/// - 无。该函数只修改传入 JSON 的内存副本。
163+
/// 边界:
164+
/// - 只服务 ChatGPT Codex OAuth 私有 backend;公开 OpenAI Responses API 兼容路径不调用。
165+
fn normalize_codex_oauth_input_items(input: Value) -> Value {
166+
let Value::Array(items) = input else {
167+
return input;
168+
};
169+
170+
Value::Array(
171+
items
172+
.into_iter()
173+
.map(normalize_codex_oauth_input_item)
174+
.collect(),
175+
)
176+
}
177+
178+
/// 清理单个 Codex Responses input item 的私有 backend 不兼容字段。
179+
///
180+
/// 参数:
181+
/// - `item`: 一条 Responses input item。
182+
/// 返回:
183+
/// - 若是非 message/reasoning item,则删除多余 `content`;其他字段原样保留。
184+
/// 副作用:
185+
/// - 无。
186+
fn normalize_codex_oauth_input_item(item: Value) -> Value {
187+
let Value::Object(mut object) = item else {
188+
return item;
189+
};
190+
191+
let item_type = object
192+
.get("type")
193+
.and_then(Value::as_str)
194+
.unwrap_or_default();
195+
if !codex_oauth_input_item_allows_content(item_type) {
196+
object.remove("content");
197+
}
198+
199+
Value::Object(object)
200+
}
201+
202+
/// 判断 Codex OAuth backend 的 input item 是否允许携带 `content`。
203+
///
204+
/// 参数:
205+
/// - `item_type`: Responses input item 的 `type` 字段。
206+
/// 返回:
207+
/// - `true` 表示该 item 可以保留 content;`false` 表示 content 是冗余字段,应移除。
208+
/// 副作用:
209+
/// - 无。
210+
fn codex_oauth_input_item_allows_content(item_type: &str) -> bool {
211+
matches!(item_type, "message" | "reasoning")
150212
}
151213

152214
/// 构造 Codex Responses 兼容的单条 user text message。
@@ -1224,6 +1286,53 @@ mod tests {
12241286
);
12251287
}
12261288

1289+
#[test]
1290+
fn codex_responses_request_normalizer_strips_content_from_tool_output_items() {
1291+
// ChatGPT Codex OAuth backend 的 tool/call output item 不接受 content 数组;
1292+
// Codex Desktop 某些工具回传会额外带 content,必须在直透前删除。
1293+
let body = json!({
1294+
"model": "gpt-5.4",
1295+
"input": [
1296+
{
1297+
"type": "message",
1298+
"role": "user",
1299+
"content": [{ "type": "input_text", "text": "run tool" }]
1300+
},
1301+
{
1302+
"type": "function_call_output",
1303+
"call_id": "call_fn",
1304+
"output": "done",
1305+
"content": [{ "type": "output_text", "text": "done" }]
1306+
},
1307+
{
1308+
"type": "custom_tool_call_output",
1309+
"call_id": "call_custom",
1310+
"output": { "body": "patched" },
1311+
"content": [{ "type": "output_text", "text": "patched" }]
1312+
},
1313+
{
1314+
"type": "tool_search_output",
1315+
"call_id": "call_search",
1316+
"status": "completed",
1317+
"execution": "client",
1318+
"tools": [],
1319+
"content": [{ "type": "output_text", "text": "[]" }]
1320+
}
1321+
]
1322+
});
1323+
1324+
let normalized = normalize_codex_oauth_responses_request(body);
1325+
let input = normalized["input"].as_array().expect("input array");
1326+
1327+
assert!(input[0].get("content").is_some());
1328+
assert!(input[1].get("content").is_none());
1329+
assert!(input[2].get("content").is_none());
1330+
assert!(input[3].get("content").is_none());
1331+
assert_eq!(input[1]["output"], "done");
1332+
assert_eq!(input[2]["output"]["body"], "patched");
1333+
assert_eq!(input[3]["tools"], json!([]));
1334+
}
1335+
12271336
#[test]
12281337
fn responses_json_maps_to_chat_completion() {
12291338
let response = json!({

0 commit comments

Comments
 (0)