OpenCode on DeepSeek Harness
Gateway origin headers · Session affinity · Free-tier tool fallback · Live dual-mode quota meter
Quick Start · What this fixes · Interface · Configuration · Troubleshooting · Changelog · Contributing
dsh-opencode-patch is a DeepSeek Harness host plugin that keeps OpenCode Zen & Go models working inside DSH. Connect claude-sonnet-4-5, gpt-5.4, gemini-3.8-flash, deepseek-v4.1-flash, muse-spark-1.3-contributor-free, qwen3.8-flash and the rest of the OpenCode catalog without network rejections, Cloudflare challenges, entitlement mismatches, or invisible limits.
OpenCode's gateways expect request traits DSH does not send by default: a valid x-opencode-session on every turn, official CLI origin proof (User-Agent, client/project headers, ses_…-shaped IDs), and read/bash tool definitions on free-tier requests. DSH subagents, background evaluations, and experimental modes like Auto Review also invoke the LLM in standalone sessions where sessionId is omitted or unlinked.
The plugin restores every missing protocol element at the network layer — strictly for OpenCode routes (opencode / opencode-go / opencode-responses / opencode-anthropic). All other traffic (DeepSeek, OpenAI, Anthropic, GitHub) passes through untouched.
Highlights
- 🔑 Deterministic
ses_<12hex><14base62>session hashing with KV-cache affinity across turns, subagents and forks - 🌐 Gateway origin restoration —
User-Agent,x-opencode-client,x-opencode-project, parent-session lineage - 🧰 Free-tier
read+bashtool-schema fallback so Zen free models stop failing with403 FreeTierError - 📇 models.dev-backed catalog with offline shims and background SWR refresh — names, context windows, prices
- ⭕ Live dual-mode composer meter — Go quota ring (5-hour / weekly / monthly) or Zen pay-as-you-go pill, plus session spend and model rate
- 🕵️ Strict credential isolation — Zen keys (
oc_sk_…) never query the Go quota endpoint
Contents
- Quick Start
- What This Fixes
- Supported Models & Protocols
- The Interface
- Configuration Reference
- Troubleshooting
- Compatibility & Verification
- Deep Dive: Protocol Specification
- Attribution & License
1. Install into your DSH Web profile (Node 24+):
cd ~/.dsh/profiles/web
npm install dsh-opencode-patchThe same tree also publishes the scoped aliases @viztor/dsh-opencode-patch and @viztor/dsh-opencode — install any one of them, the row name stays dsh-opencode-patch.
2. Enable the bundle — add the package to the profile's dsh.profile.bundles array:
3. Add a credential so the quota meter can resolve a key — store OPENCODE_GO_API_KEY (Go subscription, sk-…) and/or OPENCODE_API_KEY (Zen pay-as-you-go, oc_sk_…) in DSH Credentials or your environment.
4. Restart dsh web. The host bundle is only imported at boot (hmr root: []), so a restart is what loads lib/index.mjs; client UI changes (lib/client.js) only need a browser refresh.
Done — the OpenCode Patch card appears under Settings → Plugins, and the meter mounts in the composer dock as soon as an OpenCode model is active.
| Without the patch | With dsh-opencode-patch |
|---|---|
Zen free models fail with 403 FreeTierError |
Gateway origin headers & tool fallbacks restored automatically |
Session IDs rejected with 400 MissingSessionID |
Deterministic ses_… session hashing and affinity across turns |
| Subagents lose conversation context | Parent session tracking (x-opencode-parent-session-id, x-parent-session-id) |
Auto Review calls fail with TRANSPORT: Connection error |
Fallback session turn capture preserving turn state across eval calls |
| Quotas and balances are invisible | Live dual-mode composer meter showing Go quota or Zen pay-as-you-go |
Zen keys cause 403 EntitlementError on Go usage |
Strict credential isolation keeping Zen keys away from the Go endpoint |
Projects share a single "global" telemetry bucket |
Dynamic workspace attribution resolved from the active session.header.cwd |
Gateway /models returns a truncated, unnamed list |
models.dev enrichment with display names, context windows and prices |
OpenCode serves inference across multiple upstream protocols through one gateway, and the patch covers all four families.
- Anthropic Messages (
https://opencode.ai/zen/v1/messages):claude-sonnet-4-5,claude-opus-4-7,claude-haiku-4-5,qwen3.8-flash - OpenAI Responses (
https://opencode.ai/zen/v1/responses):gpt-5.4,gpt-5.2,gpt-5.1-codex-max,muse-spark-1.3,space-bunny-free, and the free-tiermuse-spark-1.3-contributor-free - OpenAI Chat Completions (
https://opencode.ai/zen/v1/chat/completions):deepseek-v4.1-flash,kimi-k2.5,kimi-k3,minimax-m2.5,glm-5.2, plus free-tiernemotron-3-ultra-free,ling-3.0-flash-fin-free,mimo-v2.6-flash-free - Google Generative AI (
https://opencode.ai/zen/v1/models/*:streamGenerateContent):gemini-3.8-flash,gemini-3.1-pro,gemini-3.5-flash-lite
- OpenAI Chat Completions (
https://opencode.ai/zen/go/v1/chat/completions):deepseek-v4.1-flash,deepseek-v4-pro,deepseek-v4-flash,deepseek-v4-flash-vision-exp,qwen3.8-flash,qwen3.8-max,qwen3.7-plus,kimi-k3,kimi-k2.7-code,glm-5.3,glm-5.3-flash,glm-5.2,grok-4.7,grok-4.6,minimax-m3,minimax-m2.7,mimo-v2.6-pro,mimo-v2.6-flash,gpt-5.6-luna,gpt-6-luna - Monitored by the live 3-window quota meter (5-hour rolling, weekly, monthly). The full set ships in the bundled catalog — see Authoritative Model Catalogs.
Zen's provider-level SDK is @ai-sdk/openai-compatible. models.dev names a different SDK per model only when that model needs one, so the presence of provider.npm is the signal — and it is what decides the wire protocol. Nothing is matched by hand:
models.dev provider.npm |
models | protocol | served from |
|---|---|---|---|
| (absent) | 53 | OpenAI Chat Completions | the route you configured |
@ai-sdk/openai |
32 | OpenAI Responses | opencode-responses |
@ai-sdk/anthropic |
23 | Anthropic Messages | opencode-anthropic |
@ai-sdk/google |
8 | (no such protocol in DSH) | not offered |
Counts are the 116 opencode models in models.dev as of 2026-10-05.
The patch keeps those two routes out of both the model picker and Settings → Models. Three properties hold today, and one does not yet:
- You keep choosing. The picker shows exactly the models you listed. Selecting one is matched to the right protocol automatically, so adding any Responses or Messages model to your list is enough — no second route to declare by hand.
- You are never offered a model that cannot work. The 8
@ai-sdk/googlemodels are dropped from the discovery list and from what theopencoderoute reports, because DSH implements no such protocol and selecting one could only fail — with nothing in the row to say why. - One credential. Every routed model authenticates with the
opencodekey you already configured, resolved through the credentials service and never re-asked for.
What is still yours to declare: the route itself. opencode-responses — and opencode-anthropic, once you use an Anthropic-plane model — must exist in your profile's llm-pi-ai providers block, listing the models it serves:
opencode-responses:
api: openai-responses
baseURL: https://opencode.ai/zen/v1
headers:
authorization: Bearer unused # swapped for your key by the fetch patch
models:
- id: muse-spark-1.3-contributor-free
name: Muse Spark 1.3 Free
contextWindow: 1048576
maxTokens: 131072
input: [text, image]A plugin-owned route — one you would not have to declare at all — is the intended end state, and the mechanism is implemented and verified end-to-end: responses-provider.ts mounts the host's own llm-pi-ai below isolate("authorization"), so it registers the route with zero authorization flows and cannot collide with the host's instance. What is not solved is reaching that package at runtime: it is a profile bundle, so it is not resolvable from the plugin, the profile, or the CLI's entry point, and declaring it as a dependency pulls ~1000 lockfile lines and breaks pnpm install over ignored build scripts. Until a resolution path lands, the plugin registers nothing and says so in the log. Do not remove the route from your profile yet — doing so would leave the model with nowhere to be served from.
| Mode | What the patch does |
|---|---|
| Interactive multi-turn chat | KV prompt-cache affinity via a stable per-session ses_… id |
| Subagents & forks | Child and parent sessions both hashed; lineage carried in parent headers |
| Agent teams | Shared-workspace attribution preserved across orchestration turns |
| Experimental Auto Review | Background audit calls with an omitted sessionId still get a deterministic turn state, headers and tool fallback |
Eight controls in three sections — the decisions a user actually makes. Everything renders from the platform's own primitives (Switch, Tag, Button, host tokens), and every knob carries a hint plus a reset-to-default affordance.
| Section | Control | Default | What it does |
|---|---|---|---|
| Gateway Requests | Inject User-Agent | on |
Restores the official OpenCode CLI User-Agent so Cloudflare WAF checks pass |
| Gateway Requests | Inject Origin Headers | on |
Injects x-opencode-client (and the origin header set) on gateway traffic |
| Gateway Requests | Attach Workspace Project | on |
Tags x-opencode-project with the active folder name; off omits the header |
| Models & Free Tier | Enrich Models from Models.dev | on |
Merges canonical specs, display names, prices and active free models into listings and native DSH discovery |
| Models & Free Tier | Inject Core Tools | on |
Adds the read + bash schemas free-tier /responses bodies require |
| Quota Meter | Enable Go Quota Monitor | on |
Mounts the live quota / credit meter in the composer dock |
| Quota Meter | Show Session Spend & Model Rate | on |
Adds the session's accumulated cost and the active model's per-million-token rate |
| Quota Meter | Credential Source | auto |
Which key wins when several are known: Automatic · Live request first · Declared key first |
Override-style knobs (literal strings, markers, route lists) are deliberately config-only so a default fits every documented setup — see Configuration Reference.
The meter mounts next to DSH's native ContextMeter in conversation.composer.dock:
┌─────────────────────────────────────────────────────────────────┐
│ Type a message... │
│ │
│ [+] Attach [DeepSeek V4.1 Flash ⌄] [⬆]│
└─────────────────────────────────────────────────────────────────┘
[ ⭕ 73% Context ] [ ⭕ 42% Go Quota ] ← when OpenCode Go is active
[ ⭕ 73% Context ] [ 🪙 OpenCode Zen ] ← when OpenCode Zen is active
- Adaptive bottleneck ring: a real-time SVG ring showing the currently limiting window (
42%,80%, or100%when rate-limited). - Semantic colors: green below 80% (
--dsw-alias-state-success-primary), amber at ≥80% (--dsw-alias-state-warn-primary), red at the cap (--dsw-alias-state-error-primary). - Hover panel: three window rows with live reset countdowns, three overview cards, a session-spend card, a Zen-overflow card, a rate-limited alert, and act-on-it links.
┌──────────────────────────────────────────────┐
│ 42% of 5-Hour quota used [Go Plan]│
│ ████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │
├──────────────────────────────────────────────┤
│ • 5 hours 42% │
│ Resets in 3h 12m │
│ • Weekly 18% │
│ Resets in 5d 8h │
│ • Monthly 65% │
│ Resets in 22d 4h │
├──────────────────────────────────────────────┤
│ QUOTA OVERVIEW │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 5-Hour │ │ Weekly │ │ Monthly │ │
│ │ 42% │ │ 18% │ │ 65% │ │
│ │ in 3h 12m│ │ in 5d 8h │ │ in 22d 4h│ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ SESSION SPEND │
│ deepseek-v4.1-flash · $0.15 / $0.6 per 1M │
│ $0.42 │
│ │
│ ZEN BALANCE FALLBACK │
│ Zen balance ready for overflow Ready │
├──────────────────────────────────────────────┤
│ Last updated 08:30 [ Retry ] │
│ Upgrade plan · Console & balance · Doc │
└──────────────────────────────────────────────┘
- Zen pill: a compact coin badge (
🪙 OpenCode Zen) that switches to the session's accumulated dollar figure once the first turn has been priced. - Pay-as-you-go panel: header with a
Pay-as-you-gobadge, an explanation of per-token billing, the session-spend card (when the price switch is on), and direct links to the OpenCode Console and Pricing.
┌──────────────────────────────────────────────┐
│ OpenCode Zen [Pay-as-you-go] │
│ Per-token pay-as-you-go inference │
├──────────────────────────────────────────────┤
│ AVAILABLE ZEN BALANCE │
│ Per-token pay-as-you-go inference Active │
├──────────────────────────────────────────────┤
│ Last updated 08:30 [ Retry ] │
│ Upgrade plan · Console & balance · Doc │
└──────────────────────────────────────────────┘
Zen balance & overflow. If an OPENCODE_API_KEY (or oc_sk_…) is configured, Zen pay-as-you-go is detected automatically and overflow is marked Ready. Balances change with every generated token, so the popover links straight to the OpenCode Console instead of freezing a stale number in the UI.
These exist in the schema but render no control — each is a literal, a marker, or a reference whose default fits every documented setup. They remain editable in the row's config; cordis.patch.yml is the reference.
| Knob | Default | Why it stays in config |
|---|---|---|
providers |
opencode, opencode-go, opencode-responses, opencode-anthropic |
Route ids to intercept; must cover every route this layer declares |
gatewayUrls |
opencode.ai/zen |
URL substrings marking gateway traffic; only a mirror or relay changes them |
userAgent |
empty (= canonical CLI UA) | Literal override; the default is what the gateway expects |
originClient |
cli |
Literal x-opencode-client value |
sessionIdEnv |
OPENCODE_SESSION_ID |
Names an env var only a caller-supplied session id uses |
freeModelMarker |
free |
Model-id substring; * forces the fallback, '' disables it |
usageBaseURL |
https://opencode.ai/zen/go/v1 |
Endpoint override; auto-discovered from the composition |
debug / debugFile |
false / — |
Diagnostic JSONL logging, not a behaviour anyone tunes in the UI |
# cordis.patch.yml — the plugin's row; every key is optional.
- insert:
- id: dsh-opencode-patch
name: "dsh-opencode-patch"
config:
providers:
- opencode
- opencode-go
- opencode-responses
- opencode-anthropic
gatewayUrls:
- opencode.ai/zen
sessionIdEnv: "OPENCODE_SESSION_ID"
freeModelMarker: "free"
usageBaseURL: "https://opencode.ai/zen/go/v1"
keySource: "auto" # auto | request | configured
injectUserAgent: true
injectOriginHeaders: true
originClient: "cli"
injectProject: true # attach workspace folder (or 'global'); false omits the header
injectCoreTools: true
enrichModels: true # merge models.dev specs + active free models into listings
usageEnabled: true
showUsagePrice: true # session spend + active model rate in the meter
# File-level only debug options:
debug: false
debugFile: "/tmp/dsh-opencode-debug.jsonl"Host reload rule: restart
dsh webafter any host change (lib/index.mjs); a browser refresh is enough for client UI changes (lib/client.js).
| Symptom | Likely cause | Fix |
|---|---|---|
403 FreeTierError on free models |
Gateway headers stripped or tool definitions missing | Keep Inject User-Agent, Inject Origin Headers and Inject Core Tools on |
400 MissingSessionID |
No session header attached | Ensure dsh-opencode-patch is listed in the profile's dsh.profile.bundles |
Auto Review fails with TRANSPORT: Connection error |
Host process not restarted since the update | Stop and restart dsh web so the new lib/index.mjs loads |
| Quota ring never appears for a Go model | No Go credential resolves, or the active provider is not opencode-go |
Store OPENCODE_GO_API_KEY in DSH Credentials and route through opencode-go |
| A catalog model is missing from Settings → Fetch, or a retired one persists | Stale host module, enrichment off, or the adapter answered from its packaged catalog | pnpm run build, restart dsh web, keep Enrich Models on, fetch from the matching route, search the exact id (e.g. space-bunny-free) |
| Popover shows “Limit reached” in red | 100% of a rolling/monthly window reached | Open the OpenCode Console and enable Use balance to overflow into Zen credits |
| Non-OpenCode models misbehaving | Unrelated to this patch | Traffic to other providers passes through untouched |
Verified on DeepSeek Harness 0.2.0-rc.2 (Node 24+)
| Surface | Target |
|---|---|
| Plugin package | dsh-opencode-patch on npm + the @viztor/dsh-opencode-patch / @viztor/dsh-opencode scoped aliases |
| Host profile | DSH Web profile (patchReload: live) |
| Routes claimed | opencode, opencode-go, opencode-responses, opencode-anthropic |
| Gateways | opencode.ai/zen/v1 (/responses, /chat/completions, /messages, :streamGenerateContent), zen/go/v1 (/chat/completions) |
| Supported models | claude-sonnet-4-5, gpt-5.4, gemini-3.8-flash, deepseek-v4.1-flash, muse-spark-1.3-contributor-free, qwen3.8-flash |
| Verification gate | vp check clean, 266 deterministic tests green, full schema validation, consumer install + load (scripts/check.ts) |
Decompiled from the official opencode CLI binary, the gateway enforces different headers for native OpenCode routes versus third-party relays:
// Extracted from OpenCode CLI's HTTP request builder:
headers: {
"x-opencode-session-id": e.sessionID,
...(e.parentSessionID ? { "x-opencode-parent-session-id": e.parentSessionID } : {}),
...(e.model.providerID.startsWith("opencode")
? {
...(k ? { "x-opencode-project": k } : {}),
"x-opencode-session": e.sessionID,
"x-opencode-request": e.user.id,
"x-opencode-client": e.flags.client,
"User-Agent": _i
}
: {
"x-session-affinity": e.sessionID,
"X-Session-Id": e.sessionID,
"User-Agent": _i
}),
...(e.parentSessionID ? { "x-parent-session-id": e.parentSessionID } : {})
}The fetch patch satisfies every variant:
| Header | Value | Purpose |
|---|---|---|
x-opencode-session |
ses_<12hex><14base62> |
Vendor conversation affinity; enables KV-cache prompt routing |
x-opencode-session-id |
ses_<12hex><14base62> |
Required by OpenCode CLI v1.18+ gateways |
x-session-affinity |
ses_<12hex><14base62> |
Generic proxy/relay affinity (Cloudflare AI Gateway, LiteLLM, Portkey) |
x-opencode-parent-session-id |
ses_<parent_hash> |
Hierarchical lineage for DSH subagents (subagent, subagent_fork) |
x-parent-session-id |
ses_<parent_hash> |
Generic proxy parent-session affinity |
User-Agent |
opencode/1.18.34 … |
Prevents Cloudflare WAF Error 1010 challenges |
x-opencode-client |
cli (configurable) |
Identifies the client tier to the Zen gateway |
x-opencode-project |
dynamic / global |
Workspace project attribution for the Console |
When DSH spawns subagents (subagent / subagent_fork), each child runs in a separate session. The plugin inspects the host SessionRegistry for session.header.parentSession, and maps both sessions deterministically through SHA-256:
[Parent DSH Session: "session-abc"] ──(SHA-256)──> [ses_parent_12hex14base62]
│
▼ spawns subagent
[Child DSH Session: "session-xyz"] ──(SHA-256)──> [ses_child_12hex14base62]
Outgoing subagent request:
x-opencode-session: ses_child_12hex14base62
x-opencode-session-id: ses_child_12hex14base62
x-session-affinity: ses_child_12hex14base62
x-opencode-parent-session-id: ses_parent_12hex14base62
x-parent-session-id: ses_parent_12hex14base62
This lineage lets upstream servers optimize prompt caching across agent teams and delegation workflows.
OpenCode uses x-opencode-project to group token usage, requests and cost in the OpenCode Console.
- Enabled (default): the plugin reads the active session's working directory (
session.header.cwd) and sends its folder name —/Users/viz/dev/dsh-opencode→x-opencode-project: dsh-opencode. Outside a project it falls back toglobal. - Disabled: the header is omitted entirely, matching OpenCode CLI's standalone behavior.
- Zero configuration: no project strings to type or manage — attribution follows your workspace naturally.
With the experimental Auto Review mode (@deepseek-ai/dsh-experimental-auto-review), every tool execution is audited by a background model call first:
// @deepseek-ai/dsh-experimental-auto-review
async function classifyRisk(ctx, agent, exec, signal) {
const snapshot = snapshotAutoReview(agent, exec);
const options = deepFreeze({
provider: snapshot.provider,
model: snapshot.model,
system: REVIEW_POLICY,
messages: [
{
role: "user",
content: [{ type: "text", text: reviewUserText(snapshot) }],
},
],
temperature: 0,
signal,
});
return readDecision(ctx.llm.stream(options));
}Because these calls omit options.sessionId, earlier plugin versions short-circuited the stream hook, leaving the turn store empty and sending review requests out unpatched (TRANSPORT: Connection error). The plugin now generates a deterministic fallback turn state whenever sessionId is omitted, so Auto Review streams receive full header injection and the free-tier tool schemas.
┌────────────────────────────────────────────────────────────────────────┐
│ OpenCode API surfaces │
├───────────────────────────────────┬────────────────────────────────────┤
│ V1 inference gateway (data) │ V2 control-plane API (manage) │
├───────────────────────────────────┼────────────────────────────────────┤
│ • https://opencode.ai/zen/v1 │ • https://api.opencode.ai │
│ • https://opencode.ai/zen/go/v1 │ • Local server: @opencode/client │
│ • Static API keys: │ • OAuth token pairs: │
│ - Go: sk-… (subscription) │ { type: "oauth", │
│ - Zen: oc_sk_… (pay-as-you-go)│ access: "…", refresh: "…" } │
│ • Chat completions, models, quota │ • Sessions, tools, workspaces │
└───────────────────────────────────┴────────────────────────────────────┘
Credential isolation (preventing 403 EntitlementError). Go keys (sk-…) carry the subscription entitlement and can query https://opencode.ai/zen/go/v1/usage for rolling, weekly and monthly windows. Zen keys (oc_sk_…) cannot — the Go endpoint answers:
403 {"type":"error","error":{"type":"EntitlementError","message":"OpenCode Go subscription required."}}The plugin isolates them: resolveGoApiKey excludes Zen keys from the Go usage query; if only a Zen key exists, usage discovery reports configured: false and the ring stays hidden instead of spamming 403s; an EntitlementError response is mapped to configured: false or to the Zen credit status.
Go plan overflow to Zen credits. At 100% of the monthly quota, requests only fall back to Zen balance if Use balance is enabled in the OpenCode Console. OpenCode exposes no public balance API (open feature request anomalyco/opencode#10448); overflow is handled server-side:
// OpenCode CLI rate limit handler:
if (e.data.responseBody?.includes("GoUsageLimitError")) {
let y = `${f ? `${f} usage limit` : "Usage limit"} reached... To continue using this model now, enable usage from your available balance`,
k = `https://opencode.ai/workspace/${d}/go`;
return {
message: `${y} - ${k}`,
action: { label: "open settings", link: k },
};
}How does this compare to Duskriver's dsh-opencode-go?
| Capability | dsh-opencode-go |
dsh-opencode-patch (this plugin) |
|---|---|---|
| Role | Standalone Go provider | Universal gateway patch & enhancement layer |
| Intercepted routes | Dedicated Go route only | Any claimed route: opencode, opencode-go, custom relays |
| OpenCode Go models | ✅ (/zen/go/v1) |
✅ (/zen/go/v1) |
| OpenCode Zen models | ❌ | ✅ (/zen/v1 — Claude, GPT-5, Gemini, contributor) |
| Multi-protocol gateway | OpenAI Completions only | Responses + Completions + Anthropic + Google |
| Free-tier tool fallback | ❌ | ✅ Injects read + bash schemas automatically |
| Hierarchical subagents | ❌ | ✅ Parent-session headers injected |
| Dynamic workspace project | ❌ | ✅ Derived from session.header.cwd |
| Auto Review support | ❌ Fails without sessionId |
✅ Fallback turn capture in AsyncLocalStorage |
| Composer dock meter | Text string | SVG ring + Zen pill, session spend, model rate |
| Attached Zen credit | ❌ | ✅ Credentials, env vars, auto-detection |
| Model metadata | models.dev/api.json |
Standard DSH & OpenCode catalog specs |
dsh-opencode-go targets users who only need a standalone Go provider; dsh-opencode-patch is the all-in-one layer that fixes, enriches and meters both Zen and Go across every DSH operation mode. (models.dev is the canonical catalog both draw from.)
OpenCode's gateway GET …/models endpoints frequently return a truncated subset — no display names, context windows, max tokens or input modalities. The plugin ships a stale-while-revalidate catalog for both planes:
- Dual bundled shims (zero latency, offline):
OPENCODE_GO_CATALOGcarries all 29 active Go subscription models with per-million-token rates, so session pricing works before the first refresh;OPENCODE_ZEN_CATALOGcarries the 10 active free-tier models (muse-spark-1.3-contributor-free,space-bunny-free,fledge-alpha-free,nemotron-3-ultra-free,nemotron-3.5-lightning-free,ling-3.0-flash-fin-free,ling-3.1-flash-free,longcat-2.5-preview-free,mimo-v2.6-flash-free,big-pickle) plus flagships (claude-sonnet-4-5,claude-opus-4-7,gpt-5.4,gemini-3.8-flash,qwen3.8-max,kimi-k3). Retired models are excluded so a failed refresh can never resurrect a row the gateway no longer serves — the three Zen-route Muse Spark 1.2 ids are suppressed, while the paid Go 1.2 contributor entry stays (the CLI still lists it). Startup is instant: no cold-start delay, blocking network calls, or airplane-mode failures. - Background revalidation: both catalogs revalidate against
https://models.dev/api.jsonevery 60 minutes (the OpenCode CLI's canonical cycle), merging new models, deprecations and updated limits. Errors degrade gracefully and retain the active catalog. - Gateway models-endpoint enrichment:
patchFetchinterceptsGET …/modelson OpenCode routes and merges the live Go or Zen catalog — human-friendly names (DeepSeek V4.1 Flash,Qwen3.8 Flash,Grok 4.7,MiMo V2.6 Pro), verified context windows (up to 1,000,000+ tokens) and max output tokens (up to 384,000), correct input modalities (text,image), with retired Muse Spark 1.2 rows omitted. - Settings “Fetch Available Models” decoration: DSH asks the route's own adapter first, and for an installed
opencoderoutellm-pi-aianswers from its packaged catalog without calling the gateway. The plugin therefore decorates the hosted discovery result: adapter rows and order are preserved, missing canonical rows (e.g.space-bunny-free) appended, provider-retired rows removed, and models whose protocol DSH cannot speak dropped — offering one could only fail. This is candidate metadata for the settings surface — it never rewrites saved route configuration. - Native model discovery registration: on the host runtime the plugin also registers with
ctx.llm.registerModelDiscoveryforopencode-goandopencode. All three enrichments sit behind the Enrich Models from Models.dev switch.
Behind the Show Session Spend & Model Rate switch (on by default):
- Per-turn accounting: every
llm/streamusage event is priced with the executing model's input/output/cache-read rates from the catalog and accumulated on the host — the client receives only the figures, never the catalog. - Scoped per conversation: the meter sends provider + conversation id, so two open sessions (or a subagent) never read each other's totals.
- Mid-session model switches: the active label and rate follow whatever model runs next, while cumulative spend and the used-model list are preserved.
- Free tiers and plan-included models report
Included in Go Planat$0.00rather than a misleading rate. - Go plan spend is a rate-based estimate of consumption, not an invoice — included usage is covered by the plan. The OpenCode Console remains the billing source of truth.
Different models may route to different accounts (a corporate Go subscription alongside a personal Zen key). resolveRoutedKey(ctx, provider) inspects the loaded Cordis rows for the apiKeyEnv / literal apiKey assigned to each route, derives the account tier (go vs zen) from the key prefix (sk-… vs oc_sk_…), and the meter queries accordingly:
- OpenCode Go (
opencode-go): fixed subscription quotas across three windows (5-hour rolling, weekly, monthly %). At 100% the gateway answersGoUsageLimitError(HTTP 402/429). Server-side overflow into Zen balance works only if that Go account has Use balance enabled (opencode.ai/workspace/go) — a Zen key on a separate account is never debited automatically. - OpenCode Zen (
opencode): per-token pay-as-you-go against the account balance; no rolling windows. At $0.00 the gateway returnsHTTP 402 Insufficient account funds— top up via the Console link in the popover.
Evolved from nobu121/dsh-opencode-session by @nobu121, which pioneered session ID handling for OpenCode on DSH. Extended by @viztor to support Zen free-tier gateway compatibility, hierarchical subagent lineage, dynamic workspace project attribution, live dual-mode Go quota and Zen credit monitoring, and native Web UI integration.
Links: npm · Repository · Issues · Changelog · Contributing · OpenCode · models.dev
Licensed under the MIT License.