dsh-ocg-session-head
verneuil/dsh-ocg-session-head
为以自定义供应商名接入的 OpenCode Go 路由,按会话注入 x-opencode-session 请求头:走宿主官方 llm-pi-ai/request-headers 瀑布;零依赖、不替换 fetch,且不覆盖他人已写入的同名头。
安装
dsh plugin --profile web add github:verneuil/dsh-ocg-session-headREADME
OCG Session Head (@verneuil/dsh-ocg-session-head)
Injects a per-conversation x-opencode-session request header for OpenCode Go routes
⚠️ Read this first: if either condition below does not apply, you do not need this plugin
1) Your host must be DeepSeek Harness 0.2.2 or newer
The plugin does exactly one thing: it writes a header inside the header seat the host officially provides (the
llm-pi-ai/request-headerswaterfall). That seat only appeared in 0.2.2-alpha.3 and became stable in 0.2.2-rc.1 — older versions simply do not have this foundation. Installing it on an older host does not error out, but it never takes effect — it looks like “installed but useless”, when in fact there is nowhere for it to hook into.2) It only matters for OpenCode connected under a custom provider name
- If you added OpenCode / OpenCode Go through DSH's official path (using pi-ai's built-in
opencode/opencode-goroutes), recent pi-ai already adds the session header automatically — you do not need this plugin, and installing it is pure redundancy.- Only when you connect like this — you really do use an OpenCode service, but the provider name is your own (a hand-written
baseURLplusapi: openai-completions, say you call itocg) — is that automatic wrapping out of reach, and this plugin is what supplies the header.In one sentence: official path → no plugin needed; custom name → use this plugin.
This is a replacement for dsh-opencode-session (the fetch monkey-patch): it does not replace
globalThis.fetch and does not use AsyncLocalStorage — it only writes one header inside the seat the host
reserves for exactly this purpose.
- Repository: https://github.com/verneuil/dsh-ocg-session-head
- Self-test:
node scripts/self-test.mjs(zero dependencies, no host required)
1. Why it is needed
Since 2026-09-05, OpenCode Go requires every inference request to carry a stable x-opencode-session,
otherwise it returns:
400 {"type":"MissingSessionID", ...}
pi-ai already wraps the built-in opencode / opencode-go routes; but a custom-named route
(your own name, hand-written baseURL + api: openai-completions) never gets that wrapping, so a plugin has
to supply the header.
Not sure which case you are? See item 2 of “Read this first” above.
2. Version requirements (important)
| Item | Requirement |
|---|---|
| DSH | ≥ 0.2.2 (the llm-pi-ai/request-headers seat arrived in 0.2.2-alpha.3; available from the 0.2.2 line) |
| Node | No extra requirement (pure ESM, zero dependencies) |
Installing on an older host is safe: the plugin loads and logs its ready line, but the event never fires → behaviour equals not installing it (OCG still returns 400). This makes “install first, upgrade later” a valid step-by-step verification path.
3. Installation
3.1 From npm (recommended)
Install @verneuil/dsh-ocg-session-head in DSH's plugin manager (or pin a version, e.g.
@verneuil/dsh-ocg-session-head@0.1.1), or add it to the profile's package.json directly:
"@verneuil/dsh-ocg-session-head": "^0.1.1"
3.2 Offline / local tarball
⚠️ Never point the installer at a package directory. pnpm turns directories into
link:(junction) entries, module resolution degrades to plain Node behaviour, DSH's resolution interceptor no longer applies and the entry fails withfailed to import. Only a tarball is unpacked into a real directory inside the profile.
Reference it as file: in the profile's dependencies:
"@verneuil/dsh-ocg-session-head": "file:<your absolute path>/dsh-ocg-session-head-<version>.tgz"
Then install with DSH's bundled node/pnpm (no system npm required):
$node = Join-Path $env:DSH_HOME 'dsh-runtimes\dsh-primary-runtime\dependencies\node\bin\node.exe'
$pnpm = Join-Path $env:DSH_HOME 'dsh-runtimes\dsh-primary-runtime\dependencies\pnpm\bin\pnpm.mjs'
$target = Join-Path $env:DSH_HOME 'profiles\desktop'
& $node $pnpm install --dir $target
Either way, restart DSH afterwards (restarting kills the AI processes inside the host, so it must be done by hand).
4. Configuration
cordis.patch.yml ships defaults; you can also override them by id in the profile's cordis.patch.yml:
- id: dsh-ocg-session-head
config:
providers:
- ocg
debug: true
Overriding by id replaces the whole config object.
| Key | Default | Meaning |
|---|---|---|
providers | ['ocg'] | Provider-key allowlist, matched exactly against provider in the event |
hostSuffixes | ['opencode.ai'] | baseURL host-suffix allowlist; the two gates are OR-ed, the host gate is the fallback when you rename the route |
header | x-opencode-session | Header name to inject; must be a valid HTTP header name |
mode | session-id | session-id: use the host conversation id (stable across turns, compaction and restarts); uuid: derive an opaque uuid that is stable within the process |
debug | false | Log one line per successful injection |
maxTrackedSessions | 4096 | Session map cap in uuid mode; the oldest entry is evicted first |
Config validation (errors are loud)
Following the official “misconfiguration should be loud” principle, an invalid config fails plugin activation and names the offending field — it never falls back silently.
| Situation | Behaviour |
|---|---|
Key not set (undefined) | Default is used |
| Unknown key (likely a typo) | ❌ Error, valid keys listed |
providers / hostSuffixes not a string array, or containing empty/duplicate items | ❌ Error, offending index reported |
header empty or not a valid HTTP header name | ❌ Error |
header points at a Harness attribution header (user-agent, x-deepseek-harness-*) | ❌ Error (the host overwrites it after all contributors, so writing it would be useless) |
mode is neither session-id nor uuid | ❌ Error |
debug not a boolean, maxTrackedSessions not a positive integer | ❌ Error |
providers and hostSuffixes both empty | ❌ Error (nothing could ever match) |
Array semantics: “not set” means the default; passing an empty array explicitly disables that gate. For example
hostSuffixes: []matches by provider key only, with no baseURL host fallback.
5. Behaviour guarantees
- An existing header always wins: static
headersconfig, the official implementation and other plugins are never overwritten. - No session id, no injection: auxiliary calls without a
sessionIdpass through untouched. - Fail open: any internal error only logs a warning line and then calls
next(); this plugin never breaks an inference call. - No global side effects: no
globalThismutation, nofetchreplacement, no file writes. - Disposed with the plugin: the listener is registered via
ctx.on()and follows the fiber lifecycle.
6. Verification (four checks)
- Ready line: the host console shows
[dsh-ocg-session-head] ready (header=x-opencode-session mode=session-id providers=ocg ...) - Injection line: with
debugenabled, every model call shows[dsh-ocg-session-head] x-opencode-session=<id> provider=ocg model=... purpose=chat - Real request 200: normal conversations never return 400
MissingSessionID. - Counter-proof: disable the plugin and the 400 comes back immediately — proving the header was coming from it.
7. Uninstall and rollback
- Uninstall from the plugin manager, or remove the package from the profile's
dependenciesand reinstall. - The older plugin
dsh-opencode-sessioncan coexist (both let existing headers win: this implementation writes first, the older one then no-ops). During migration, keep the old plugin installed but disabled, so you can switch back in 30 seconds.
8. Development and packaging
# self-test (zero dependencies, no host required)
node scripts/self-test.mjs
# pack with DSH's bundled runtime (no system node/pnpm needed)
$node = Join-Path $env:DSH_HOME 'dsh-runtimes\dsh-primary-runtime\dependencies\node\bin\node.exe'
$pnpm = Join-Path $env:DSH_HOME 'dsh-runtimes\dsh-primary-runtime\dependencies\pnpm\bin\pnpm.mjs'
& $node $pnpm pack
Local test packages may be named <version>-rc.<YYYYMMDD-HHmm> (bump the version temporarily → pack →
revert immediately).
9. Known limitations
- Only pi-ai routes are covered (
@deepseek-ai/dsh-llm-pi-ai).deepseek-officialgoes through@deepseek-ai/dsh-llm-deepseek, which sends its ownx-deepseek-harness-session-idand is unrelated. - Model discovery is not covered (the host contract explicitly says the event
excludes model discovery). - The event is synchronous: a listener must mutate
headersin place andreturn next()immediately, and must not retain the object reference. engines.dshis declared as>=0.2.2-alpha.3. The host evaluates compatibility withincludePrerelease: true, so a plain>=0.2.2would reject0.2.2-rc.1, and>=0.2.2-rc.1would reject a future0.2.3-rc.1. Anchoring at the alpha that introduced the header seat accepts both the stable line and every later RC, while anything below0.2.2is correctly rejected.
License
MIT