环境
- CCSwitchMulti v3.19.2-18(macOS,commit
e470fff)
- Codex 桌面端(wire_api = responses),经 MultiRouter 路由到第三方 provider
- 第三方上游:OpenAI 兼容中转(tm-token.com),严格按 OpenAI 官方 Responses schema 校验
- 模型
gpt-5.6-terra,model_reasoning_effort = "high"
现象
对话进行到中途(历史里积累了若干 reasoning item 后),每次请求都被上游 400 拒绝:
Invalid 'input[42].content': array too long.
Expected an array with maximum length 0, but got an array with length 1 instead.
对话一旦触发即永久卡死(历史不会消失,每次请求都带同样的 reasoning item)。
关键观察:Codex 原始发出的请求是合法的(reasoning item 只有 summary,无 content)。用透明代理在 CCSwitchMulti 和上游之间抓包对比,发现 400 的请求是 CCSwitchMulti 转发前改写过的:
- Codex 原始请求:reasoning item 结构正常(
summary + 无 content)
- CCSwitchMulti 发出的请求:每个 reasoning item 都被塞进了
content: [{type:"reasoning_text", text: <summary 原文>}],且 summary/encrypted_content 被移除(本例中 35 个 reasoning item 全部被改写)
按 OpenAI 官方 schema,reasoning item 的 content 最大长度为 0,上游校验直接拒绝。
根因定位
src-tauri/src/proxy/providers/openai_compat.rs:321(normalize_third_party_responses_reasoning_item):
object.insert(
"content".to_string(),
Value::Array(vec![json!({ "type": "reasoning_text", "text": text })]),
);
object.remove("summary");
object.remove("encrypted_content");
调用点在 src-tauri/src/proxy/forwarder.rs:2772,触发条件是 should_normalize_codex_responses_passthrough_control_messages(forwarder.rs:7023)——对所有第三方原生 Responses 透传上游无差别执行。
注释里写明了设计动机(“第三方原生 Responses 上游(DeepSeek 等)要求 reasoning 历史以 reasoning_text content 回传”),这个假设对 DeepSeek 系成立,但对严格按 OpenAI 官方 schema 校验的第三方上游(如本例)恰好相反——它们要求 reasoning.content 必须为空数组。两类上游的期望互斥,无差别的归一化必伤其一。
当前版本无开关可绕过
- 路由
capabilities 只有 supportsRemoteCompaction
codex_reasoning_manual_overrides / resolve_codex_chat_reasoning_projection 只作用于 Chat 转换路径,不影响这条 Responses 透传归一化
修复建议(任一即可)
- 按协议探测结果门控:
protocol_compatibility_profiles 已经记录了上游的 reasoning_shape(semantic/source/pre_tool_visible_content)。探测能区分上游是否原生返回 reasoning,同理应能探测“回传带 content 的 reasoning item 是否被接受”,仅对接受的 DeepSeek 系上游启用该归一化;
- 加路由级 capability 开关:类似
supportsRemoteCompaction,如 supportsReasoningContentReplay,默认关闭,让用户显式声明上游风格。
当前 workaround
把该路由的 upstream.apiFormat 改为 openai_chat(走 Responses→Chat 转换,跳过这条归一化路径),问题消失。但代价是放弃了原生 Responses 透传,且依赖 Chat 转换边界的行为完全一致。
如果需要完整证据(抓包对比文件、探测档案记录、400 响应原文),我可以补充。
环境
e470fff)gpt-5.6-terra,model_reasoning_effort = "high"现象
对话进行到中途(历史里积累了若干 reasoning item 后),每次请求都被上游 400 拒绝:
对话一旦触发即永久卡死(历史不会消失,每次请求都带同样的 reasoning item)。
关键观察:Codex 原始发出的请求是合法的(reasoning item 只有 summary,无 content)。用透明代理在 CCSwitchMulti 和上游之间抓包对比,发现 400 的请求是 CCSwitchMulti 转发前改写过的:
summary+ 无content)content: [{type:"reasoning_text", text: <summary 原文>}],且summary/encrypted_content被移除(本例中 35 个 reasoning item 全部被改写)按 OpenAI 官方 schema,
reasoningitem 的content最大长度为 0,上游校验直接拒绝。根因定位
src-tauri/src/proxy/providers/openai_compat.rs:321(normalize_third_party_responses_reasoning_item):调用点在
src-tauri/src/proxy/forwarder.rs:2772,触发条件是should_normalize_codex_responses_passthrough_control_messages(forwarder.rs:7023)——对所有第三方原生 Responses 透传上游无差别执行。注释里写明了设计动机(“第三方原生 Responses 上游(DeepSeek 等)要求 reasoning 历史以 reasoning_text content 回传”),这个假设对 DeepSeek 系成立,但对严格按 OpenAI 官方 schema 校验的第三方上游(如本例)恰好相反——它们要求
reasoning.content必须为空数组。两类上游的期望互斥,无差别的归一化必伤其一。当前版本无开关可绕过
capabilities只有supportsRemoteCompactioncodex_reasoning_manual_overrides/resolve_codex_chat_reasoning_projection只作用于 Chat 转换路径,不影响这条 Responses 透传归一化修复建议(任一即可)
protocol_compatibility_profiles已经记录了上游的reasoning_shape(semantic/source/pre_tool_visible_content)。探测能区分上游是否原生返回 reasoning,同理应能探测“回传带 content 的 reasoning item 是否被接受”,仅对接受的 DeepSeek 系上游启用该归一化;supportsRemoteCompaction,如supportsReasoningContentReplay,默认关闭,让用户显式声明上游风格。当前 workaround
把该路由的
upstream.apiFormat改为openai_chat(走 Responses→Chat 转换,跳过这条归一化路径),问题消失。但代价是放弃了原生 Responses 透传,且依赖 Chat 转换边界的行为完全一致。如果需要完整证据(抓包对比文件、探测档案记录、400 响应原文),我可以补充。