Bundle
dsh-missing-stop-heal
DeepSeek Harness plugin to heal third-party gateways that omit the stream terminal event (message_stop/done), which otherwise triggers repeated TRANSPORT retries in the dsh agent loop; scope is configurable per provider and per model.
- Source
- zzzyaar
- stars
- 1 stars
- License
- MIT
- Updated
- Updated yesterday
Readme
# dsh 第三方 API 缺失 `message_stop` 重试修复
> **DeepSeek Harness (dsh) 插件**:当第三方提供方网关**翻译漏发了终止事件 `message_stop`/`done`** 时,dsh 把**内容已完整到达**的回复误判为 `TRANSPORT` 传输错误,并对同一步最多做 **5 次指数退避重试**。本插件把"内容已到、缺终止信号"的流干净地结束,让回复正常提交、重试不再发生。
>
> *English:* A dsh plugin that heals third-party Anthropic-compatible gateways which close the stream without a terminal `message_stop`/`done` event, so the agent loop commits a complete reply instead of burning up to 5 backoff retries.
**目录**
1. [这是什么 / 现象](#一现象)
2. [根因(源码依据)](#二根因源码依据)
3. [修复方案](#三修复方案)
4. [安装](#四安装)
5. [配置](#五配置)
6. [设置页(GUI)](#六设置页gui)
7. [使用说明](#七使用说明)
8. [注意事项与边界](#八注意事项与边界)
9. [常见问题](#九常见问题)
10. [如何验证 / 诊断](#十如何验证--诊断)
11. [许可](#十一许可)
---
## 一、现象
- 在 dsh 里把某个网关配成 `anthropic-messages` 协议。
- 每次对话都报 **Anthropic stream ended before message_stop**,然后 `llm-retry` 重试 **5 次**(延迟约 467 → 1090 → 2031 → 3982 → 7282 ms,指数退避)。
- 但**最终输出是正确的、完整的**——内容早在重试触发前就到了,缺的只是终止信号。
从 `session.jsonl` 可见:
```json
{"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","retry":1,"maxRetries":5,...}}
```
## 二、根因(源码依据)
1. **缺失终止事件**由 pi-ai 适配器抛出:`@deepseek-ai/dsh-llm-pi-ai/lib/index.js` 的 `toStreamChunks()` 只在收到 `done`/`error` 事件时产出 `usage`+`finish` 并正常返回;若事件流**自然跑完却无终止事件**,则在末尾抛
```js
throw new LlmError("pi-ai event stream ended without done/error", "STREAM_CLOSED")
```
2. **错误码分类**:`classifyPiAiError()` 把含 `stream ended (before|without)` 的消息归为 `TRANSPORT`。
3. **触发重试**:`agent-loop` 的 `step()` 在流式收尾时,若 `finish` 为 `error`/`aborted`,就走 `agent/request-error` 瀑布;`@deepseek-ai/dsh-llm-retry` 的 `recover()` 见到 `TRANSPORT` 在 `retryableCodes` 内、且未达 `maxRetries`,就按 `initialDelayMs * 2**n` 退避并返回 `{kind:'retry'}`——于是同一步被重新完整请求一次。
4. **为何"最终内容仍正确"**:每次重试都重新拿到同样完整的内容;重试耗尽后该步抛错,界面展示的其实是最后那批流式渲染的 `assistant/chunk`。
5. **为何"干净结束"就够**:`BlockAssembler` 的 `finish` 取值是
```js
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`](./heal.mjs)(ESM,导出标准 Cordis 插件 `{ name, apply }`)。
## 四、安装
### 方式 A(推荐):作为 web profile bundle,用设置页配置
装进 web profile 后,插件在**进程级**生效,规则可在 GUI 设置页里调。
```bash
dsh plugin --profile web add dsh-missing-stop-heal
```
- 从 **dshmarket(插件市场)** 里搜 `missing-stop-heal` 一键安装也行。
- 装完**重启 dsh**(bundles 在启动时读取)。
- 用仓库源码本地开发:`dsh plugin --profile web add link:<本仓库绝对路径>`(link 方式 = 改仓库即生效,不用重装)。
- 想先只给某个会话用,见方式 B 的 preset 挂载(此时配置走行内 `config`,没有设置页)。
### 方式 B:作为 agent preset 的一行(旧式,会话级)
在某个 agent preset 的 `agent.cordis.yml` 末尾加一行(`name` 以 `.` 开头,loader 按 preset 目录相对路径解析):
```yaml
- id: missing-stop-heal
name: './heal.mjs'
config:
providers:
- xjhc
```
把 `heal.mjs`(或经目录联接指向本仓库)放到该 preset 目录下即可。
> 注意:`tool-cordis` / 动态 Cordis 插件是**进程内临时**的,重启即失效;本插件作为 preset / profile 组成行是**持久**的。
## 五、配置
配置可通过**行内 `config`**(方式 B / 补丁层)或**设置页**(方式 A,写 user 层)提供。两种写法,`rules` 为主:
```yaml
config:
rules:
- provider: xjhc # 必填:provider 标识(llm/stream options.provider)
models: ['*'] # 选填:缺省/空 = 全部模型;支持 * 通配,如 deepseek-v4*
enabled: true # 选填:缺省 true
```
- 旧写法 `config.providers: ['a','b']` 仍兼容=这些 provider 的全部模型;`providers` 与 `rules` 可混用,逐条累加。
- **`models` 语义**:留空或填 `*` = 该 provider 的**全部模型**;写多个用逗号/数组分隔;可用 `*` 通配前缀(如 `deepseek-v4*` 匹配 `deepseek-v4` 开头)。
- **什么都不配 = 不修复任何流**(插件对每个请求只提示一次,其余原样放行)。
- 规则按 `llm/stream` 事件的 `options.provider` + `options.model` 逐条匹配;命中才包裹该流。
## 六、设置页(GUI)
装成 bundle(方式 A)后,dsh web 的设置侧栏会多一个**「流重试修复」**独立入口(`settings.section`,类似「插件市场」的独立项):
- **provider**:下拉候选来自 host 读取的**真实已配置 provider 列表**(`ctx.llm.listProviders`),可点选/手填;
- **models**:选定 provider 后下拉列出该 provider 的**真实模型候选**(host `ctx.llm.listModels`),也可手填 + `*` 通配;
- 每行可勾选**启用**、可增删;点**「保存规则」**立即写入并**对下一条请求生效**(当前正在进行的请求不受影响);「恢复部署默认」清空 user 层、回到 patch/行配置里的默认。
> provider/模型目录(`catalog`)由 host 端写入设置文档,随 `llm/adapters-updated` 自动刷新;客户端直接从同一 settingsScope 快照读取,无需手动同步。
## 七、使用说明
1. 安装(方式 A 或 B)、必要时重启 dsh;
2. 配置生效范围:小任务常用网关(如 `xjhc`)用 `rules` 列出;**官方 API 不必列**,未被规则命中的请求一律放行;
3. 想随时改:用设置页勾选(方式 A),或编辑 patch/行配置后刷新即可;
4. 改宿主代码(`heal.mjs`)需重启 dsh;改客户端(`client/client.js`)浏览器 Ctrl+F5 即可。
## 八、注意事项与边界
- **只对命中规则的 provider/模型生效**;其余(含官方 API)完全不受影响。
- **只修复"内容已到 + 缺终止信号"这一种特征**:参数残缺的未闭合 tool-call、空响应、abort、真断网/限流/超时/鉴权失败**一律不修**,保持原报错/重试语义,不会掩盖真正的错误。
- **对下一条请求生效**:规则改动不追溯当前在途请求。
- **被修复的流没有 `finish` chunk、也没有 `replayState`**;`usage` 是否保留取决于错误是 in-band(有 usage)还是抛错(无 usage),通常 token 计量走估算。
- **需要 dsh web ≥ 0.1.0-rc.6**(`engines.dsh` 已声明);`@deepseek-ai/schemastery` 以 peerDependencies 声明,用于设置命名空间(缺失时自动退化为纯行配置,修复功能不受影响)。
- 该插件**不接触凭据、不发网络请求**,只包一层流;无安全面扩展。
- 若某次更新 dsh 后事件名/结构变化导致不再生效,检查 `llm/stream` 的 `options.provider/options.model` 字段(本仓库根因分析里有完整签名)。
## 九、常见问题
- **装完在设置里看不到「流重试修复」?** 确认用方式 A 装进了 web profile 并**重启**;设置页是独立 `settings.section`,不是藏进「插件」列表。
- **页面显示"设置服务不可用"?** 多为页面连上时命名空间尚未就绪/mirror 快照过期——Ctrl+F5 或重开标签;插件内置自动重试,稍等即可。
- **provider 下拉是空的?** 目录由 host 在 `llm/adapters-updated` 时写入;首次装完重启后即有,之后可手填。
- **为什么官方 API 也会被修?** 不会——未在 `rules` 里列出的 provider 一律放行。
- **会不会把真错误吞掉?** 不会——只匹配"内容已到 + 缺终止信号"特征,且要求未闭合 tool-call 参数为合法 JSON;其它错误照常抛。
## 十、如何验证 / 诊断
dsh 会话日志是 JSONL(默认在 `$DSH_HOME/sessions/.../session.jsonl`)。判断该会话是否发生过此问题:
```powershell
Select-String -Path <session.jsonl> -Pattern 'llm/retry|TRANSPORT|message_stop|STREAM_CLOSED|stream ended|request-error'
```
- **修复前**:能看到 `llm/retry`、`TRANSPORT`、`Anthropic stream ended before message_stop`;
- **修复后**:`assistant/message` 的 `sourceEventSeqs` 是**单次连续区间**(每步一次尝试即落库),没有 `llm/retry` 事件。
## 十一、许可
MIT,作者 zzzyaar。欢迎在此基础上扩展、提交 PR。
Install
dsh plugin --profile web add github:zzzyaar/dsh--API-message_stop-
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install dsh-missing-stop-heal from the hub
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.