Skip to content
dsh.fish
Bundle

dsh-undici-timeout-fix

Removes undici's 300s body timeout so long local-model prefills are not cut off mid-stream.

Source
lilinhua0413
License
MIT
Updated
Updated 6 days ago

Readme

# dsh-undici-timeout-fix

Fixes DSH cutting off local-model requests after exactly five minutes of silence.

English | [简体中文](#简体中文)

## What this fixes

When a local model spends more than five minutes on prompt prefill, the request dies with
`terminated` and the model server logs `Client disconnected`.

The cause is **not** a DSH setting. Node's built-in `fetch` runs on undici, whose connection-level
`bodyTimeout` defaults to `300,000ms`. That timer is an *idle* timer: every body chunk resets it.
A model that is busy prefilling sends no bytes at all, so undici destroys the socket the moment
the five minutes run out.

`streamIdleTimeoutMs` does not help here. It configures DSH's own watchdog, which is a separate
timer and — before this plugin — fired at the same 300,000ms mark. Raising it just lets undici
win the race, which is why the error changes from `stream idle timeout after 300000ms` to the
bare `terminated`.

## How it works

DSH reaches the network through pi-ai calling the openai SDK, and that SDK uses the **global**
`fetch` (`Shims.getDefaultFetch()`). Replacing undici's global dispatcher therefore covers model
requests without patching DSH itself:

```js
import { Agent, setGlobalDispatcher } from 'undici'

setGlobalDispatcher(new Agent({ headersTimeout: 0, bodyTimeout: 0 }))
```

`0` **disables** undici's timers rather than setting a duration.

## Install

### From a release tarball (no registry needed)

Download `dsh-undici-timeout-fix-1.1.0.tgz` from the repository's Releases page, then:

```sh
dsh plugin --profile <your-profile> add /path/to/dsh-undici-timeout-fix-1.1.0.tgz
```

The one dependency (`undici`) is still resolved from a registry you can reach, so a local npm
mirror or a proxy works fine.

### From npm

```sh
dsh plugin --profile <your-profile> add dsh-undici-timeout-fix
```

Or through the plugin manager in the DSH UI: add the package name `dsh-undici-timeout-fix` and
point the install source at a registry that serves it.

Installing by hand? Add the row to your profile's `cordis.patch.yml`:

```yaml
- insert:
    - id: undici-timeout-fix
      name: 'dsh-undici-timeout-fix'
```

### Restart required

**Fully quit DSH and start it again.** The dispatcher is installed while the plugin loads, so a
running process keeps whatever it already set. On macOS use `Cmd+Q` — closing the window is not
enough, because the process survives.

Skipping the restart is the most common reason for "it still fails".

## Also configure `streamIdleTimeoutMs`

This plugin removes the transport ceiling, but it also removes DSH's own backstop. With no upper
bound at all, a genuinely wedged connection can hang forever. Keep a large but finite watchdog on
the provider that talks to your local model:

```yaml
- id: llm-pi-ai
  name: "@deepseek-ai/dsh-llm-pi-ai"
  config:
    providers:
      lmstudio1:
        displayName: lmstudio
        apiKeyEnv: LMSTUDIO1_API_KEY
        api: openai-completions
        baseURL: http://127.0.0.1:1234/v1/
        streamIdleTimeoutMs: 1800000   # 30 minutes
        models:
          - id: qwen3.5-122b-a10b
            name: qwen3.5-122b-a10b
```

Do this for **every** provider you point at a local model. A provider without the key falls back
to the 300,000ms default.

You can also prefer a finite transport timeout over disabling it entirely:

```js
setGlobalDispatcher(new Agent({ headersTimeout: 3_600_000, bodyTimeout: 3_600_000 }))
```

## Verify

Send a request whose prefill exceeds five minutes. If it completes, the fix is live.

You can also confirm which timer fired by reading the error:

| Error | Meaning |
|---|---|
| `terminated` | undici's transport timer — this plugin's concern |
| `pi-ai stream idle timeout after xxxms` | DSH's watchdog — raise `streamIdleTimeoutMs` |

## Do not set proxy environment variables

`HTTP_PROXY`, `HTTPS_PROXY`, and `ALL_PROXY` make `dsh-http-proxy` install its own Agent during
boot, which carries the same five-minute limit and replaces this plugin's. DSH installs the proxy
before plugins load, so this plugin always wins *only when no proxy is configured*.

## Platform status

| Platform | Status |
|---|---|
| macOS (Darwin 24, Node 24.21.0 / undici 7.29.1 built in) | Tested, working |
| Windows | Untested — please open an issue with your result |

The code has no platform-specific paths, file writes, or process spawning, and depends only on
`undici`, so it should behave identically on Windows. It has not been verified there yet.

## Notes

- This bundle declares no `peerDependencies`, so DSH's plugin version-compatibility check does not
  block installation.
- pnpm's `minimumReleaseAge` policy (24 hours by default) blocks freshly published versions. If
  installation reports an older version, install an exact one: `dsh plugin add dsh-undici-timeout-fix@1.1.0`.

## License

MIT

---

## 简体中文

修复 DSH 使用本地大模型时,静默满五分钟后请求被切断的问题。

### 解决的问题

本地模型做 prompt 预处理(prefill)超过五分钟时,请求会以 `terminated` 失败,模型服务端日志出现
`Client disconnected`。

原因**不在 DSH 配置**。Node 内置 `fetch` 基于 undici,其连接级 `bodyTimeout` 默认 `300,000ms`。
该计时器统计的是**空闲时长**:每收到一个 body 数据块就重置。正在 prefill 的模型一个字节都不发,
于是五分钟一到 undici 直接销毁 socket。

`streamIdleTimeoutMs` 解决不了这个问题。它配置的是 DSH 自己的看门狗,是另一个计时器——而且在装本插件
之前,它同样在 300,000ms 处触发。把它调大只是让 undici 抢先触发,所以报错会从
`stream idle timeout after 300000ms` 变成一个光秃秃的 `terminated`。

### 原理

DSH 经由 pi-ai 调用 openai SDK 发起请求,而该 SDK 使用**全局** `fetch`(`Shims.getDefaultFetch()`)。
因此替换 undici 的全局 dispatcher 就能覆盖模型请求,无需改动 DSH 本体:

```js
import { Agent, setGlobalDispatcher } from 'undici'

setGlobalDispatcher(new Agent({ headersTimeout: 0, bodyTimeout: 0 }))
```

`0` 表示**禁用** undici 的计时器,而不是某个时长。

### 安装

### 从 Release 附件安装(无需registry)

从仓库的 Releases 页面下载 `dsh-undici-timeout-fix-1.1.0.tgz`,然后:

```sh
dsh plugin --profile <你的 profile> add /path/to/dsh-undici-timeout-fix-1.1.0.tgz
```

唯一的依赖(`undici`)仍会从你能访问到的 registry 解析,所以内网 npm 镜像或代理都可以。

### 从 npm 安装

```sh
dsh plugin --profile <你的 profile> add dsh-undici-timeout-fix
```

也可以在 DSH 界面里安装:包名填 `dsh-undici-timeout-fix`,安装源指向提供该包的 registry。

手动安装时,在 profile 的 `cordis.patch.yml` 里加一行:

```yaml
- insert:
    - id: undici-timeout-fix
      name: 'dsh-undici-timeout-fix'
```

### 必须重启

**完全退出 DSH 后重新启动。** dispatcher 在插件加载时安装,已运行的进程会继续沿用它之前的状态。
macOS 上请用 `Cmd+Q`——只关窗口不够,进程仍然存活。

漏掉重启是"怎么还是不行"最常见的原因。

### 同时配置 `streamIdleTimeoutMs`

本插件移除了传输层的上限,同时也移除了 DSH 自身的兜底。完全没有上限时,真正卡死的连接可能永远挂着。
请为每个指向本地模型的 provider 配一个足够大但有限的看门狗:

```yaml
- id: llm-pi-ai
  name: "@deepseek-ai/dsh-llm-pi-ai"
  config:
    providers:
      lmstudio1:
        displayName: lmstudio
        apiKeyEnv: LMSTUDIO1_API_KEY
        api: openai-completions
        baseURL: http://127.0.0.1:1234/v1/
        streamIdleTimeoutMs: 1800000   # 30 分钟
        models:
          - id: qwen3.5-122b-a10b
            name: qwen3.5-122b-a10b
```

**每个**指向本地模型的 provider 都要配。漏配的会退回默认的 300,000ms。

如果你更想设一个有限值而不是完全禁用:

```js
setGlobalDispatcher(new Agent({ headersTimeout: 3_600_000, bodyTimeout: 3_600_000 }))
```

### 验证

发一个 prefill 超过五分钟的请求。正常完成即表示生效。

也可以通过报错文本判断是哪个计时器触发的:

| 报错 | 含义 |
|---|---|
| `terminated` | undici 传输层计时器——本插件负责的范围 |
| `pi-ai stream idle timeout after xxxms` | DSH 看门狗——调大 `streamIdleTimeoutMs` |

### 不要设置代理环境变量

`HTTP_PROXY`、`HTTPS_PROXY`、`ALL_PROXY` 会让 `dsh-http-proxy` 在 boot 阶段安装它自己的 Agent,
它同样带五分钟限制,并会替换掉本插件。DSH 先装 proxy、后加载插件,所以**只有不设代理时本插件才一定生效**。

### 平台状态

| 平台 | 状态 |
|---|---|
| macOS(Darwin 24,Node 24.21.0,内建 undici 7.29.1) | 已测试,可用 |
| Windows | **未测试**——欢迎提 issue 分享你的结果 |

代码不含任何平台相关路径、文件写入或进程调用,唯一依赖是 `undici`,因此在 Windows 上行为应当一致,
但尚未实测验证。

### 其他说明

- 本 bundle 未声明 `peerDependencies`,因此不受 DSH 插件版本兼容性检查拦截。
- pnpm 的 `minimumReleaseAge` 策略(默认 24 小时)会拦截刚发布的版本。若安装提示装了旧版本,
  请指定精确版本:`dsh plugin add dsh-undici-timeout-fix@1.1.0`。

### 许可

MIT

Install

dsh plugin --profile web add github:lilinhua0413/dsh-undici-timeout-fix

Profile: web

  • This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.
Source