dsh-stream-timeout-fix:放宽 undici 默认超时,避免长静默 LLM 流被误报为 TRANSPORT

前言

在 DeepSeek Harness(DSH)里接第三方或自建的 OpenAI 兼容端点时,流式 SSE 有时会在内容已经输出一段之后,中间静默数分钟,再在本地约 304–305 秒处失败:报错 TypeError: terminated,cause 为 BodyTimeoutError(UND_ERR_BODY_TIMEOUT),并被 DSH 归类为 TRANSPORT。平台侧日志却常显示客户端主动断开(例如 LiteLLM / nginx 的 499),上游仍继续生成并计费——看起来像「网络或对端问题」,实际是 Node 内置 fetch 所依赖的 undici 客户端时钟在起作用。

下面介绍维护者 SundaeYAGO 发布的宿主侧插件 dsh-stream-timeout-fix:在进程级替换 undici 全局 dispatcher 的 bodyTimeout / headersTimeout,消除上述误判;并说明安装、配置,以及与 DSH 自身 streamIdleTimeoutMs 的关系。

这是什么

dsh-stream-timeout-fix 是一个极小的 DeepSeek Harness 宿主插件(仓库约 120 行),分类上属于模型推理相关能力。它不包含重试逻辑,也不改 provider、鉴权或路由;只做一件事:让 DSH 整条 LLM 调用链(dsh-llm-pi-ai → pi-ai → OpenAI SDK v6 → globalThis.fetch)使用的全局 undici dispatcher 不再沿用默认的 300 秒 body/headers 超时。

适用对象主要是:响应时间长、且偶尔在流中途长时间无字节的第三方 / 自建 OpenAI 兼容端点(私有化部署、LiteLLM 类网关、自托管 vLLM 等)。文档说明:若对接行为稳定的大型云 API,通常不需要此插件。

维护者声明插件出于自用目的开源,不保证持续维护;尚未发布到 npm registry。插件以当前 DSH 进程权限运行,安装前应自行阅读源码与 MIT 许可证。

现象与根因(简要)

典型失败序列:

  1. SSE 已建立,推理 token、部分答案或工具调用增量已在流动;
  2. 中间数分钟没有任何 body 字节(上游可能仍在生成);
  3. 本地在静默约 304–305 秒后抛出 TypeError: terminated,DSH 侧常显示为传输类失败。

undici 与 Node 内置 fetch 相关的两个默认时钟(资料原文):

时钟 默认值 作用 触发时报错
headersTimeout 300_000 ms 等待第一个响应字节 HeadersTimeoutError(UND_ERR_HEADERS_TIMEOUT)
bodyTimeout 300_000 ms 相邻 body 字节之间的空闲(每收到一字节重新计时) BodyTimeoutError(UND_ERR_BODY_TIMEOUT),消息常为 terminated

bodyTimeout 按字节重置,因此「慢但持续有数据」的长流一般不受影响;问题出在相邻分片间隔超过 300 秒的 stall。DSH 失败分类器匹配 /terminated/ 并映射为 TRANSPORT,容易把客户端时钟误读成对端或网络问题。

核心功能

插件在加载时执行进程级替换(逻辑等价于):

setGlobalDispatcher(new Agent({ bodyTimeout: 1_800_000, headersTimeout: 600_000 }))

默认含义(可在配置中覆盖):

  • bodyTimeout:30 分钟(1_800_000 ms),故意仍为有限值,以便回收真死连接;
  • headersTimeout:10 分钟(600_000 ms)。

一次替换覆盖该宿主进程内所有走 globalThis.fetch 的 provider。流层看门狗(如 pi-ai 的 streamIdleTimeoutMs)与 DSH 重试插件等仍独立生效;本插件只消除 undici 默认 300 秒与业务层「空闲」判断之间的口径冲突。

重要边界:若静默仍在 恰好 300 秒 被 pi-ai stream idle timeout after 300000ms(code: TIMEOUT)切断,那是 DSH 路由上的 streamIdleTimeoutMs 默认值,不是 undici——需另行调大(见下文「与 streamIdleTimeoutMs 一起调整」)。网关或服务端自身的空闲断连,本插件也无法修改。

安装与启用

环境要求:Node ≥ 22.19.0(与 package.json 中 engines 及 undici 依赖一致)。

包尚未在 npm 发布,npm install dsh-stream-timeout-fix 与 dsh plugin … add dsh-stream-timeout-fix 目前会 404。资料给出的可用方式如下。

若你的 DSH CLI 支持与其他插件相同的 git 规格,可直接:

dsh plugin --profile <profile-name> add github:SundaeYAGO/dsh-stream-timeout-fix

上述命令可代替手动改 profile 的多步操作;插件自带的 cordis.patch.yml 会挂载插件行,无需手改 profile patch。

