Auth Load Balancer
Load-balance opencode requests across multiple Anthropic (Claude) and OpenAI (Codex) OAuth accounts and Kimi Code subscriptions, weighted primarily by weekly usage with drain-before-reset scheduling.
1
438
近 7 天 34
38.4
生态多维模型
12 天前
2026-09-23
快速安装与配置
opencode.json写入当前项目的 opencode.json,只对这个仓库生效。
opencode.json
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["opencode-auth-load-balancer@0.5.0"]
}写入 ~/.config/opencode/opencode.json,对所有项目生效。
~/.config/opencode/opencode.json
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["opencode-auth-load-balancer@0.5.0"]
}若你要在本地改造这个插件,先装到项目里再从本地路径引用。
shell
pnpm add -D opencode-auth-load-balancerOpenCode 启动时会通过内嵌运行时自动加载 npm 依赖并缓存至本地目录,无需手动在全局环境执行安装。
Load-balance opencode across multiple Claude (Anthropic) and Codex (OpenAI/ChatGPT) OAuth accounts and Kimi Code subscriptions so you never have to stop and re-login when one account runs out of quota.
Selection is not round-robin. It is weighted primarily by weekly usage, with a continuous "drain the soonest-resetting account first" rule, per-conversation session affinity (to preserve prompt caching), and a proactive switch before an account hits 100%.
Features
- Account pool — register many Claude / Codex / Kimi Code accounts (Kimi also by API key); the plugin manages and rotates them.
- Weekly-usage-weighted scheduling —
urgency = weeklyRemaining / daysUntilWeeklyReset. A sooner reset (e.g. 3 days) outranks a later one (7 days) at equal headroom; perishable quota is drained progressively, never crammed into the final hour. - Automatic rotation — on
429/auth errors an account is cooled down and the next-best is tried;retry-afteris honored. - Model-tier fallback ladder (Fable, Opus, …) — Claude Max accounts have separate weekly caps per premium model tier. When one is exhausted (a 429 whose
representative-claimnames a tier window, e.g.seven_day_fable/seven_day_opus), the balancer records a per-tier cooldown instead of cooling the whole account down (which used to cascade every account into a false "cooldown" and block every model on it). Requests for that tier then steer to an account with tier headroom — keeping the model you asked for — and only when the whole pool is tier-limited does the request descend one rung down the fallback ladder: the next model family infable → opus → sonnet → haiku(order configurable viaOPENCODE_AUTH_LB_ANTHROPIC_FAMILY_ORDER), picking the highest-versioned model your provider config actually has (a cappedclaude-fable-5prefersclaude-opus-4-9overclaude-opus-4-8). If that tier is capped too, it descends again (fable → opus → sonnet), each step toasted, preferably on the session's pinned account (keeping its prompt cache). Pin a fixed target or disable entirely viaOPENCODE_AUTH_LB_ANTHROPIC_OPUS_FALLBACK_MODEL. - Durable provider pending — after model fallback and account rotation are exhausted, a session-bound turn whose whole provider is blocked by account-wide quota stays pending without sending a guaranteed
429. It resumes when capacity returns, survives an opencode restart as the same user message, and is removed immediately when you cancel withEsc. Sessions wait and resume independently; there is no provider-wide FIFO. - Self-updating Claude Code version — Anthropic gates new models on the client version the request claims, so a pinned version stops working the day a model ships (
Claude Code 2.1.87 does not support this model; version 2.1.251 or newer is required). The plugin discovers the current version from npm instead, caches it for a day, and never regresses below a version known to work. See Claude Code version. - Session affinity — a conversation stays pinned to one account so you keep its prompt cache and don't re-send context on every turn.
- Proactive migration — leaves an account at a configurable soft threshold (~95%) instead of waiting for a hard 100% wall (which can break in-flight subagents).
- Single-use refresh-token safety — per-account singleflight refresh; rotated tokens are persisted immediately.
- Visibility — toasts for account switches, model fallback, pending/recovery, and restart restoration; an on-demand
auth_lb_statustool; and abun run statusCLI dashboard.
Requirements
- Bun ≥ 1.3
- opencode (TUI), with the Anthropic, OpenAI, and/or Kimi For Coding providers available
Install & build
bun install
bun run build # → dist/index.js (a single self-contained file)
The bundle imports only Node built-ins, so it can be dropped into opencode as one file.
Load it into opencode (local / dev)
opencode auto-loads any .ts/.js file in a plugins directory:
.opencode/plugins/— project-level (recommended; unambiguous on every OS)~/.config/opencode/plugins/— global
Copy or symlink the built bundle into your opencode project's plugin dir:
# macOS / Linux — symlink so rebuilds are picked up automatically
mkdir -p .opencode/plugins
ln -sf "$(pwd)/dist/index.js" .opencode/plugins/auth-load-balancer.js
# Windows (PowerShell) — copy (symlinks need Developer Mode / admin)
New-Item -ItemType Directory -Force -Path .opencode\plugins | Out-Null
Copy-Item dist\index.js .opencode\plugins\auth-load-balancer.js
Restart opencode to load it. (opencode does not hot-reload plugins — see the dev loop below.)
Install it from npm (once published)
// opencode.json
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["opencode-auth-load-balancer"]
}
opencode installs the package and its dependencies automatically at startup.
Register your accounts
Run opencode's auth flow once per account:
opencode auth login- Choose "Claude Pro/Max (add account to load balancer)" (or "ChatGPT/Codex …").
- Open the URL, authorize, and paste the result back. For Codex, the browser lands on a
localhost:1455page that does not load — paste either its whole address-bar URL or just thecodevalue from it; both work. - Repeat for every account you want in the pool.
Each login appends to the pool (it does not overwrite opencode's single auth slot). If you already logged in with the upstream opencode-anthropic-auth plugin, that credential is imported automatically on first run.
The pool lives in a JSON file you can inspect/edit (e.g. to rename labels):
| When | Path |
|---|---|
| Default (every OS, incl. Windows) | ~/.local/share/opencode/auth-load-balancer.json |
$XDG_DATA_HOME set |
$XDG_DATA_HOME/opencode/auth-load-balancer.json |
opencode resolves its data dir via xdg-basedir, which is platform-agnostic — it does not use %LOCALAPPDATA% on Windows or Application Support on macOS (verified with opencode debug paths).
Durable turn references live beside the pool as auth-load-balancer-pending.json. This file contains only workspace/provider/session/message ids and scheduling timestamps — never prompt text, request bodies, attachments, headers, OAuth tokens, or provider responses. Both files use atomic writes and cross-process locks.
A third file, auth-load-balancer-cc-version.json, caches the discovered Claude Code version ({"version","fetchedAt"} — see Claude Code version). It holds no account data and is safe to delete; it is rebuilt on the next start.
OpenAI/Codex note: the OpenAI path assumes opencode is configured to use the Responses API (the standard ChatGPT/Codex setup);
/responsesrequests are routed to the Codex backend.
Kimi Code
A Kimi Code subscription joins the pool either through an OAuth sign-in (device code) or as a static API key. The plugin hooks both deployments in opencode's model catalog, each with its own pool: Kimi For Coding (kimi.com) (kimi-code-plan-cn, api.kimi.com/coding/v1, sign-in at auth.kimi.com) and Kimi For Coding (kimi.ai) (kimi-code-plan-global, api.kimi.ai/coding/v1, sign-in at auth.kimi.ai).
opencode auth login→ Kimi For Coding (kimi.com) (or (kimi.ai)), then either:- "Kimi Code (…) (add account to load balancer)" — the sign-in: opencode shows a URL, you approve the sign-in there (the code is already in it), and the login completes on its own. Tokens refresh automatically.
- "Kimi Code (…) API key (add account to load balancer)" — create a key in the console the login links to (kimi.com / kimi.ai) and paste it at the prompt; the CLI labels it "authorization code", but paste the key.
- Repeat for every subscription.
Every login is checked against the account behind it (GET /me), so one subscription stays one pool row: a second key or a sign-in of the same account replaces that row instead of double-counting its quota. A sign-in is stored in opencode as an oauth credential and a key as a plain api one; whichever opencode already stores for the provider is imported on first start. Usage (5h + weekly) comes from /usages alone — Kimi's inference responses carry no quota headers — polled at most every 5 minutes while requests flow. A revoked key (401) or refresh token (invalid_grant) switches the row to re-login instead of being retried, and the TUI's re-login uses the kind of login the row was added with.
The sign-in identifies itself to Kimi's OAuth host the way Kimi's own clients do, with X-Msh-* headers: platform opencode_auth_load_balancer, this machine's host name and OS, and a device id hashed from the host name and home directory.
See which account is in use
Three surfaces, in increasing detail:
- Toast on switch — when the in-use account changes, opencode shows a toast:
Claude account ▶ claude-work · weekly 45% · 5h 10%. auth_lb_statustool — ask the agent to "show auth load balancer status"; it prints the full dashboard in chat.- CLI — run it directly in a terminal:
bun run status
Claude — in use: claude-personal
# account weekly 5h resets state
1 claude-work 45% 10% 2d2h ready
2 ▶ claude-personal 72% 38% 1d6h in use
3 claude-burner 100% - 30m exhausted
Codex — in use: chatgpt-plus
# account weekly 5h resets state
1 ▶ chatgpt-plus 20% 5% 3d18h in use
The workspace-aware auth_lb_status tool also appends durable waits; the standalone TUI layout remains unchanged:
Pending turns
Claude 2 sessions · nearest recovery 3h0m
Codex 1 session · checking again 5m
▶ marks the in-use account; # is the rank the scheduler would pick next.
Persistent bottom status bar (TUI)
A SolidJS TUI plugin renders a persistent bottom status bar (opencode's always-visible app_bottom slot) showing both the in-use account(s) per provider (polled from the pool, e.g. Claude anthropic-1 58% · 5h 12%) and the current session usage — tokens, % of the model context window, and $ cost — computed the same way opencode's own footer does. It is four files:
| File | Role |
|---|---|
tui/auth-load-balancer-tui.ts |
Plugin entry (no JSX). Registered in tui.json (below). Inside tui() it lazily imports the view. |
tui/auth-load-balancer-tui.view.tsx |
The SolidJS view (JSX). Compiled by opencode's TUI runtime Solid transform, whose loader matches *.{tsx,jsx}. |
tui/auth-load-balancer-tui.logic.ts |
Pure, non-JSX pool-file logic (read/normalize the pool file, the sidebar's rename/delete mutations, and the pct/until/winPct/tierResets/stateOf display-formatting helpers) split out of the view so it's directly unit-testable, imported unchanged by .view.tsx. |
tui/auth-load-balancer-scoring.ts |
A byte-identical copy of src/scheduler/score-core.ts (kept in sync by bun run build + a test) so the dashboard ranks accounts with the exact same scorer as the server — never a drifting re-implementation. |
Install the four files into a directory the server does not scan (anything other than plugin/ / plugins/) and register the entry in tui.json:
# macOS / Linux
mkdir -p ~/.config/opencode/tui-plugins
cp tui/auth-load-balancer-tui.ts tui/auth-load-balancer-tui.view.tsx tui/auth-load-balancer-tui.logic.ts tui/auth-load-balancer-scoring.ts ~/.config/opencode/tui-plugins/
# Windows
New-Item -ItemType Directory -Force -Path $env:USERPROFILE\.config\opencode\tui-plugins | Out-Null
Copy-Item tui\auth-load-balancer-tui.ts,tui\auth-load-balancer-tui.view.tsx,tui\auth-load-balancer-tui.logic.ts,tui\auth-load-balancer-scoring.ts $env:USERPROFILE\.config\opencode\tui-plugins\
Then register the entry in ~/.config/opencode/tui.json (this is how opencode loads TUI plugins) and restart:
{
"plugin": [
"file:///absolute/path/to/.config/opencode/tui-plugins/auth-load-balancer-tui.ts"
]
}
Why this shape (it matters): TUI plugins load from the
pluginarray intui.json, not from the server's plugins-dir glob. The server separately globs{plugin,plugins}/*.{ts,js}and loads every match as a server plugin — so a TUI entry dropped inplugins/is also loaded by the server, rejected (must default export … server()), and logs an error on every launch (and a stray scoring.tsthere would be mis-loaded as a plugin and break provider resolution). Keeping the four files OUTSIDEplugins/and registering only viatui.jsonavoids that. The split into a.tsentry that lazily imports a.tsxview is still required because opencode's runtime SolidJS JSX transform (@opentui/solid) only matches*.{tsx,jsx}— a.tscannot itself contain JSX, and the lazy import keeps the server plugin worker (which never runstui()) from evaluating the SolidJS module graph. Developed against opencode /@opencode-ai/plugin>=1.18.26+@opentui/solid0.5.10(the versions currently pinned inpackage.json). The.tsxview is typechecked (tsconfig.tui.json) and linted here — its JSX deps (solid-js+@opentui/*) are installed as devDependencies pinned to those versions — so type/import/prop breaks are caught in CI; only its runtime rendering still needs a live opencode TUI to verify. The toast +auth_lb_statustool +bun run statusCLI cover the same information regardless. (auth-load-balancer-tui.logic.tshas no JSX and no TUI-runtime dependency, so it is directly unit-tested rather than only typechecked.)
Configuration
All knobs are environment variables with sane defaults.
| Variable | Default | Meaning |
|---|---|---|
OPENCODE_AUTH_LB_HOURLY_INFLUENCE |
0.5 |
How much 5h headroom modulates the weekly-urgency score (0–1). |
OPENCODE_AUTH_LB_MIN_RESET_MS |
300000 |
Floor on time-to-reset (caps urgency near a reset). |
OPENCODE_AUTH_LB_WEEK_WINDOW_MS |
604800000 |
Baseline horizon used when a weekly reset time is unknown. |
OPENCODE_AUTH_LB_EXHAUSTED_AT |
0.999 |
Hard exhaustion: at/above this utilization an account is excluded. |
OPENCODE_AUTH_LB_MIGRATE_AT |
0.95 |
Soft threshold to proactively leave a pinned account (before 100%). |
OPENCODE_AUTH_LB_WEEKLY_DRAIN_TARGET |
0.98 |
Soft threshold for the WEEKLY window: scoring treats weekly quota as "fully drained" past this utilization, and a pinned session proactively migrates once its weekly util crosses it. The 5h window uses MIGRATE_AT (~0.95); the weekly window uses this (~0.98). Must be in (MIGRATE_AT, EXHAUSTED_AT]. |
OPENCODE_AUTH_LB_CHEAP_SWITCH_MAX_BYTES |
65536 |
For non-forced (proactive/drain) switches, only switch when the request body ≤ this many bytes, so a grown conversation isn't re-sent onto a fresh (uncached) account — a full per-account prompt-cache write. 0 disables the gate (always switch). A proactive switch bypasses this gate when the pinned account is within ~1% of hard exhaustion (a forced switch is imminent anyway and the context only grows); forced switches always ignore it. |
OPENCODE_AUTH_LB_DRAIN_MIGRATE |
false |
Allow switching a healthy session to drain another account whose weekly window is about to reset. |
OPENCODE_AUTH_LB_DRAIN_MIGRATE_MARGIN |
1.5 |
Urgency factor required to justify a drain switch. |
OPENCODE_AUTH_LB_SESSION_TTL_MS |
21600000 |
Session→account assignments older than this are pruned. |
OPENCODE_AUTH_LB_MAX_WAIT_MS |
305000 |
Bounded wait for requests that do not carry both an opencode session id and user-message id (provider-internal or external fetches). Session-bound user turns use durable pending instead and wait until provider quota recovers or the user cancels. Auth (401/403), disabled/re-login accounts, and network failures are never converted into quota waits. 0 makes anonymous requests fail fast. |
OPENCODE_AUTH_LB_ANTHROPIC_OPUS_FALLBACK_MODEL |
(unset — ladder mode) | Claude Max accounts have separate weekly caps per premium model tier (Fable, Opus, …). When one is exhausted, that tier's requests 429 (anthropic-ratelimit-unified-representative-claim: seven_day_fable / seven_day_opus / …) even though the account's aggregate 5h/7d windows still have headroom and every other model works. Instead of cooling the whole account down (which cascaded every account into "cooldown"), the balancer records a per-tier cooldown: requests for that tier steer to accounts with tier headroom, and once the whole pool is tier-limited they descend the fallback ladder (next family down, best version in your provider's model list; toasted, never silent). Unset = ladder mode (recommended). Set to a model id to pin a fixed downgrade target (bypassing the ladder). Set to an empty string to disable (revert to the account-wide cooldown). The env name keeps OPUS for compatibility but applies to every tier. |
OPENCODE_AUTH_LB_ANTHROPIC_FAMILY_ORDER |
fable,opus,sonnet,haiku |
The fallback ladder's model families, best first. A tier-capped request downgrades to the highest-versioned configured model of the next family below the capped one (e.g. capped fable → newest opus; capped opus → newest sonnet). A family not in the list (a future top tier) is treated as above the first entry — so when Anthropic ships a new premium tier, a config tweak (or nothing at all, if it slots on top) keeps the ladder correct without a code change. |
OPENCODE_AUTH_LB_ANTHROPIC_CLAUDE_CODE_VERSION |
(unset — auto) | Pin the Claude Code version the plugin claims to be, bypassing both the npm lookup and the built-in floor. Anthropic gates new models on this version — asking for a model newer than the version you report is rejected with claude_code_version_too_old — so by default the plugin resolves it automatically, and relearns it from a rejection when the gate moves before npm does (see Claude Code version). A pin also disables that reactive recovery, since the retry would send the version you pinned. Set this only to pin forward (a version npm hasn't tagged latest yet) or back (to reproduce a failure, or if a future release changes the request fingerprint and the newest version starts failing). Must be a plain x.y.z; anything else is ignored. |
OPENCODE_AUTH_LB_DIR |
— | Override the pool-file directory (handy for tests). |
OPENCODE_AUTH_LB_DEBUG |
— | 1/true logs each selection to stderr. |
ANTHROPIC_BASE_URL |
— | Route Anthropic requests through a custom base URL. |
Claude Code version
Anthropic only accepts OAuth (subscription) requests that look like they came from Claude Code, so the plugin reports a version in the claude-cli/<version> User-Agent and in the cc_version= billing fingerprint. That version is a gate, not decoration: requesting a model newer than the version you claim is rejected outright.
{"type":"error","error":{"type":"invalid_request_error",
"message":"Claude Code 2.1.87 does not support this model; version 2.1.251 or newer is required.",
"details":{"error_code":"claude_code_version_too_old"}}}
A hard-coded version therefore breaks on every model launch, so the plugin resolves it, best-first:
OPENCODE_AUTH_LB_ANTHROPIC_CLAUDE_CODE_VERSION— an explicit pin. Skips the lookup entirely.- npm — the
latestdist-tag of@anthropic-ai/claude-code, cached in the data dir for 24 h. - A built-in floor — a version verified to work, used offline and on a cold first run.
The resolved version only ever moves up, so a registry blip or a rolled-back latest can never drag the plugin below a version already known to work; use the env pin to go down deliberately. The lookup is a single ~56-byte request fired at startup: the loader awaits only the local cache read, never the network, so neither opencode's startup nor your first request waits on npm — and a registry outage just leaves the previous version in place.
The gate's own rejection is the fourth route, and the only one that cannot lag. A cache is only as fresh as its TTL, so a model launching inside that window is rejected for as long as the TTL has left to run — precisely when you need the new version. No polling cadence fixes that in general either: Anthropic can gate a model before npm tags the release. So the plugin learns from the failure instead. A claude_code_version_too_old response is parsed for the minimum it demands, npm is asked for the real latest in the same breath (the minimum only clears the model that just failed), the result is cached, and the request is transparently retried once — with the new version in both the User-Agent and the body's cc_version= fingerprint. The turn succeeds instead of failing, and every later request starts from what was just learned.
The retry is spent only when the reported version actually changed, so a 400 for any other reason is passed straight through with its body untouched, and a pinned deployment is unaffected: under an explicit OPENCODE_AUTH_LB_ANTHROPIC_CLAUDE_CODE_VERSION the rejection is returned as-is, because a retry would send the very version you pinned.
Only the version string is discovered. The request fingerprint's salt is still compiled in, so if Anthropic ever changes that algorithm, pin a known-good version with the env var and open an issue.
Development
Scripts
| Script | What it does |
|---|---|
bun run build |
Bundle src/index.ts → dist/index.js + emit .d.ts. |
bun run dev |
Rebuild the bundle on every change (watch). |
bun run typecheck |
tsc --noEmit. |
bun run lint |
Lint with oxlint (DevFive shared config). |
bun run lint:fix |
Auto-fix lint + formatting. |
bun test |
Run the suite with 100% coverage enforced. |
bun run test:watch |
Re-run tests on change. |
bun run status |
Print the dashboard for the current pool. |
Linting & formatting
oxlint with the DevFive shared config — oxlint.config.ts re-exports eslint-plugin-devup/oxlint-config (single quotes, no semicolons, sorted imports, interface over type). A husky pre-commit hook runs bun lint.
bun run lint # check
bun run lint:fix # auto-fix
Testing
bun test
The unit/integration suite runs with coverage gated at 100% (lines and functions) via bunfig.toml. They mock the network and isolate the pool file per test, so no real accounts are needed to test the logic. Tests live in src/__tests__/.
Dev loop (trying it in a real opencode)
opencode loads plugins once at startup, so the loop is:
bun run dev— keepsdist/index.jsrebuilt on every edit.- Symlink
dist/index.jsinto your opencode project's.opencode/plugins/once (see install above). - Edit code → the watcher rebuilds → restart opencode to reload the plugin.
- Use it; watch the toasts, run
bun run status, and setOPENCODE_AUTH_LB_DEBUG=1to log selections.
Project structure
src/
index.ts # plugin entry: 5 exports (Anthropic, OpenAI, Kimi Code ×2, Status-tool)
fetch.ts # load-balanced fetch — the per-request choke point
refresh.ts # singleflight OAuth refresh, invalid_grant handling
accounts.ts # append / bootstrap accounts into the pool
session.ts # derive a stable session key (affinity)
types.ts # provider-agnostic data model (accounts, usage windows, pool file)
usage-merge.ts # fixed weekly-anchor preservation / roll-forward
util.ts # shared helpers (sleep, clamp01, JSON guards)
status.ts # ranked status model + text renderer
notify.ts # toast on account switch
usage-refresh.ts # cold-start usage seeding via the usage endpoint
prime.ts # point the in-use marker at the top-ranked account at startup
pending/ # recovery classifier, reference store, per-turn lease, restart coordinator
scheduler/ # config, score-core (shared scorer), select
pool/ # data-dir resolution + atomic, serialized pool store
providers/ # ProviderAdapter contract + headers
anthropic/ # Claude OAuth + Claude Code request transforms + usage
openai/ # ChatGPT/Codex OAuth + Responses transforms + usage
kimi/ # Kimi Code device-code OAuth + API-key login + /usages (kimi.com + kimi.ai)
cli/status.ts # `bun run status`
__tests__/ # all tests
tui/
auth-load-balancer-tui.ts # TUI plugin ENTRY (registered in tui.json; no JSX)
auth-load-balancer-tui.view.tsx # SolidJS view (lazily imported; app_bottom + sidebar slots)
auth-load-balancer-tui.logic.ts # pure pool-file logic + display helpers (unit-tested)
auth-load-balancer-scoring.ts # byte copy of src/scheduler/score-core.ts (shared scorer)
How it works
opencode lets an auth plugin's loader return a custom fetch that every request for a provider flows through. That single choke point (src/fetch.ts) is where the magic happens, per request:
- capture opencode's session and user-message ids, then derive the session-affinity key;
- pick the session's pinned account — or, if it's unavailable / over the soft threshold, the highest weekly-urgency account (
src/scheduler/select.ts); - refresh the OAuth token if needed (singleflight, rotated token persisted);
- apply provider-specific auth + request transforms (Claude Code identity / Codex Responses quirks);
- send the request, then record usage from the response headers;
- on a model-tier
429, steer across accounts and descend the configured model-family fallback ladder before considering provider pending; - on an account-wide
429/402, cool that account and rotate through the remaining accounts; - only when every usable account is provider-quota blocked, persist the turn reference before waiting — known exhaustion sends no speculative provider request;
- on recovery, retry selection; on
Esc, delete the reference; on restart, reconcile OpenCode's persisted message/history and resume the same message id without duplicating its prompt parts.
The account pool and durable pending references are separate JSON files (opencode's native auth store holds only one credential per provider). Writes are atomic and serialized both in-process and across processes. No daemon runs while opencode is closed; restoration begins only when opencode starts again.
Limitations
- Bottom status bar (
tui/auth-load-balancer-tui.ts+.view.tsx+.logic.ts+auth-load-balancer-scoring.ts) is a SolidJS TUI artifact compiled by opencode (its JSX deps —solid-js+@opentui/*— are installed as devDependencies, so the.tsxview is typechecked viatsconfig.tui.jsonand linted, though not render-tested here since that needs a live opencode TUI; its scorer is a byte-identical copy of the unit-testedsrc/scheduler/score-core.ts, enforced by a sync test). It is written against opencode>=1.18.26internals (theapp_bottomslot + thesubagent-footerusage computation, verified against source); confirm it renders in your opencode build. The toast/tool/CLI cover the account info regardless. - OpenAI/Codex assumes the Responses API; chat-completions → responses conversion is out of scope.
- Kimi Code sign-ins must be approved within 15 minutes, or the login fails and has to be started again. A key imported from opencode's store at startup is keyed by the key itself — the import makes no network call to learn its account — so log that subscription in again before adding it a second way, or it can count twice.
- Plugin reloads require restart: opencode loads server plugins once at process startup. After installing a new build, restart the running opencode process once; durable turns are restored on that next start and are never sent while opencode is closed.
- Cross-process refresh: per-process singleflight protects token rotation within one opencode instance. Running two opencode instances at once could still race the single-use refresh token.
- TUI pool writes: the TUI sidebar's Rename / Delete actions write the pool file atomically (temp + rename) but WITHOUT the cross-process file lock the server uses around its own read-modify-write — so a server usage / cooldown / session /
tokenGenupdate committed between the TUI'sreadFileSyncandrenameSynccan be silently overwritten. Impact is bounded: the next request re-records usage from response headers, so the window is one cycle of staleness on the affected account; correctness recovers on its own. - Live OAuth/login and real-account end-to-end behavior should be smoke-tested in your environment; the test suite mocks the network.
License
MIT
同类生态推荐
Kimi Subscription
opencode-kimi-subscription
Use your Kimi Code (Moonshot) subscription in OpenCode via OAuth — sign in with the device flow, no API tokens.
Claude Subscription
opencode-claude-subscription
OpenCode v2 plugin: use your Claude Pro/Max subscription with OpenCode's built-in Anthropic provider.
Magic Compact
magic-compact
Lossless context compression plugin for OpenCode.