Skip to content

dsh-opencode-quota-router

Verified

dsh-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/usage probe 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-error waterfalls. 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.json with {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 until until.
  • accounts[].exhaustedUntil — the moment that account's window resets; 0 means 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