Repository navigation
[Bug] empty tool-call id/name from Anthropic Messages gateway → "format v4 tool/result requires toolCallId matching its tool source" crash (fix provided) #10732
leo-liushuhong
started this conversation in
General
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
摘要
当 DeepSeek 模型通过第三方网关(
wdtrip-gateway)以 Anthropic Messages 协议(api: "anthropic-messages")对外提供、并由 harness 的 pi-ai 适配器(kind: "pi-ai",version: 2)消费时,网关偶尔会下发一个id与name均为空字符串的tool_use块(模型在 reasoning 里输出 DSML 标记、网关误判为工具调用所致)。llm-pi-ai适配器把这个空id/name原样透传成 harness 的tool-call块,于是:tool/call事件携带callId: "";unknown tool ""),ToolCallRecovery生成tool/result时toolCallId也为空字符串;assertV4ToolResultMessage拒绝toolCallId.length === 0,报错format v4 tool/result at seq N requires toolCallId matching its tool source,该tool/result事件无法写入会话,本轮强制终止。关键点:
llm-deepseek适配器早已对空工具身份做了防御(if (!content.id || !content.name) return malformed('empty tool identity')),但llm-pi-ai适配器缺失同一道防线——这是两个适配器之间的不一致,正是本次崩溃的根因。修复代码见 fork 分支:
leo-liushuhong/deepseek-harness@fix/reject-empty-tool-identity复现条件
wdtrip-gateway,api: "anthropic-messages"kind: "pi-ai",version: 2deepseek-v4-pro-huoshan[1m](响应模型deepseek-v4-pro-ga-260813)session-20fc753c-164e-48af-bf2a-b742355cec42表现症状(证据)
会话日志尾部(
seq112 → 119)展示了「占位名不崩、空 id 才崩」的精确对照:seq 112assistant/message 含tool-call id="call_8uwxpajwomekefw1vnwkofrj" name="tool_name"(占位名tool_name、真实非空 id);seq 113tool/callcallId="call_8uwxpajwomekefw1vnwkofrj" name="tool_name";seq 114tool/resulttoolCallId="call_8uwxpajwomekefw1vnwkofrj",content 为Error: unknown tool "tool_name"—— 非空 id 时正常走「未知工具」路径,不崩溃;seq 117assistant/message 含tool-call id="" name=""(空 id 与空 name);seq 118tool/callcallId="" name="";seq 119的tool/result缺失——写入被assertV4ToolResultMessage拒绝,没有step/end、没有turn/end,会话在此强制终止。错误信息在本会话中出现 41 次:
format v4 tool/result at seq … requires toolCallId matching its tool source。根因分析
崩溃链路(全部在官方
master上确认):packages/llm/llm-pi-ai/src/stream.ts的toolcall_end分支直接把event.toolCall.id/event.toolCall.name原样写入 harness 的tool-call块,id可以为空字符串。??拦不住空串:packages/llm/llm/src/assembler.ts里id: partial.toolCallId ?? brandString(\call-${index}`)用??,只拦undefined,**不拦空字符串""**(??与||的差异);且block-end路径直接返回partial.block`,完全绕过该兜底。packages/core/agent-loop/src/tool-calls.ts的callId = block.id、ToolCallRecovery的toolCallId = callId都直接继承空 id。packages/session/session-format-v3-to-v4/src/tool-role.ts的assertV4ToolResultMessage里toolCallId.length === 0抛SessionFormatError——报错文案「requires toolCallId matching its tool source」其实是由空 id 分支触发的,具有误导性。不一致点(关键):
packages/llm/llm-deepseek/src/translate.ts在tool_use分支已有if (!content.id || !content.name) return malformed('empty tool identity')(抛MALFORMED_RESPONSE,且该 code 不在默认重试清单内,不会被重试放大成循环)。llm-pi-ai缺失同一防线,导致同样类别的畸形响应一个被拒、一个崩溃。对话证据摘录(真实负载)
seq 117assistant 消息的真实内容块(JSON 视图):{"type": "assistant/message", "seq": 117, "data": {"message": {"role": "assistant", "content": [ {"type": "reasoning", "text": "I need to stop trying to write these tags. The environment intercepts my tool-call-like syntax. …"}, {"type": "tool-call", "id": "", "name": "", "arguments": "{}"} ]}}}紧随其后的
seq 118:{"type": "tool/call", "seq": 118, "data": {"callId": "", "name": ""}}可见网关把模型 reasoning 里的 DSML 标记误判成了
tool_use,且下发的id/name为空。修复方案
修复位置:
llm-pi-ai适配器(与llm-deepseek对齐),而非 assembler 层打补丁选择在
packages/llm/llm-pi-ai/src/stream.ts的toolcall_end分支加校验,理由:llm-deepseek已在同一语义位置(tool_use块落地处)拒绝空工具身份;把llm-pi-ai补齐到一致,消除两个适配器之间的行为分歧,而不是在 provider 中立层再加一道掩盖 provider 缺陷的兜底。id/name来自网关下发的 wire 载荷,属于「协议违规」;在适配器翻译边界以MALFORMED_RESPONSE拒绝,语义与 deepseek 一致,且该 code 不在默认重试清单(retryableCodes = [EMPTY_RESPONSE, RATE_LIMIT, SERVER, TIMEOUT, TRANSPORT]),不会把畸形响应放大成重试循环。assertV4ToolResultMessage或让 assembler 用||兜底,只会把空 id 换成call-${index}再往下传,name仍为空、依旧unknown tool ""——治标不治本。实现要点
在
toolcall_end分支 yieldblock-end之前:并同步更新
toStreamChunks的 JSDoc@returns(MALFORMED_RESPONSE抛出口),与既有的STREAM_CLOSED抛出口并列。修复后的验证
packages/llm/llm-pi-ai/tests/convert.spec.ts):新增it.each两例(空 id、空 name)断言抛MALFORMED_RESPONSE;整文件vitest run→ 85 例全部通过(新增 2 + 原有 83 无回归)。tsc --noEmit对llm-pi-ai包无类型错误(strict 模式、无any、无as unknown);oxlint对两个改动文件 0 warning / 0 error。90d8d6a7,pre-commit 门(staged lint、whitespace、vendor manifest)全部通过。附件
完整会话日志(
session-20fc753c-164e-48af-bf2a-b742355cec42,session.v4.jsonl.zstd约 90KB)包含崩溃全程与上述对照证据;如需,我可另行提供。All reactions