DSH plugin 本地模型 5 分钟断流?Body Timeout Error 排查与调优
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。 社区实跑记录:
- 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)。 - 改配置后错误改名不改时间:
settings.yaml里加上超时项后,失败原因从timeout变成terminated,但仍在 5 分钟整点——说明第一个闸门被绕过、第二个闸门在同一时刻接手。 - 与输出长度强相关:实测短回合总能过(<400 token 全部完成),长回合必挂(>2500 token 全部在 5:00 报
terminated);触发点集中在本地模型「思考很久才开始写」的阶段,典型如write工具要一次性生成整个文件内容(实测某回合写了 5.8 KB 文件,正是修好前必死的动作)。 - 代理侧可观测的证据:如果 dsh 与模型服务之间还有中继,会看到同一字节数的请求每 5 分钟重发一次(如
120 493 bytes反复出现)——相同大小意味着这是重试同一个请求,而不是对话在推进。Ollama 服务端日志则会出现cancel task,且上一条请求耗时接近 5 分钟。 - 后端差异:llama.cpp 每 30 秒发一次 SSE ping,靠这些数据流把 HTTP 层拖住,因此长提示(20 万 token、跑 30 分钟以上)也不会触发同一个
bodyTimeout;Ollama 默认不保活,就必中。
DeepSeek Harness 的双超时机制:undici bodyTimeout 与 harness 空闲看门狗
两个相互独立的 5 分钟闸门叠在一条链路上,这是本问题最容易被误判的地方。 源码核对结论(#4518,cd5ef81481 / 0.1.2-alpha.1):
- undici 层:
bodyTimeout默认 300000 毫秒(undici 文档)。dsh 源码里没有任何一处覆盖bodyTimeout(全树 grep 无命中),完全依赖第三方 SDK 的 fetch 映射。timeoutMs确实会被原样传下去(packages/llm/llm-pi-ai/src/adapter.ts:127的profileOptions()把它展开进SimpleStreamOptions),但 SDK 是否把它映射成 undici 的bodyTimeout、还是映射到别的计时器,取决于 SDK 自身——所以在 dsh 配置层调它,未必能解决 undici 这一关。 - harness 层:
DEFAULT_STREAM_IDLE_TIMEOUT_MS = 300_000在两个适配器里各定义一次:packages/llm/llm-pi-ai/src/config.ts:43与packages/llm/llm-deepseek/src/adapter.ts:138。它是一个挂在每次流读取上的看门狗(packages/util/timeout/src/index.ts:126-180的idleWatchdog):每次iterator.next()都重置计时器,零字节持续超过streamIdleTimeoutMs就以LLM_STREAM_IDLE_TIMEOUT中止回合。上限为 2147483647 毫秒。 - 配置键必须落在实际路由上:顶层
timeout:被忽略,键要写在你在模型选择器里选中的那条 provider 字典下(通常就是ollama)——这是「改了 settings.yaml 却没反应」的最常见原因。 - 改源码只对源码运行有效:用
npx dsh或npm i -g启动时进程加载的是已发布包,改 git checkout 里的adapter.ts不会有任何效果;要打补丁就得改编译产物,路径是node_modules/@earendil-works/pi-ai/dist/api/openai-completions.js。 - 还有两个附带杀手:Shell 插件默认 120 秒的命令超时从工具调用被创建时开始计时,慢速本地模型填
write参数就能把这点预算烧光;llama.cpp 用--parallel 1时,主 agent 与子 agent(或压缩)请求重叠,第二个请求被服务端静默挂起、零字节返回,300 秒的 headers timeout 与看门狗会同时开火(#4518)。
DSH plugin 两个超时一起调:本地模型长输出的正确配法
做法是「两键同调 + 去掉会把一次超时放大成五次的重试 + 后端保活」,改完必须开新会话。 社区验证过的组合:
- 两键写在同一条路由下,值按需给(1 小时示例):
llm-pi-ai:
providers:
ollama:
timeoutMs: 3600000 # 传给 pi-ai SDK
streamIdleTimeoutMs: 3600000 # harness 空闲看门狗(默认 300000)
-
校验与生效:
dsh web --dump-config查看组合树里该路由是否显示新值(有用户反馈配置未出现在输出里但实际已生效,可两边都确认);改完开一个新的会话,已在进行的回合保留旧边界。同一条路由上的模型选择也要对——配置的是ollama路由,就别在别的路由上跑。 -
去掉 TIMEOUT 重试:把
TIMEOUT从retryPolicy.retryableCodes里移除(或直接maxRetries: 0)。每次重试都会把整段提示词重发一遍,默认 5 次能把一次超时变成 30 分钟的停滞。 -
不改 node_modules 的路线:装社区 DSH插件
dsh-fetch-timeouts,它把headersTimeout与bodyTimeout默认都调到 30 分钟,可在 profile 的cordis.patch.yml覆写,全进程fetch生效、本地单用户部署影响可控,设置NODE_USE_ENV_PROXY时代理仍然有效;注意它不会替你调 harness 看门狗,timeoutMs与streamIdleTimeoutMs仍要一起调大(dsh-fetch-timeouts)。装插件可以用 DSH Plugin Hub 的「设置 → 插件市场」,也可以直接命令行:shdsh plugin --profile web add dsh-fetch-timeouts -
后端侧减负:调大 Ollama 的
keep_alive、调整OLLAMA_NUM_PARALLEL、降低单次请求体积(输出分段或增量流式),让响应体不要长时间零字节。 -
并发重叠单独处理:llama.cpp
--parallel 1的挂起问题靠排队解决——社区 DeepSeek插件dsh-llm-gate(#4995)让请求在 DSH 内部等槽位空闲再发出,从源头避开两个 300 秒计时器。 -
源码补丁路线(不推荐日常用):给
openai-completions.js加一个 undiciAgentdispatcher,把bodyTimeout/headersTimeout对齐到适配器的超时(#3157)。两个坑:undici 不是 dsh 的依赖,直接 import 会报ERR_MODULE_NOT_FOUND(要先npm install undici);补丁打在 node_modules 里,每次重装 dsh 都会丢。
本地模型与 provider 路由的其它配置(模型清单、上下文、图像能力)另见 本地模型配置;连接与鉴权类报错见 模型连接排查。
DSH plugin 排查注意事项
先看错误名定闸门、再认 5 分钟整点——大部分误判都出在「以为是模型太慢」上。 五条要点:
- 先看错误名,再决定调哪个闸门:
UND_ERR_BODY_TIMEOUT/Body Timeout Error指向 undici 层;timeout或回合以LLM_STREAM_IDLE_TIMEOUT结束指向 harness 看门狗;terminated只说明连接被掐,要结合时间点与代理日志判断。 - 5 分钟整点是最强线索:任何恰好卡在 5:00 的失败,优先怀疑这两个 300000 毫秒默认值,而不是模型太慢。
- 改完配置不重启会话等于没改:运行中的回合沿用旧边界。
- Shell 插件超时与模型超时是两回事:它在工具调用创建时开始计时,慢速本地模型要单独放宽。
- 补丁会随重装丢失:把 node_modules 补丁记进自己的部署清单,或在版本升级后重新确认插件版本是否已内置该能力。

来源:Discussion #4518、Discussion #3157、undici Client 文档、d3vmeh/dsh-fetch-timeouts。
常见问题
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)。
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 必须写在你在模型选择器里实际选中的那条 provider 路由下(字典键通常是 ollama),顶层的 timeout: 会被忽略。改完用 dsh web --dump-config 检查组合树里该路由是否显示新值(有用户反馈配置文件未显示但实际已生效),然后开一个新会话——已经在跑的回合保留旧边界。若你是用 npx 或 npm i -g 启动的,改源码不生效,进程加载的是已发布包(来源:Discussion #3157)。
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
来源
- deepseek-harness Discussion #4518:用本地模型报错 Body Timeout Error(含 UND_ERR_BODY_TIMEOUT 栈与两处 300 秒闸门的源码核对)· deepseek-ai(GitHub Discussions)
- deepseek-harness Discussion #3157:llm-pi-ai 本地 LLM 超时调大(含 streamIdleTimeoutMs 配置键与 undici dispatcher 社区补丁)· deepseek-ai(GitHub Discussions)
- undici 文档:Client 的 bodyTimeout / headersTimeout 默认值· undici
- d3vmeh/dsh-fetch-timeouts:把 headersTimeout / bodyTimeout 调大的社区插件· GitHub