DSH plugin 本地模型 5 分钟断流?Body Timeout Error 排查与调优

故障排查发布于 2026-09-12作者: DeepSeek Plugin 插件市场
DeepSeek HarnessDSH pluginBody Timeout Error本地大模型streamIdleTimeoutMs
DeepSeek Harness 接本地模型约 5 分钟断流报 Body Timeout Error / UND_ERR_BODY_TIMEOUT?那是 Node 内置 undici 的 bodyTimeout 默认 300 秒,与 harness 自己的 300 秒空闲看门狗两个闸门,必须一起调大。

DeepSeek Harness 接本地模型跑长任务,约 5 分钟断流并报 TypeError: terminated / BodyTimeoutError / UND_ERR_BODY_TIMEOUT,根因是 Node 内置 fetch(undici)的 bodyTimeout 默认 300000 毫秒到点掐断连接——而 harness 自己还有一个同为 300 秒的流空闲看门狗,两个闸门必须同时调大。 Ollama 在生成 write 参数期间不发送任何字节,是本地模型最容易踩中的场景;把两个超时、重试策略与保活选项一起处理,才能把长输出跑完。

DeepSeek Harness 本地模型 5 分钟断流长什么样

报错时间点精确落在 5:00,形态有两种:先 timeout、调过一次配置后变成 terminated 社区实跑记录:

  1. undici 栈直出TypeError: terminated at Fetch.onAborted (node:internal/deps/undici/undici:13842:53),随后 name: "BodyTimeoutError"code: "UND_ERR_BODY_TIMEOUT"message: "Body Timeout Error"#4518)。
  2. 改配置后错误改名不改时间settings.yaml 里加上超时项后,失败原因从 timeout 变成 terminated,但仍在 5 分钟整点——说明第一个闸门被绕过、第二个闸门在同一时刻接手。
  3. 与输出长度强相关:实测短回合总能过(<400 token 全部完成),长回合必挂(>2500 token 全部在 5:00 报 terminated);触发点集中在本地模型「思考很久才开始写」的阶段,典型如 write 工具要一次性生成整个文件内容(实测某回合写了 5.8 KB 文件,正是修好前必死的动作)。
  4. 代理侧可观测的证据:如果 dsh 与模型服务之间还有中继,会看到同一字节数的请求每 5 分钟重发一次(如 120 493 bytes 反复出现)——相同大小意味着这是重试同一个请求,而不是对话在推进。Ollama 服务端日志则会出现 cancel task,且上一条请求耗时接近 5 分钟。
  5. 后端差异:llama.cpp 每 30 秒发一次 SSE ping,靠这些数据流把 HTTP 层拖住,因此长提示(20 万 token、跑 30 分钟以上)也不会触发同一个 bodyTimeout;Ollama 默认不保活,就必中。

DeepSeek Harness 的双超时机制:undici bodyTimeout 与 harness 空闲看门狗

