Skip to content

第三方原生 Responses 上游严格校验时,normalize_third_party_responses_reasoning_item 强制写入 content 导致 400 #71

Description

@Tarschu

环境

  • CCSwitchMulti v3.19.2-18(macOS,commit e470fff
  • Codex 桌面端(wire_api = responses),经 MultiRouter 路由到第三方 provider
  • 第三方上游:OpenAI 兼容中转(tm-token.com),严格按 OpenAI 官方 Responses schema 校验
  • 模型 gpt-5.6-terramodel_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:321normalize_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 透传归一化

修复建议(任一即可)

  1. 按协议探测结果门控protocol_compatibility_profiles 已经记录了上游的 reasoning_shape(semantic/source/pre_tool_visible_content)。探测能区分上游是否原生返回 reasoning,同理应能探测“回传带 content 的 reasoning item 是否被接受”,仅对接受的 DeepSeek 系上游启用该归一化;
  2. 加路由级 capability 开关:类似 supportsRemoteCompaction,如 supportsReasoningContentReplay,默认关闭,让用户显式声明上游风格。

当前 workaround

把该路由的 upstream.apiFormat 改为 openai_chat(走 Responses→Chat 转换,跳过这条归一化路径),问题消失。但代价是放弃了原生 Responses 透传,且依赖 Chat 转换边界的行为完全一致。

如果需要完整证据(抓包对比文件、探测档案记录、400 响应原文),我可以补充。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions