DeepSeek Harness 第二轮才 400:redacted_thinking 与 Gemini schema
第一步成功了,第二步才炸——这个顺序本身就是线索:块不是在「收到」时出问题,而是在「回放」时出问题。 第一步的响应里带了 Anthropic 的 redacted_thinking 块,它被写进历史;第二步要把历史重新翻译成请求,适配器不认这个块,直接抛 UNSUPPORTED_CONTENT(#11139)。同族的另一条时间线在 Gemini 上:工具 schema 每一轮都被原样重发,所以一坏就是整场坏(#10904)。
DeepSeek Harness 第一轮:块第一次返回,还没进入下一份请求
第一轮能成功,是因为这个块只是被接收;此时代码走的是正常的分块读取路径,没有任何地方要求「把它翻译出去」。
- 看环境:
@deepseek-ai/dsh-llm-deepseek0.2.1-rc.3,端点是通过 Bifrost 的 Anthropic 兼容网关。 - 第一步:agent 调一个 MCP 工具,成功返回。
- 响应里出现
redacted_thinking块——这是 Anthropic 协议定义的块类型,data是签名载荷。 - 此时会话还正常,用户看不出任何异常。
- 但这一步的响应已经落盘,成为下一轮要回放的历史。
DSH插件 第二轮:历史回放时它被重新翻译
回放要把每个历史块翻译成 DeepSeek Messages 能表示的形态;startBlock() 只处理 text、thinking、tool_use,其余走 default 分支抛错。
- 看报错:
DeepSeek Messages does not support response block redacted_thinking,code=UNSUPPORTED_CONTENT。 - 安装包里抛错位置在
lib/index.js:1878,源码对应packages/llm/llm-deepseek的翻译层。 - 事件流显示
assistant/attempt以 error 结束,turn/end带同一个错误。 - 后果:第二步之后的所有工具调用都跑不了,多步 agent 循环直接中断。
- 换模型(
deepseek-v4-pro与deepseek-flash)无效,设reasoningEffort: none也无效——触发的是块类型,不是模型或推理档位。
DeepSeek Harness 为什么「丢掉这个块」不是修法
它的 data 是签名载荷,后续步骤依赖它原样回传;接受但不记录,会让推理连续性静默变化,失败从大声变成无声。
- 仓库里的协议往返测试已经把要求写清楚:输入
{ type: 'redacted_thinking', data: '...' }。 - 记录形式是
{ type: 'reasoning', thinkingSignature: '...', redacted: true }。 - 断言要求下一次请求仍然带着这份载荷。
- 而
dsh-llm-deepseek这条链路有三处要一起改:翻译层对未知块抛错、ReplayBlock类型没有 redacted 标记与 data 字段、序列化层只会把 reasoning 写成thinking + signature。 - 所以真正的修复是「接受并记录,再在原索引重新发出」,属于持久化结构的变更,需要版本号配合。
另一个边界也要说清:抛错发生在 translate() 的生成器里,早于任何 llm/stream 监听器。外层包装只能看到「一个错误」,看不到那个块;而且请求侧也需要载荷已经被持久化,否则外部没有东西可回放。
DeepSeek插件 同族时间线:Gemini 的 schema 每轮都在重发
另一条「第二轮才炸」的变体更极端:它不是第二轮,而是每一轮;因为完整工具清单每轮都发,一个坏 schema 就让整场失败。
- 现象:会话切到 Gemini 后,每条消息都返回
400 INVALID_ARGUMENT,与内容无关。 - 对照实验:无工具 200、扁平 schema 200、
oneOf200;含$ref、$defs、additionalProperties全部 400。 - 报错细节:
Invalid JSON payload received. Unknown name "$defs" at 'tools[0].function_declarations[0].parameters',而 UI 只显示泛化的Request contains an invalid argument.。 - 原因:DSH 把工具 schema 原样转发,Gemini 只接受 OpenAPI 3.0 子集。
| schema 形态 | 结果 |
|---|---|
无工具 / 扁平 schema / oneOf | 200 |
含 $ref / $defs | 400 |
含 additionalProperties | 400 |
- 时间线差异:
redacted_thinking是「上一轮留下的东西在下一轮炸」,schema 是「每一轮都带同一份清单,所以每轮都炸」。
社区核对后还指出两个结构性事实:additionalProperties 在核心 schema 语言里是 type: 'object' 的必填项,所以大量内置工具天然带着它;而 MCP 桥把服务器的 inputSchema 原样挂上去,$schema、examples 一个不改。
DeepSeek Harness 两条临时路线,按代价排序
redacted_thinking 换走 pi-ai 的 anthropic-messages 路由;Gemini schema 先挂规范化插件做观察,再等核心在适配器边界做降级。
- 换路由(redacted 场景):用 pi-ai 适配器声明
anthropic路由,api: anthropic-messages,指向同一网关。仓库内的往返测试表明这条路径能处理 redacted 块。 - 这条的证据是「对 mock Anthropic 服务器的树内测试」,不是对具体网关的实测,按「能处理该块的适配器」来理解,而不是「已验证的修复」。
- 挂规范化插件(Gemini 场景):
@argszero/dsh-mcp-schema-normalizer0.2.0,提供inline-ref、prune-defs、drop-additional-properties三条规则。 - 先开
mode: observe,它只报告会改什么,不改请求;确认命中范围后再切normalize。 - 顺序上要挂在连接 MCP 服务器的那一行之前。
- 认清边界:它只作用于
mcp__<server>__<tool>,内置工具与$schema、examples仍会 400。
社区实测的对比数据很直观:挂载前后,$ref 从 745 降到 0、$defs 从 169 降到 0、additionalProperties 从 1422 降到 12;剩下 12 个来自 ask_user_question、workflow、present、todo_write 等 8 个非 MCP 工具。
DSH插件 修复方向:决定要放在序列化那一层
两类问题的正确修法都在适配器边界:为每个协议各做一步规范化,而不是让工具作者写多份 schema。
- redacted 场景:在 durable replay 信封里加入 redacted 标记与 data,序列化时按原索引重新发出,并同步更新固定当前行为的测试。
- Gemini 场景:在为某个路由序列化请求之前做一次降级——内联
$ref/$defs(带深度上限防递归)、删除不支持的关键字、必要时替换成宽松 fallback。 - 更彻底的做法是改用允许清单姿态:只输出 Gemini 接受的关键字集合,否则逐条删键永远会碰到下一个键。
- 内置声明那一半只能由核心改,插件够不到;
function_response那一半(另一条相关报告)也在核心。 - 顺带一个便宜的改进:把响应体里已有的字段级原因(
Unknown name "$defs")透出来,而不是只显示泛化文案。
注意事项
- 别把「第一轮成功」当成没问题。块已经落盘,问题只是延后一轮。
- 别靠调低推理档位绕开。
reasoningEffort: none实测无效。 - 别在请求监听器里改 schema。那个请求是深冻结的,监听器只能读、不能替换。
- 别把插件当核心修复。它重写的是「告诉模型什么」,不是「提供商接受什么」。
- 报问题时带上字段级错误。泛化文案会把「协议不兼容」误导成「密钥或网络问题」。
还有一条通用经验:两条时间线的共同点是「持久化的东西在下一轮被重放」。只要报错位置每轮都一样、内容却与你这轮输入无关,就先去翻历史里最近新增了什么块或什么声明。
如果你的 400 里既没有块类型名、也没有 Unknown name,那属于第三类问题,先去查请求体格式与协议端点。
两条时间线的处置顺序也不同:redacted 只能换路由或等核心改持久化结构,Gemini schema 至少可以先用插件把 MCP 那一半压下去。相关排查见 DeepSeek Harness Gemini 工具调用 400:thought_signature 缺失怎么修。
常见问题
因为第一步的响应里带了 redacted_thinking 块,它被写进 DeepSeek Harness 的会话历史;第二步要回放这份历史时,适配器只认 text/thinking/tool_use 三种块,其余走 default 分支直接抛错,报「DeepSeek Messages does not support response block redacted_thinking」。
不行,这块不能丢:它的 data 是签名过的载荷,后续步骤需要原样回传,丢掉会让 DeepSeek Harness 的推理连续性在服务端静默变化——失败从「大声报错」变成「悄悄出错」,更糟。
是,DeepSeek Harness 在 Gemini 上遇到的 400 就是工具 schema 引起的:Gemini 的 functionDeclarations 只接受 OpenAPI 3.0 子集,遇到 $ref、$defs、additionalProperties 直接 400。社区实测无工具、扁平 schema、oneOf 都返回 200,这三类关键字全部 400。
因为 DeepSeek Harness 每轮都重发完整工具清单,只要其中一个工具带不支持的关键字,请求在推理前就被拒。所以失败与你的消息内容无关,表现为「每一条都 400」。
只能修一半,dsh-mcp-schema-normalizer 修不动 DeepSeek Harness 里的内置工具。它按 mcp__<server>__<tool> 选择器重写 MCP 工具 schema,社区实测 $ref/$defs/additionalProperties 计数从 745/169/1422 降到 0/0/12;剩下 12 个来自内置工具,插件够不到,而且 $schema、examples 仍会被拒。
相关术语
- redacted_thinking
- redacted_thinking 是 Anthropic Messages 的一种内容块,携带经过加密或脱敏的推理内容,客户端应原样回传而不解释。丢弃它会让后续请求的推理连续性发生变化。— https://github.com/deepseek-ai/deepseek-harness/discussions/11139
- UNSUPPORTED_CONTENT
- UNSUPPORTED_CONTENT 是 DeepSeek 适配器遇到无法表示的响应块时返回的错误码。它在翻译阶段抛出,早于任何流式监听器,因此外部无法通过过滤响应来修复。— https://github.com/deepseek-ai/deepseek-harness/discussions/11139
- functionDeclarations
- functionDeclarations 是 Gemini generateContent 请求里声明可调用函数的结构,其 parameters 只接受 OpenAPI 3.0 子集。出现 $ref、$defs、additionalProperties 等键会返回 Cannot find field。— https://github.com/deepseek-ai/deepseek-harness/discussions/10904
- inline-ref
- inline-ref 是把本地 $ref 指向的子 schema 直接内联展开的处理方式,用于让 Gemini 这类只认扁平结构的端点接受原本带引用的 schema。— https://github.com/deepseek-ai/deepseek-harness/discussions/10904
来源
- #11139 — dsh-llm-deepseek throws UNSUPPORTED_CONTENT on Anthropic redacted_thinking blocks· deepseek-ai(GitHub Discussions)
- #10904 — Gemini: every request fails with HTTP 400 when a tool schema contains $ref / $defs· deepseek-ai(GitHub Discussions)
- deepseek-ai/deepseek-harness(源码仓库)· GitHub