dsh-opencode-go-path
September 24, 2026 · View on GitHub
One of the plugins bundled with DSH Ready GUI — the GUI ships all four, ready to tick. Each one also installs standalone into any DSH host (see "Install and go" below). GUI: https://github.com/itchenshi/dsh-ready-gui
One plugin for the OpenCode / OpenCode Go routes: install it, tick it, done — no hand-editing config, no command line.
What it fixes for you
| The problem you hit | What the plugin does |
|---|---|
deepseek-v4.1-flash is missing from the model list, and adding it fails with needs an api | Declares the route's wire protocol, so models the catalog doesn't describe work too |
| Newly released models never show up in the catalog | Detects the route's catalog at engine startup, adds the DeepSeek V4.1 models and puts them first |
| The model is there but requests fail and saving it is refused | Supplies the route baseURL (a model outside the catalog has nowhere to point without it) |
Multi-turn calls fail with 400 MissingSessionID | Attaches a per-conversation x-opencode-session header to OpenCode requests |
Multi-turn calls fail with 400 reasoning_content must be passed back | Supplies the DeepSeek thinking-protocol declaration a catalog-unknown model is missing |
Install and go
-
Using DSH Ready GUI (recommended): the plugin ships inside the GUI. Open the GUI → Settings → Third-party plugins → tick OpenCode Go routes, then restart the engine as prompted.
-
Any other DSH host (
dsh web, the CLI) — either route works:# Recommended: install straight from GitHub (recorded in your profile, updatable) dsh plugin --profile web add github:itchenshi/dsh-opencode-go-path # Fallback: install the release tarball (use this if github.com is unreachable for you) dsh plugin --profile web add https://github.com/itchenshi/dsh-opencode-go-path/releases/download/v0.1.4/dsh-opencode-go-path-0.1.4.tar.gzBoth land the full repository contents (including
cordis.patch.yml); no extra configuration is needed afterwards.
Not on npm yet: sign-up is unreachable (
www.npmjs.comanswers with a Cloudflare challenge), so nothing can be published. Use one of the two routes above; publishing resumes once sign-up works.
How to confirm it worked: the first entry in the session model selector is
DeepSeek V4.1 Flash, and $DSH_HOME/settings.yaml lists deepseek-v4.1-flash first under
llm-pi-ai.providers.opencode-go (the plugin also adds baseURL: https://opencode.ai/zen/go/v1 if you
never set one).
What it actually does
1. Declares the route protocol (config layer)
The engine resolves a model's api in this order:
route.api ?? the model's api in the catalog ?? the api shared by all models in the catalog
The opencode-go catalog spans three protocols (anthropic-messages / openai-completions /
openai-responses), so the third fallback cannot be inferred; a model outside the catalog (such as
deepseek-v4.1-flash) has no second fallback either. The engine then reports needs an api, and the
model page's strict validation refuses to save it.
Through cordis.patch.yml the plugin adds providers.opencode-go.api: openai-completions to the
llm-pi-ai row — the protocol OpenCode Go's OpenAI-compatible endpoint speaks.
2. Detects the catalog, adds the models, puts them first (runtime)
Once the llm-pi-ai settings namespace is ready, the plugin:
- Detects the route's models through the engine's own
llm.discoverModels(). For a route pi-ai ships a catalog for, that channel answers the catalog with no network request; the plugin passes nobaseURLand no credential on purpose, so a route the catalog does not describe fails before reaching the network. Nothing detected means nothing is guessed. - Completes and hoists:
- you configured
models→ the DeepSeek V4.1 models go to the front, your own entries stay in place and in order; - you configured none → the list is seeded from the detected catalog ("V4.1 first + every catalog model"). The engine treats a non-empty configured list as the provider's complete set, so writing only our models would replace the whole catalog — the seed has to come from the catalog, and nothing is lost;
- "is the model already listed" is decided by our own ids, not the
deepseek-v4.1prefix, so a future upstream sibling cannot displace the Flash we know.
- you configured
- Supplies an endpoint when the route has none: a catalog-unknown model resolves its endpoint as
route.baseURL ?? the model's baseUrl in the catalog ?? the provider-level baseUrl, and opencode-go has no provider-levelbaseUrl— so without one the model can neither be sent nor stored (needs a baseURL). The plugin addshttps://opencode.ai/zen/go/v1only when the route has none; a route you pointed somewhere else is left untouched. A hand-written config that already has the right order but no endpoint is repaired too. - Is idempotent: right shape, endpoint present → no write, no event, no loop.
3. Attaches the session header
OpenCode's relay pins requests carrying the same x-opencode-session to the same upstream backend,
keeping the prompt cache warm across one conversation (fixes 400 MissingSessionID).
Safe by default: every DSH session derives a random opaque UUID, and the internal session ID is
never sent to a third party. mode: 'session-id' is an explicit opt-in for people who fully trust
the upstream.
4. Supplies the thinking protocol a catalog-unknown model is missing (fixes 400 reasoning_content)
deepseek-v4.1-flash is not in the installed catalog, so the engine treats it as a plain model: it
neither asks for thinking nor replays reasoning_content. pi-ai detects its DeepSeek compatibility
from the provider name or host, and the OpenCode Go relay matches neither — so replayed history
omits reasoning_content and the upstream answers 400
(The reasoning_content in the thinking mode must be passed back to the API). Every multi-turn
conversation on that model fails.
The plugin supplies the declaration the catalog would have provided (byte-for-byte what the same
route's deepseek-v4-flash / -pro entries declare): compat.requiresReasoningContentOnAssistantMessages,
thinkingFormat: deepseek, and reasoningEfforts. It fills gaps rather than overwriting: a name,
limits or compat switch you changed is kept, so an entry you already added is upgraded in place instead
of needing to be deleted and re-added.
Configuration (the cordis.patch.yml row config, all optional)
| Key | Description |
|---|---|
providers | Route names to attach the session header to; default ['opencode', 'opencode-go'] |
mode | 'uuid' (default, safe) or 'session-id' (explicit opt-in; sends the internal session ID) |
debug | true logs every streaming call that received the header |
debugFile | Path to append JSON lines to; only paths under $DSH_HOME/logs or the system temp directory take effect; the logged session is a one-way SHA-256 hash |
Permissions and boundaries (for marketplaces that scan statically)
- Runtime dependencies: none. Node built-ins only. Outbound network: none. The plugin makes no
requests of its own; it wraps
globalThis.fetchto add the header and calls the engine's ownllm.discoverModels()(which answers the shipped catalog without touching the network). - Files: none by default. Only an explicitly configured
debugFileis ever written, and only under$DSH_HOME/logsor the system temp directory. - Local routes: none. No page half, no HTTP route.
- Credentials / commands / native artifacts / lifecycle scripts: none.
- Failure boundary: it depends on the public contracts of the engine's assembly layer, the
settingsservice and thellmevents — not on an engine version. If those change, the engine reports the plugin as failed to load instead of failing silently. Writes go through thesettingsservice and re-run the engine's strict validation; a rejected write is refused as a whole (with the reason logged) rather than leaving half a config.
Development
node --check lib/index.js
npm test # local behaviour tests (no network, no engine)
Add an entry to V4_1_MODELS to support another V4.1 model. planRouteUpdate() /
withV41ModelsFirst() / withV41Defaults() / isV41() decide "what to write", "how to hoist", "how to
fill gaps" and "is it a v4.1 model", and all four are unit-tested.
License
MIT