dsh-opencode-free-tier
Verifieddsh-opencode-free-tier Β· v0.1.1 Β· MIT
Make DSH llm-pi-ai opencode routes pass Zen free-tier gate: opencode User-Agent + canonical ses_ session + bash/read tools. Scoped to opencode.ai only.
Install
dsh plugin add dsh-opencode-free-tier Confirm the layer applied with dsh --profile default --dump-config β see the install guide.
Source
Tags
Creators
Readme
dsh-opencode-free-tier
Free OpenCode Zen models inside DeepSeek Harness (DSH) via the stock llm-pi-ai adapter β no API key, no extra provider plugin.
The problem
Since 2026-09-16, OpenCode Zen's anonymous free lane rejects every request that doesn't look like traffic from the OpenCode CLI:
403: {"type":"FreeTierError","message":"Error from provider (Console): OpenCode's free tier can only be used from within OpenCode"}
Live-probed 2026-09-18, the gate has three parts β all must hold:
User-Agentstarts withopencode/x-opencode-sessionisses_+ 26 chars (12 lowercase hex + 14 Base62)- the chat body streams (
stream: true) with function tools namedbashandread
DSH's llm-pi-ai opencode route sends none of the three (UA deepseek-harness/...,
the DSH session id, no tools on plain chats), so every free-model call fails β while
OpenCode CLI works keyless. No login and no key are required; the anonymous lane key
is the literal string public.
What this plugin does
A zero-dependency Cordis plugin that fixes all three at the fetch transport layer:
llm/streamwaterfall observer β carriesGenerateOptions.sessionIdin anAsyncLocalStorageacross each adapter stream, so the fetch layer knows which DSH conversation a request belongs to (stable session β optimal upstream prompt-cache routing).- Fetch middleware, scoped strictly to
opencode.ai(+ subdomains) β every other host passes through byte-for-byte untouched:- missing/non-CLI
User-Agentβopencode/<cli-version> (platform arch; node...) - missing/malformed
x-opencode-sessionβ canonicalized (ses_+ 26) from the DSH conversation id; an already-canonical id passes through (cache affinity preserved) - fills
x-opencode-client: cli,x-session-affinity,X-Session-Id,x-opencode-request,x-opencode-projectwhen absent - chat-completions bodies missing
bash/readtools get the stubs appended (tool_choice: "none"when the caller had no tools, so the model never calls them)
- missing/non-CLI
Already-correct requests pass through untouched (idempotent) β e.g. it coexists with
opencode2dsh instead of breaking it.
It supersedes dsh-opencode-session-header, which only stamped a non-canonical
session (no UA, no tools) and actively breaks canonical sessions by overwriting them.
Remove that plugin when installing this one.
Requirements
- DSH (
DeepSeek Harness) with awebprofile; Node.js β₯ 20 (already present if DSH runs) - Outbound HTTPS to
opencode.ai
Install
Option A β from npm (recommended)
dsh plugin --profile web add dsh-opencode-free-tier
then register the bundle in package.json:
{
"dsh": {
"profile": {
"bundles": [
"@deepseek-ai/dsh-base",
"@deepseek-ai/dsh-web-app",
"dsh-opencode-free-tier",
"dsh-file-upload"
]
}
}
}
Option B β from git
cd ~/.dsh/profiles/web
pnpm add github:DOCUTEE/dsh-opencode-free-tier
then register the bundle as above.
Option C β from a local clone
git clone https://github.com/DOCUTEE/dsh-opencode-free-tier.git
cd ~/.dsh/profiles/web
pnpm add file:/path/to/dsh-opencode-free-tier
then register the bundle as above.
Finally:
cd ~/.dsh/profiles/web
pnpm install
Remove dsh-opencode-session-header from dependencies + bundles if present,
then restart dsh web once β plugins load at boot.
Configure the free route
No API key needed. The anonymous lane key is the literal string public, but
llm-pi-ai still requires the route to name a credential β otherwise pi-ai
refuses the request before it is even sent (Provider is not configured: opencode). So expose the anonymous key through $DSH_HOME/.env:
# ~/.dsh/.env (DSH_HOME defaults to ~/.dsh)
OPENCODE_ANON_KEY=public
In ~/.dsh/settings.yaml:
llm-pi-ai:
providers:
opencode:
apiKeyEnv: OPENCODE_ANON_KEY
headers:
Authorization: Bearer public
Restart dsh web after editing .env β the environment snapshot is taken
at launch, so a key added while DSH runs is invisible until restart.
Then pick any free model from the opencode route (e.g. mimo-v2.5-free,
deepseek-v4-flash-free, ling-3.0-flash-fin-free, nemotron-3-ultra-free).
Verify
curl -s -X POST https://opencode.ai/zen/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer public" \
-H "User-Agent: deepseek-harness/0.1.0 test" \
-d '{"model":"mimo-v2.5-free","messages":[{"role":"user","content":"hi"}],"stream":true,"max_completion_tokens":10}' \
--max-time 20 | head -c 300
- Without the plugin:
FreeTierError. - With the plugin (restart DSH, chat with the model): normal streamed chunks.
Or run the plugin's own tests:
npm test
Runtime switch (no restart needed)
State file: ~/.dsh/plugins/dsh-opencode-free-tier.json (defaults to ~/.dsh,
or $DSH_HOME when set):
{ "enabled": false }
falseβ everything passes through untouchedtrueor file missing β fixing on- re-read on every matching request
Plugin config in cordis.patch.yml also accepts { hosts?, enabled? }.
How it works
DSH llm-pi-ai (opencode route)
β pi-ai openai-completions stream
βΌ global fetch
dsh-opencode-free-tier middleware (opencode.ai only)
β UA β opencode/β¦ Β· session β ses_+26 Β· tools β +bash/+read
βΌ
https://opencode.ai/zen/β¦ Authorization: Bearer public
Session derivation mirrors the CLI: SHA-256("ses\0" + DSH-session-id) β
ses_ + 6 bytes hex + 10 bytes Base62. The same conversation keeps a stable
session; different conversations separate (same scheme as opencode2dsh, so a
mixed setup shares cache affinity).
Testing
node --test test/free-tier.test.mjs
Covers: canonical session passthrough/hashing, CLI UA shape, tool injection + idempotence, middleware fixing a DSH-like request while preserving an already-correct one and leaving foreign hosts untouched.
Compatibility & retirement
- Verified against DSH
0.1.2-rc.1and@earendil-works/pi-ai0.85.x. - Depends on DSH outbound LLM traffic using the process-global
fetch. If a future DSH build changes its network stack, the plugin silently stops fixing β the symptom is simply the403returning; uninstall then. - If upstream DSH ever ships native CLI disguise for the free lane, retire this plugin:
remove it from
dependencies+bundles,pnpm install, restart.
Troubleshooting
| Symptom | Cause & fix |
|---|---|
Provider is not configured: opencode |
The route names no credential, so pi-ai rejects before sending. Add OPENCODE_ANON_KEY=public to ~/.dsh/.env and apiKeyEnv: OPENCODE_ANON_KEY to the route (see Configure), then restart dsh web. |
403 FreeTierError: free tier can only be used from within OpenCode |
The disguise isn't applied: plugin not installed/enabled, DSH not restarted after install, or the runtime switch disables it. Check ~/.dsh/plugins/dsh-opencode-free-tier.json is absent or {"enabled": true}. |
400 MissingSessionID |
An old dsh-opencode-session-header is overwriting the canonical session β remove that plugin. |
Key added to .env but still MISSING_CREDENTIAL |
.env is snapshotted at launch β restart dsh web. |
License
Release process (maintainers)
Publishing uses npm trusted publishing (OIDC) β
no tokens, no OTP. One-time setup on npmjs.com β package β Settings β
Trusted Publisher: GitHub Actions, user DOCUTEE, repository
dsh-opencode-free-tier, workflow publish.yml, allowed action npm publish.
To release:
# 1. bump version in package.json (must match the tag below)
# 2. commit, then:
git tag v0.1.0 && git push origin v0.1.0
Pushing the tag runs .github/workflows/publish.yml, which runs tests and
npm publishes. Provenance is generated automatically.