手动方式(在 profile 目录 ~/.dsh/profiles/<profile-name>/):

  1. 在 package.json 的 dependencies 中加入 "dsh-stream-timeout-fix": "^1.0.0"(或指向本地 link 的路径,按你实际获取包的方式填写);
  2. 将 "dsh-stream-timeout-fix" 追加到 dsh.profile.bundles;
  3. 在 profile 目录执行安装/链接(如 pnpm install),然后重启 DSH(或按你所用版本是否支持 live bundle 应用)。

首次 npm 发布之后,官方文档给出的 CLI 安装方式为:

dsh plugin --profile <profile-name> add dsh-stream-timeout-fix

配置

配置项 默认值 设置位置
bodyTimeoutMs 1800000 patch 行 config,或环境变量 DSH_STREAM_TIMEOUT_BODY_MS
headersTimeoutMs 600000 patch 行 config,或环境变量 DSH_STREAM_TIMEOUT_HEADERS_MS

设为 0 表示关闭对应时钟;文档不建议全局关闭,否则会失去回收死 socket 的手段。

验证是否生效

  1. loader 中对应条目为 enabled/active(include:stream-timeout-fix);
  2. 宿主日志出现类似:[stream-timeout-fix] global dispatcher replaced: bodyTimeout=1800000ms, headersTimeout=600000ms ...;
  3. 运行时可通过 globalThis.__streamTimeoutFix 查看装上的 Agent 与生效超时。

本地可运行仓库测试与复现脚本(在插件源码目录):

npm test
node scripts/reproduce-300s.mjs            # 默认 fetch:复现约 304 秒被杀
node scripts/reproduce-300s.mjs --plugin   # 应用插件逻辑后:挺过 300 秒静默

若长静默后仍失败,可对照间隔:304–305 秒多指向栈内 undici 默认时钟;恰好 300 秒且含 pi-ai stream idle timeout 指向 DSH 看门狗;其他数值更可能是服务端或网关空闲限制。

与 streamIdleTimeoutMs 一起调整

仅安装本插件不会自动提高 DSH 默认可容忍的流空闲时间。路由未设置时,streamIdleTimeoutMs 默认为 300000,仍会在 300 秒处切断。资料建议在 profile patch 中为对应 provider 显式加大,且必须大于你希望挺过的最长 stall:

providers:
  your-provider:
    streamIdleTimeoutMs: 1800000   # 30 分钟

pi-ai 的计时从请求下发开始,包含首 token 前的等待;大 prompt 预填充与流中途停顿共用同一份预算。

适用场景与注意

更适合:自建或第三方 OpenAI 兼容端点,流式生成中偶发长时间无分片,且错误指纹为 terminated + UND_ERR_BODY_TIMEOUT、静默约 304–305 秒的场景。

不必强求:对接稳定云 API、且从未出现上述指纹时。

进程级影响:宿主内所有 fetch 均改用你配置的超时,这是设计目标,但影响面是整个进程。

费用:插件本身不发网络请求;修复前可能在 ~304 秒被掐断却仍被上游计费;修复后请求可能等待更久,收到更多 token 也会按 provider 规则计费。若叠加无上限重试, stall 重复出现会放大支出。

与本插件无关的能力:不含重试(可与 @deepseek-ai/dsh-llm-retry 等在 profile 中分别配置);不解决服务端 SSE 心跳或网关 idle 限制;仅适用于 Node 客户端路径。

安装前请阅读仓库 README 中的「验证状态」:作者用本地复现与测试证明机制,并记录了 2026-10-13 至 2026-10-18 期间目标失败类未再出现等观测;一次「分片间隔 >300 秒且端到端成功」的验收在文档中仍列为未完成。审计会话分片间隔可使用:

node scripts/audit-session-gaps.mjs ~/.dsh/sessions --min-gap-s=250

DSH 生态强调「一切皆插件」;社区目录 skillhub.cn 上的插件页 为独立站点,与 DeepSeek / 幻方无官方从属关系。

结尾

dsh-stream-timeout-fix 针对 Node undici 在 LLM SSE 长静默上的 300 秒 bodyTimeout,用可配置的进程级 dispatcher 替换默认值,并把误报的 TRANSPORT 与真实的 DSH TIMEOUT、网关限制区分开。使用前请同时评估 streamIdleTimeoutMs 与计费风险。

  • 目录页:https://www.skillhub.cn/plugins/SundaeYAGO/dsh-stream-timeout-fix
  • 源码与完整说明:https://github.com/SundaeYAGO/dsh-stream-timeout-fix
羽毛球分组比赛记分
小程序二维码

欢迎使用《羽毛球分组比赛记分》微信小程序

小夜