dsh-opencode-quota-router
Verifieddsh-opencode-quota-router · v1.2.0 · MIT
dsh plugin: multi-account OpenCode Go quota routing with per-model isolation and automatic account failover. Manage accounts from the OpenCode usage Settings page.
Install
dsh plugin add dsh-opencode-quota-router Confirm the layer applied with dsh --profile default --dump-config — see the install guide.
Source
Tags
Creators
Readme
dsh-opencode-quota-router
A DeepSeek Harness (dsh) plugin that spends OpenCode Go
subscriptions before the user's DeepSeek account balance — across several accounts and per model.
OpenCode meters every model against its own monthly limit (5-hour = 20% of it, weekly = 50%, monthly = 100%), so an exhausted DeepSeek V4 Pro budget must not drag DeepSeek V4.1 Flash back to the DeepSeek account. And a spent account must not end the plan while another account still has allowance.
What it does
- Every routed request prefers the OpenCode Go route (
opencode-go) on the active account. - Account pool: accounts are tried in order. When the active account's 5-hour, weekly, or monthly window is spent (or the key stops authenticating), the router parks it until its window resets, switches to the next enabled account, and retries the failed step in the same turn.
- Per-model isolation: when only one model reports exhaustion and the plan still has room, only that model falls back to the DeepSeek account; every other model keeps using the plan.
- Plan-wide fallback: only when no account is usable does every primary model fall back.
- While anything is parked or falling back, a zero-token
GET /inference/go/v1/usageprobe runs every 30 minutes (or at the rolling-window reset) and returns the plan to service automatically. - No session reload: all switches happen at step boundaries through the
agent/request/agent/request-errorwaterfalls. Streaming requests and other agents are never interrupted.
Both listeners register with prepend: true, because a Cordis waterfall returns the outermost listener's
value; an observer appended later would otherwise shadow the rewrite.
How an account switch reaches the wire
pi-ai resolves the provider credential once per stream call, from the reference named by apiKeyEnv
(OPENCODE_API_KEY). There is no per-request key override, and inventing one provider per account would need a
hand-written catalog for each. So the router keeps the keys in the credential store under
OPENCODE_API_KEY_<ID> and mirrors the active account's key into OPENCODE_API_KEY right before the call.
The write happens after the agent/request rewrite and before resolveApiKey, which is exactly the window
where it takes effect — no core change required.
Managing accounts
The UI lives in the sibling plugin dsh-opencode-usage-settings (Settings → OpenCode 用量 → 账户):
status line (正常 / 全量回退 + reason, active account, next probe, 立即探测 / 清空隔离) plus the
account table — masked key, official 5-hour / weekly / monthly percentages, and per-row actions
(设为当前, ↑ / ↓, 停用 / 启用, 改密钥, 删除) and 添加账户 (note + key).
Keeping one page avoids two Settings sections for the same pool, and lets the page show each account's live
quota next to its controls. This package owns the pool and serves GET|POST /opencode-quota-router.json;
the usage page is a client of that endpoint.
GET /opencode-quota-router.json→ the snapshot (accounts, masked hints, statuses, usage, quarantine).POST /opencode-quota-router.jsonwith{action: add|update|remove|activate|move|probe|reset, ...}.
The key is written to the credential store and never echoed back.
The page talks to GET / POST <path>.json on the harness webserver; the snapshot it reads never contains a
plaintext secret (only a mask such as oc_sk_5…_cGU).
Routes
| Primary (OpenCode Go) | Fallback (DeepSeek account) |
|---|---|
opencode-go/deepseek-v4.1-flash |
deepseek-account/deepseek-flash |
opencode-go/deepseek-v4-pro |
deepseek-account/deepseek-v4-pro |
The bundle also declares the opencode-go catalog route (pruned to the models the live endpoint serves) and an
opencode-free route for the free models (beam-free, iceman-contributor-free).
Install
dsh plugin --profile <profile> add dsh-opencode-quota-router
Then add accounts in the Settings page. An existing OPENCODE_API_KEY is adopted as the first account
(OPENCODE_API_KEY_A1) on the first start, so upgrading an existing install needs no re-entry.
Configuration
The bundle patch carries defaults; override the row opencode-quota-router in your profile patch:
- id: opencode-quota-router
config:
usageUrl: https://opencode.ai/inference/go/v1/usage
apiKeyRef: OPENCODE_API_KEY
path: /opencode-quota-router
probeIntervalMs: 1800000
modelQuarantineMs: 1800000
authCooldownMs: 1800000
routes:
- primary: { provider: opencode-go, model: deepseek-v4.1-flash }
fallback: { provider: deepseek-account, model: deepseek-flash }
Optional fields: minProbeDelayMs, unconfirmedDelayMs, modelQuarantineMs, authCooldownMs, statePath.
State
$DSH_HOME/opencode-quota-router.json:
{
"version": 3,
"mode": "primary",
"active": "a1",
"accounts": [
{ "id": "a1", "label": "主账号", "keyRef": "OPENCODE_API_KEY_A1", "enabled": true, "exhaustedUntil": 0, "usage": {} }
],
"quarantine": {},
"updatedAt": 0
}
mode: "fallback"— no account can serve; every primary model falls back.quarantine— one entry per exhausted model; only that model falls back untiluntil.accounts[].exhaustedUntil— the moment that account's window resets;0means usable.
Editing the file is picked up before the next step, so it doubles as a manual override:
{ "version": 3, "mode": "fallback", "until": 1793446953969, "accounts": [ ], "quarantine": {} }
Test
node test.mjs
Exercises routing in both directions, per-model quarantine, quarantine expiry, account failover with key mirroring, the Settings API (add / update / move / reset / remove, secret masking, the never-empty rule), plan-wide fallback, and the "transient rate limit is not a quarantine" rule against a fake Cordis context, a fake credential store, and a local usage endpoint that answers per API key.
License
MIT