Bundle
dsh-finish-reason-patch
Tolerate OpenAI-compatible model streams that end without a terminal finish_reason so DSH turns complete instead of aborting.
- Source
- Canson666
- stars
- 1 stars
- License
- MIT
- Updated
- Updated 14 days ago
Readme
# dsh-finish-reason-patch
DSH plugin: stop agent turns from being aborted when an OpenAI-compatible
provider (GPT-family models, gateways, proxies) ends a streaming response with
full output but **without a terminal `finish_reason`**.
## The bug it fixes
DSH routes OpenAI-compatible providers through `@earendil-works/pi-ai`. When
the wire stream ends without ever seeing `choices[].finish_reason`, pi-ai
raises `Stream ended without finish_reason` (`openai-completions.js:438`). By
that point every content block is already closed and delivered — the response
is complete, only the terminal marker is missing. pi-ai surfaces that as an
`error` event, the `dsh-llm-pi-ai` adapter maps it to a terminal
`finish {kind:'error', code:'TRANSPORT'}` chunk, and the agent loop
(`dsh-agent-loop`) throws — aborting a perfectly good model turn. Other agents
(e.g. Codex) tolerate the same wire behavior by treating end-of-stream as the
end of the response.
## The fix
The plugin hooks the `llm/stream` waterfall — every model call (agent turns,
session-title requests, prepared calls, subagents) flows through it via
`LlmRuntime.streamWithRegistration`. When the terminal finish is exactly that
error, it is rewritten to a normal `stop` finish (or `tool-calls` if tool-call
deltas were delivered), so the turn completes instead of aborting. This holds
whether or not content was delivered: some OpenAI-compatible gateways stream
only usage — or nothing — before ending without a `finish_reason` (reporting
`outputTokens: 0`), and they are accepted as complete empty responses instead
of retry-looping (Codex-style tolerance). All other error finishes (transport
resets, rate limits, HTTP errors, ...) pass through untouched, so the existing
retry policy still applies to genuinely broken responses.
When it fires you'll see:
`[finish-reason-patch] provider finished without finish_reason -> mapped to "stop"`
## Installation
The plugin is a plain Cordis host plugin (exports `name` / `apply`, no external
dependencies beyond the `ctx` Cordis passes in).
### 1. As a persistent profile bundle (recommended, survives restarts)
Ship this folder as a package named `dsh-finish-reason-patch`:
1. Place the package where your DSH profile can resolve it, e.g. a junction or
symlink inside `<profile root>/node_modules/dsh-finish-reason-patch`
pointing at this folder (same pattern as other local DSH plugins).
2. In the profile root `package.json`, add the package to
`dsh.profile.bundles` and to `dependencies` (e.g.
`"dsh-finish-reason-patch": "link:<absolute path to this folder>"`).
3. Restart DSH — bundle patches compose at boot. The package's own
`cordis.patch.yml` inserts the plugin row automatically.
### 2. As a dynamic Cordis plugin (current process only)
Define/run the plugin host body (`plugin.mjs`) through the Cordis dynamic
plugin tools. It stays active until the process exits; re-run after a restart.
Effects are identical and idempotent if both routes are active.
## Verifying
- Logic self-test (no network): `node selftest.mjs` — all 5 cases pass.
- Live: run a normal conversation with a GPT-class model. Turns that used to
abort now complete, and the log line above appears.
## Removal
Remove the bundle entry + dependency from the profile `package.json`, delete
the `node_modules` junction, restart DSH. No changes are made to any shipped
DSH file.
## Files
- `plugin.mjs` — the plugin (`llm/stream` waterfall hook + rewrite logic)
- `cordis.patch.yml` — bundle patch layer (the profile-tree insert row)
- `package.json` — npm metadata (`dsh.runtime: host`, `dsh.bundle.patch`)
- `selftest.mjs` — local logic self-test
- `LICENSE` — MITInstall
dsh plugin --profile web add github:Canson666/dsh-finish-reason-patch
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-finish-reason-patch from the hub
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.