Back to home@zzzyaar

dsh--API-message_stop-

deepseek harness 使用第三方提供者的api时的缺失message_stop导致的重复重试

Stars
1
Language
JavaScript
Created
Sep 6, 2026
Updated
Sep 7, 2026
GitHub repo

Introduction

dsh 第三方 API 缺失 message_stop 重试修复

用于 DeepSeek Harness (dsh):当使用第三方提供方的 API时,网关翻译漏发了终止事件 message_stop/done,导致 dsh 每次都把内容已完整到达的回复误判为 TRANSPORT 传输错误,并对同一步做最多 5 次指数退避重试(每次重新完整问一遍),白白烧掉数倍的算力与时间。

本仓库是一次完整的根因分析 + 最小修复:一个 Cordis 插件,在 dsh 的 llm/stream 瀑布上把"内容已到、缺终止信号"的流干净地结束,从而让回复正常提交、重试不再发生。


一、现象

  • 在 dsh 里把某个网关配成 anthropic-messages 协议。
  • 每次对话都报 Anthropic stream ended before message_stop,然后 llm-retry 重试 5 次(延迟约 467 → 1090 → 2031 → 3982 → 7282 ms,指数退避)。
  • 最终输出是正确的、完整的——内容早在重试触发前就到了,缺的只是终止信号。

session.jsonl 可见(诊断方法见下文第五节):

{"type":"assistant/chunk", "...":"chunk":{"type":"finish","reason":{"kind":"error","failure":{"message":"Anthropic stream ended before message_stop","code":"TRANSPORT"}}}}
{"type":"llm/retry","...":"data":{"provider":"xjhc","mode":"normal","policyKey":"[\"normal\",5,[\"EMPTY_RESPONSE\",\"RATE_LIMIT\",\"SERVER\",\"TIMEOUT\",\"TRANSPORT\"],500,10000,0.1]","retry":1,"maxRetries":5,...}}

二、根因(源码依据)

以 dsh 一份实际安装为例,链条如下:

  1. 缺失终止事件由 pi-ai 适配器抛出:@deepseek-ai/dsh-llm-pi-ai/lib/index.jstoStreamChunks() 只在收到 done/error 事件时产出 usage+finish 并正常返回;若事件流自然跑完却无终止事件,则在末尾抛

    throw new LlmError("pi-ai event stream ended without done/error", "STREAM_CLOSED")
    
  2. 错误码分类classifyPiAiError() 把含 stream ended (before|without) 的消息归为 TRANSPORT

  3. 触发重试agent-loopstep() 在流式收尾时,若 finisherror/aborted,就走 agent/request-error 瀑布;@deepseek-ai/dsh-llm-retryrecover() 见到 TRANSPORTretryableCodes 内、且未达 maxRetries,就按 initialDelayMs * 2**n 退避并返回 {kind:'retry'}——于是同一步被重新完整请求一次。

  4. 为何"最终内容仍正确":每次重试都重新拿到同样完整的内容;重试耗尽后该步抛错,界面展示的其实是最后那批流式渲染的 assistant/chunk

  5. 为何"干净结束"就够BlockAssemblerfinish 取值是

    get finish() { return this._finish ?? { kind: 'stop' } }
    

    流没有 finish chunk 也算正常 stop。所以只要把"缺终止信号"这个错误吞掉、让流干净结束,回复就会被正常提交,request-error/llm-retry 根本不会发生。

三、修复方案

监听 dsh host 端公开的 llm/stream waterfall("around every streaming model call"),把下游流包一层:

  • 内容 chunk(block-start / text、reasoning、tool-call 的 delta & end / usage)原样透传
  • 只有当错误为**"缺终止信号"特征**(code === 'STREAM_CLOSED',或失败信息含 stream ended (before|without) / message_stop / done/error),且已收到内容、且所有未闭合的 tool-call 块参数已是合法 JSON 时,才丢弃该错误、干净结束;
  • 其余一切情况(真断网、限流、超时、鉴权、abort、空响应、参数残缺的 tool call)原样放行/照常抛错,重试行为不变。

这样语义等价于"把这个网关变得宽容:内容完整即算完"。

代码见 heal.mjs(单一文件、无依赖,导出标准 Cordis 插件)。

四、使用

方式 A:作为 agent preset 的一行

在某个 agent preset 的 agent.cordis.yml 末尾加一行(name. 开头,loader 会按 preset 目录相对路径解析):

- id: missing-stop-heal
  name: './heal.mjs'
  config:
    providers:
      - xjhc

heal.mjs 放到该 preset 目录下即可。要修别的网关,改 providers 列表。

方式 B:作为 host 层插件

同理由 host 组成挂载 heal.mjs,效果一致(llm/stream 是 host 级瀑布)。默认只对 providers 里的网关生效。

注意:tool-cordis/动态 Cordis 插件进程内临时的,重启即失效;本插件作为 preset/host 组成行是持久的,重启后依然生效。

五、如何确认问题 / 验证修复

dsh 会话日志是 JSONL(默认在 $DSH_HOME/sessions/.../session.jsonl)。可用下面命令判断某次会话是否发生过该问题:

Select-String -Path <session.jsonl> -Pattern 'llm/retry|TRANSPORT|message_stop|STREAM_CLOSED|stream ended|request-error'
  • 修复前:能看到 llm/retryTRANSPORTAnthropic stream ended before message_stop
  • 修复后assistant/messagesourceEventSeqs单次连续区间(每步一次尝试即落库),没有 llm/retry 事件。

六、限制与边界

  • 仅对 providers 列表内的网关生效,其余不受影响。
  • 只修复"内容已到 + 缺终止信号"这一种特征;参数残缺的未闭合 tool-call、空响应、abort 一律不修,保持原报错/重试语义。
  • 被修复的流没有 finish chunk、也没有 replayStateusage 是否保留取决于错误是 in-band(有 usage)还是抛错(无 usage)。通常 token 计量会走估算。
  • 本仓库是该问题的最小可复现修复;更完整的形态是把它做成"设置页里列出 provider 勾选"(GUI 配置),见上文根因与 heal.mjs 中的配置字段。

七、许可

MIT,作者 zzzyaar。欢迎在此基础上扩展、提交 PR。