Repository navigation
[llm-deepseek] 一个非法的流式工具入参会终止整轮(MALFORMED_RESPONSE,且不在默认重试白名单) #7581
Replies: 1 comment 1 reply
Additional data point: a recoverable bracket slip in streamed tool input ends the whole turn with non-retryable MALFORMED_RESPONSESummaryWhen a DeepSeek Messages stream finishes normally ( In our case the stream was complete and the concatenation was correct. The model had dropped one closing Adding to this thread (same failure family as #8976, #9402, #10195 and #10406). This comment adds one concrete, repairable shape (a structural bracket slip, not a string-escape issue) and a confirmation on the current ReproductionAny tool whose input has nested arrays of objects works. Synthetic, sanitized example of the defect we observed (the outer array is never closed): {"items":[{"id":"a","query":{"top":3}}}
Our real occurrence: 978-character input, 360 Recoverability of the same input:
Current behavior{"type":"turn/end","data":{"reason":{"kind":"error","error":{"message":"DeepSeek Messages stream: tool input is invalid JSON","code":"MALFORMED_RESPONSE"}}}}
Expected behaviorA model-side formatting slip in tool arguments should degrade to a recoverable, per-call failure, not a terminal turn error. Either:
Root cause (master @ 687f282)
Inconsistent with the rest of the pipeline:
Proposed fix
We are glad to contribute a patch with a unit test (an SSE fixture for the bracket slip, an escape slip and an unrepairable input) for options 1 and 2. CONTRIBUTING says external PRs are not accepted right now, so we can publish it on a fork branch for maintainers to pick up, if that helps. Workaround available todayOverride the retry table on the DeepSeek provider config. It replaces the whole list, so include the defaults: retryPolicy:
mode: normal
retryableCodes: [EMPTY_RESPONSE, RATE_LIMIT, SERVER, TIMEOUT, TRANSPORT, MALFORMED_RESPONSE]The failed attempt is not committed to history, so the step is resampled. The cost is one extra step of tokens, and the model still gets no feedback about the error. Environment
Related#8976 (retry-table patch on a fork), #9402, #10195, #10406, #9503 (survey of how other platforms repair), #9066 (pi-ai |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
包:
@deepseek-ai/dsh-llm-deepseek0.1.7-alpha.2(涉及dsh-llm、dsh-agent-loop、dsh-tools)路由:
deepseek-official(DeepSeek Messages)· 模型deepseek-flash环境: Windows 11,Node 24,web profile
摘要
只要有一次工具调用的流式
input_json_delta累积出来不是 JSON 对象,整轮就会以MALFORMED_RESPONSE: DeepSeek Messages stream: tool input is invalid JSON终止,该轮已经完成的每一步都被丢弃。我认为这个校验不该卡在流式层:同一个字段的另外两个消费方都是刻意宽容的。模型侧的一次格式打滑,应该退化成一次工具报错,而不是干掉整轮。
实际表现
模型在为
ask_user_question生成参数时,在 JSON 字符串值里写了一个未转义的 ASCII 双引号(跨两个 delta 下发):{"questions": [{"description": "相当于回到"什么都不设"的原厂行为。"}]}会话日志先记录
assistant/attempt(完整原始流已保留),紧接着是:{"type":"turn/end","data":{"turn":2,"reason":{"kind":"error","error":{"message":"DeepSeek Messages stream: tool input is invalid JSON","code":"MALFORMED_RESPONSE"}}}}这是模型侧的格式问题,不是链路损坏——流本身是完整的,而且该 block 在抛错之前已经以
block-end正常下发。它同样不可重试:dsh-llm的DEFAULT_RETRYABLE_CODES只有EMPTY_RESPONSE, RATE_LIMIT, SERVER, TIMEOUT, TRANSPORT,所以一份坏文档就要赔上整轮,连一次重试都没有。为什么说这个校验是「异类」
同一个字段的另外两个消费方都是有意为之的宽容:
dsh-agent-loop的parseArguments()(lib/index.js:534)——「解析模型参数,把非法 JSON 保留为文本,空输入映射为{}」。无法解析的参数会流到工具层,工具层抛出ToolArgsError(INVALID_ARGS),注册表再经toolErrorResult把它转成isError结果(dsh-toolslib/index.js:3344-3348)——这就是一次普通工具报错,模型下一步可以自行纠正。llm-deepseek的toolInput()(lib/index.js:1308),用于把 assistant 历史重新序列化——「Messages 无法表示的历史参数使用空 input;durable 内容保持不变」。无法解析或非对象的历史在线上降级为{},而会话记录保留原文。只有
message_stop处的校验(lib/index.js:1729-1738,即lib/types/translate.js/src/types/translate.ts区域)把它当成致命错误。同一处的第二种情况
同一个循环还会拒绝「能解析但不是对象」的 JSON:
object(parsed)会抛MALFORMED_RESPONSE: DeepSeek Messages expected a JSON object,例如整个工具入参是["a"]或"text"。结果同样是整轮终止。(max_tokens截断本来就是豁免的,这更让人觉得这条严格路径并非有意设计。)复现
确定性、可离线,不需要真实模型。用本地 HTTP 服务返回一段合成的 Messages SSE,然后驱动
DeepSeekAdapter.stream():在 0.1.7-alpha.2 上跑四个用例:
MALFORMED_RESPONSE: tool input is invalid JSONblock-end.arguments["a"])MALFORMED_RESPONSE: expected a JSON objectusage→finish)max_tokens截断的入参建议修复
把该校验改成 advisory,与上面两条宽容通路一致:
非法文档随后会原样到达工具层,产生
invalid arguments: "arguments" must be an object,模型用正确转义重发——也就是本来设计好的自我纠正路径。相关(可能是有意为之)
MALFORMED_RESPONSE不在DEFAULT_RETRYABLE_CODES中。由于这类失败属于 provider 输出问题而非传输问题(与被重试的EMPTY_RESPONSE同类),也许它也值得重试——不过上面的修复才是真正保证前进,而不是赌重新采样。我目前在本机的临时缓解:打上上述补丁,并把
MALFORMED_RESPONSE加进该路由的retryPolicy.retryableCodes。如果你们需要,我可以开 PR。All reactions