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
No comments yet. Be the first to write one.