两个相互独立的 5 分钟闸门叠在一条链路上,这是本问题最容易被误判的地方。 源码核对结论(#4518cd5ef81481 / 0.1.2-alpha.1):

  1. undici 层bodyTimeout 默认 300000 毫秒(undici 文档)。dsh 源码里没有任何一处覆盖 bodyTimeout(全树 grep 无命中),完全依赖第三方 SDK 的 fetch 映射。timeoutMs 确实会被原样传下去(packages/llm/llm-pi-ai/src/adapter.ts:127profileOptions() 把它展开进 SimpleStreamOptions),但 SDK 是否把它映射成 undici 的 bodyTimeout、还是映射到别的计时器,取决于 SDK 自身——所以在 dsh 配置层调它,未必能解决 undici 这一关。
  2. harness 层DEFAULT_STREAM_IDLE_TIMEOUT_MS = 300_000两个适配器里各定义一次packages/llm/llm-pi-ai/src/config.ts:43packages/llm/llm-deepseek/src/adapter.ts:138。它是一个挂在每次流读取上的看门狗(packages/util/timeout/src/index.ts:126-180idleWatchdog):每次 iterator.next() 都重置计时器,零字节持续超过 streamIdleTimeoutMs 就以 LLM_STREAM_IDLE_TIMEOUT 中止回合。上限为 2147483647 毫秒。
  3. 配置键必须落在实际路由上:顶层 timeout: 被忽略,键要写在你在模型选择器里选中的那条 provider 字典下(通常就是 ollama)——这是「改了 settings.yaml 却没反应」的最常见原因。
  4. 改源码只对源码运行有效:用 npx dshnpm i -g 启动时进程加载的是已发布包,改 git checkout 里的 adapter.ts 不会有任何效果;要打补丁就得改编译产物,路径是 node_modules/@earendil-works/pi-ai/dist/api/openai-completions.js
  5. 还有两个附带杀手:Shell 插件默认 120 秒的命令超时从工具调用被创建时开始计时,慢速本地模型填 write 参数就能把这点预算烧光;llama.cpp 用 --parallel 1 时,主 agent 与子 agent(或压缩)请求重叠,第二个请求被服务端静默挂起、零字节返回,300 秒的 headers timeout 与看门狗会同时开火(#4518)。

DSH plugin 两个超时一起调:本地模型长输出的正确配法

做法是「两键同调 + 去掉会把一次超时放大成五次的重试 + 后端保活」,改完必须开新会话。 社区验证过的组合:

  1. 两键写在同一条路由下,值按需给(1 小时示例):
yaml
llm-pi-ai:
  providers:
    ollama:
      timeoutMs: 3600000            # 传给 pi-ai SDK
      streamIdleTimeoutMs: 3600000  # harness 空闲看门狗(默认 300000)
  1. 校验与生效dsh web --dump-config 查看组合树里该路由是否显示新值(有用户反馈配置未出现在输出里但实际已生效,可两边都确认);改完开一个新的会话,已在进行的回合保留旧边界。同一条路由上的模型选择也要对——配置的是 ollama 路由,就别在别的路由上跑。

  2. 去掉 TIMEOUT 重试:把 TIMEOUTretryPolicy.retryableCodes 里移除(或直接 maxRetries: 0)。每次重试都会把整段提示词重发一遍,默认 5 次能把一次超时变成 30 分钟的停滞。

  3. 不改 node_modules 的路线:装社区 DSH插件 dsh-fetch-timeouts,它把 headersTimeoutbodyTimeout 默认都调到 30 分钟,可在 profile 的 cordis.patch.yml 覆写,全进程 fetch 生效、本地单用户部署影响可控,设置 NODE_USE_ENV_PROXY 时代理仍然有效;注意它不会替你调 harness 看门狗,timeoutMsstreamIdleTimeoutMs 仍要一起调大(dsh-fetch-timeouts)。装插件可以用 DSH Plugin Hub 的「设置 → 插件市场」,也可以直接命令行:

    sh
    dsh plugin --profile web add dsh-fetch-timeouts
    
  4. 后端侧减负:调大 Ollama 的 keep_alive、调整 OLLAMA_NUM_PARALLEL、降低单次请求体积(输出分段或增量流式),让响应体不要长时间零字节。

  5. 并发重叠单独处理:llama.cpp --parallel 1 的挂起问题靠排队解决——社区 DeepSeek插件 dsh-llm-gate#4995)让请求在 DSH 内部等槽位空闲再发出,从源头避开两个 300 秒计时器。

  6. 源码补丁路线(不推荐日常用):给 openai-completions.js 加一个 undici Agent dispatcher,把 bodyTimeout / headersTimeout 对齐到适配器的超时(#3157)。两个坑:undici 不是 dsh 的依赖,直接 import 会报 ERR_MODULE_NOT_FOUND(要先 npm install undici);补丁打在 node_modules 里,每次重装 dsh 都会丢。

本地模型与 provider 路由的其它配置(模型清单、上下文、图像能力)另见 本地模型配置;连接与鉴权类报错见 模型连接排查

DSH plugin 排查注意事项

先看错误名定闸门、再认 5 分钟整点——大部分误判都出在「以为是模型太慢」上。 五条要点:

  1. 先看错误名,再决定调哪个闸门UND_ERR_BODY_TIMEOUT / Body Timeout Error 指向 undici 层;timeout 或回合以 LLM_STREAM_IDLE_TIMEOUT 结束指向 harness 看门狗;terminated 只说明连接被掐,要结合时间点与代理日志判断。
  2. 5 分钟整点是最强线索:任何恰好卡在 5:00 的失败,优先怀疑这两个 300000 毫秒默认值,而不是模型太慢。
  3. 改完配置不重启会话等于没改:运行中的回合沿用旧边界。
  4. Shell 插件超时与模型超时是两回事:它在工具调用创建时开始计时,慢速本地模型要单独放宽。
  5. 补丁会随重装丢失:把 node_modules 补丁记进自己的部署清单,或在版本升级后重新确认插件版本是否已内置该能力。
DSH Plugin Hub 插件市场:查找并安装本地模型超时修复插件

来源:Discussion #4518Discussion #3157undici Client 文档d3vmeh/dsh-fetch-timeouts

常见问题

DeepSeek Harness 用本地模型约 5 分钟后报 Body Timeout Error、code UND_ERR_BODY_TIMEOUT 到底是什么原因?

DSH plugin 接本地模型约 5 分钟断流并报 Body Timeout Error / UND_ERR_BODY_TIMEOUT,根因是 Node 内置 fetch 客户端 undici 的 bodyTimeout 默认 300000 毫秒(5 分钟)到期掐断连接,不是模型或 Ollama 的问题。本地模型在长思考或生成 write 参数期间响应体零字节、也没有 keepalive,bodyTimeout 就按 5 分钟整点触发,报错栈落在 undici 的 Fetch.onAborted(来源:Discussion #4518)。

为什么把 timeoutMs 调到 1 小时,DeepSeek Harness 仍会在 5 分钟断流,甚至把错误从 timeout 变成 terminated?

DeepSeek Harness 的本地模型链路里叠着两个同为 300 秒的闸门:undici 的 bodyTimeout,以及 harness 自己的流空闲看门狗(DEFAULT_STREAM_IDLE_TIMEOUT_MS = 300_000,定义在 llm-pi-ai/src/config.ts:43 与 llm-deepseek/src/adapter.ts:138)。只调一个,另一个会在同一时刻接手,所以你看到的是错误名变了、时间点没变;两个键必须一起调大(来源:Discussion #4518)。

在 DeepSeek Harness 里,timeoutMs 和 streamIdleTimeoutMs 配置写在哪个位置才会生效?

DeepSeek Harness 的 timeoutMs 与 streamIdleTimeoutMs 必须写在你在模型选择器里实际选中的那条 provider 路由下(字典键通常是 ollama),顶层的 timeout: 会被忽略。改完用 dsh web --dump-config 检查组合树里该路由是否显示新值(有用户反馈配置文件未显示但实际已生效),然后开一个新会话——已经在跑的回合保留旧边界。若你是用 npx 或 npm i -g 启动的,改源码不生效,进程加载的是已发布包(来源:Discussion #3157)。

有没有不用改 node_modules 就能让 DeepSeek Harness 本地模型不再 5 分钟断流的办法?

DSH plugin 生态里有 dsh-fetch-timeouts 社区插件可以不改 node_modules 解决这个问题:它把 headersTimeout 与 bodyTimeout 默认都调到 30 分钟、可在 profile 的 cordis.patch.yml 覆写。安装用 dsh plugin --profile web add dsh-fetch-timeouts;同时仍要把 ollama 路由上的 timeoutMs 与 streamIdleTimeoutMs 一起调大,把 TIMEOUT 从 retryPolicy.retryableCodes 里移除、调大 Ollama 的 keep_alive、降低单次请求体积(来源:d3vmeh/dsh-fetch-timeouts)。

相关术语

undici bodyTimeout
undici bodyTimeout 是 Node.js 内置 fetch 实现 undici 的响应体超时,默认 300000 毫秒;从请求发出到收到完整响应体之间没有任何数据流动即触发,报 UND_ERR_BODY_TIMEOUT / Body Timeout Error。https://undici.nodejs.org/#/docs/api/Client
streamIdleTimeoutMs
streamIdleTimeoutMs 是 DeepSeek Harness 自己的流空闲看门狗阈值,默认 300000 毫秒,写在所选 provider 路由下;每次读取流片段都会重置计时,零字节超过该值即中止回合并标记 LLM_STREAM_IDLE_TIMEOUT,上限 2147483647。https://github.com/deepseek-ai/deepseek-harness/discussions/4518
keep_alive(Ollama 保活)
keep_alive 是 Ollama 服务端为模型设置的驻留时长参数,决定一次请求结束后模型在内存里保留多久;它不改变响应体是否有字节流动——Ollama 默认在生成工具调用参数时不发送任何字节,而 llama.cpp 每 30 秒发一次 SSE ping,这解释了为什么同样配置下两种后端的断流表现不同。https://github.com/deepseek-ai/deepseek-harness/discussions/4518

来源