teamclaude-cloud
v1.18.0
Published
Multi-account Claude proxy with quota-based rotation + cloud token sync and auth-key control
Maintainers
Readme
TeamClaude Cloud
Multi-account Claude proxy with automatic quota-based rotation for Claude Code — plus a cloud SaaS layer that syncs authenticated tokens to the cloud and controls, per auth key, which accounts each machine may use.
Cloud SaaS (teamclaude-cloud)
The base proxy runs locally. The cloud layer lets you authenticate every account once on one PC, sync those tokens to the cloud, and let other machines pull only the accounts you allow them — via an auth key, without re-authenticating each account per PC.
[Admin PC] teamclaude login ×N → teamclaude cloud push
│
▼
[Cloud] teamclaude.cloud: encrypted tokens (AES-256-GCM) + auth keys (hashed) + key→account mapping
Cloud API: sync push/pull, auth-key issue/revoke/map
▲
│ auth key
[Consumer PC] teamclaude cloud pull --key tck_… → teamclaude serverSecurity model: tokens are encrypted at rest and token storage is server-only (row-level security denies all client roles); auth keys are stored as a SHA-256 hash (for pull-time lookup) and an AES-256-GCM ciphertext under the same key as the tokens, so the owner can re-copy the full teamclaude cloud pull --key … command from the dashboard at any time (owner-only GET /auth-keys/:id/reveal, revoked keys return 409). Keys issued before this change are hash-only and non-revealable — re-issue to get a copyable key. Access control (which key may use which account) is enforced server-side in the cloud API.
Web dashboard
A browser dashboard (signup/login + auth-key issuance and control) is served on Vercel:
https://teamclaude-cloud.vercel.app
Sign up there, then use the same account with teamclaude cloud login on the admin PC. The dashboard shows your synced accounts with their usage (5h session, weekly, and the model-scoped "Fable" weekly window — as measured by the proxy at push time, with the push timestamp shown), toggles accounts globally, manages groups (create/rename/delete, per-group account enable + priority ordering), and issues/moves/revokes auth keys per group. The dashboard is a static SPA (public/) plus a health endpoint (api/health.js); GET /api/health returns service status JSON.
Cloud commands
# Admin PC: authenticate to the cloud, then push local account tokens
# (the proxy's quota snapshot rides along so the dashboard shows 5h/weekly/Fable usage)
teamclaude cloud login --email <[email protected]>
teamclaude cloud push
# Groups: an auth key belongs to ONE OR MORE groups (default "main"); accounts are
# allowed per GROUP, and a key's pull returns the UNION of all its groups' accounts.
# Assign a key to multiple groups from the dashboard (Auth Keys → group checkboxes).
teamclaude cloud group list
teamclaude cloud group create team-a
teamclaude cloud group allow team-a --accounts <uuid1>,<uuid2> # order = priority (#0 first)
teamclaude cloud group rename team-a --to team-alpha
teamclaude cloud group delete team-a # its keys move to "main"
# Auth keys (raw key printed once)
teamclaude cloud key create --label laptop --group team-a
teamclaude cloud key list
teamclaude cloud key move <key-id> --group main
teamclaude cloud key revoke <key-id>
# Global account switch (excluded from EVERY group's pull when disabled)
teamclaude cloud account disable <accountUuid|localName>
teamclaude cloud account enable <accountUuid|localName>
# Consumer PC: pull the key's group's accounts (tokens + priority) into the local config
# (key-only — no `cloud login`; the endpoint is built in). The key is saved so the
# running server keeps syncing on its own.
teamclaude cloud pull --key tck_…
teamclaude serverGroup exclusivity: an account can be enabled in at most one group at a time (DB-enforced with a partial unique index; the API answers 409 account_in_use naming the conflicting group) — so two groups can never pull, and therefore never concurrently use, the same account.
Priority & selection: the group's account order is delivered as each pulled account's priority, which the local proxy's existing selection consumes unchanged — explicit priority first, then the same use-or-lose algorithm (weekly reset soonest, session reset, lowest usage) as always.
Live sync (running server ⇄ cloud). cloud pull stores the endpoint + auth key, so a running teamclaude server re-pulls on an interval (config.cloudSyncIntervalMs, default 30 s; 0 disables) and applies changes without a restart: refreshed tokens are updated in place (freshest-token-wins), accounts added to / removed from the group appear / disappear live, and revoking the key drops all of its accounts and the proxy refuses new requests (it self-heals the moment you re-pull with a valid key). The rotation/quota/failover handling still runs entirely in the local proxy (unchanged algorithm) — the cloud is only the control plane feeding it fresh config. A cloud outage is non-blocking: a transient pull error keeps the last-known accounts and retries next interval; only an explicit 403 (revoked/invalid key) removes accounts.
What revocation does and does not do. A pull hands the consumer both the access and refresh tokens for each allowed account. Server-side controls — revoking the key (it can no longer pull) and disabling/removing the account from the group (it stops being handed out) — only cut off future pulls. They do not invalidate a token a consumer already pulled: that consumer holds the refresh token and can keep minting new access tokens directly against Anthropic until Anthropic itself revokes it (this proxy cannot remotely invalidate an OAuth grant). So treat a pulled key/account as "access granted until the underlying OAuth credential is rotated at the source." The client-side prune (dropping local accounts a key no longer allows, scoped per key) — applied automatically by the running server's live sync (above), or via TUI R / restart — is hygiene/convenience, not a hard revocation boundary. One consumer is expected to use one key.
Sync is freshest-token-wins: a push/pull never overwrites a newer token with an older one (compared by normalized expiresAt), so a token refreshed on any machine wins. Pulled priority applies even when the token itself is kept-local (control-plane vs token freshness). The cloud layer is additive — if the cloud is unreachable the local proxy still runs on whatever accounts are in the local config.
The cloud API lives at auth.teamclaude.cloud — the CLI ships no backing-infrastructure hosts or keys; consumers authenticate with their auth key, admins with cloud login.
Base proxy
Sits transparently between Claude Code and the Anthropic API, managing multiple Claude Max (or API key) accounts and automatically switching when one approaches its session or weekly quota limit.

Features
- Use-or-lose account priority — measures each account once at startup, then prioritizes the account whose weekly (7d) quota resets soonest (then soonest session reset, then lowest usage), so quota about to renew unused is drained first; re-evaluates every 5 minutes and switches immediately when the active account reaches the quota threshold (default 98%). Pin explicit ranks in the TUI (
o) or viateamclaude priorityfor the accounts you want first — everything unranked stays on this automatic (auto) ordering - One account per session, and a cap that never refuses — every Claude Code session (its
x-claude-code-session-id, hashed) is placed on the usable account carrying the fewest sessions and stays there across keep-alive socket changes, so N parallel sessions run on N accounts instead of stacking on one until it hits a limit (sessionSpread: falserestores pure use-or-lose placement). The per-account concurrency cap (maxConcurrentPerAccount, default 3) is a spreading preference: when every usable account is at its cap the request is still sent — to the least-loaded one — and upstream's real limit decides; the old queue-then-429 isconcurrencyCap: "hard", whose 429 now says the fleet is busy (retry-after: 5), not "exhausted" - Instant failover on 429 — an exhausted account (token quota hit) is throttled for its
retry-after(clamped to 1s–5m) and skipped; a rate/concurrency 429 (quota left but hit too fast) fails the request over to another account so concurrent overflow spreads instead of erroring. Either way nothing blocks, and a request-global 429 only passes through after every account has been tried — never throttling the fleet, and never with aretry-afterthe client would give up on (capped at 60 s, see 8 below) - Blocked accounts are labelled, not hidden — a 403 whose body names an account/organization-level block (e.g.
OAuth authentication is currently not allowed for this organization) marks that accountforbidden: out of rotation, reason + remedy shown in the TUI andteamclaude status(re-authenticate withteamclaude login), and back toactiveautomatically once upstream serves it again (a request/probe with quota headers, R, the hourly re-check, or a re-login). Other 403s just fail over — no account state is touched - Interactive TUI — real-time dashboard with color-coded quota bars showing usage %, reset countdowns, an activity log, and keyboard controls (switch, enable/disable, reorder accounts)
- Manual account controls — enable/disable accounts and pin an explicit account order from the TUI or CLI (
teamclaude disable|enable|priority); a disabled account is excluded from rotation while its in-flight requests drain, and everything unranked stays on automatic use-or-lose ordering - Quota survives restarts — per-account quota state and the warm-up probe template are snapshotted to
<config>.quota.json(every minute and on exit) and restored at startup, so a restart doesn't blank the dashboard, blind the account ordering, or leave forced re-measure (R) dead until traffic flows again - Active warm-up — after a (re)start the proxy probes still-unmeasured accounts with a minimal request (reusing the shape of the first real request, restored across restarts), so the whole fleet's quota populates within seconds instead of waiting for traffic to reach each account
- Server lifecycle —
teamclaude stop/teamclaude restartcleanly stop or replace the running server from any terminal - OAuth token management — automatically refreshes tokens nearing expiry and persists them to config; client token refreshes pass through untouched
- Hot-reload accounts — add accounts via
importorloginwhile the server is running, press R to pick them up; R also force-re-measures the whole fleet's quota, so the dashboard reflects usage spent outside this proxy (other devices/sessions) — and works right after a restart, since the probe template is restored from the snapshot - Account deduplication — detects duplicate accounts by UUID and keeps the most recent
- Request logging — optional full request/response logging for debugging
- Codex (ChatGPT) accounts too — pool several ChatGPT subscriptions for the Codex CLI on the same port; they rotate on their own quota, separately from the Claude accounts (see Codex accounts)
- Zero dependencies — uses only Node.js built-in modules
Quick Start
Requires Node.js 18+.
# Install (published to npm — auto-updates via `npm update -g` or `teamclaude update`)
npm install -g teamclaude-cloud
# Add your first account (opens browser for OAuth)
teamclaude login
# Add a second account
teamclaude login
# Start the proxy
teamclaude server
# In another terminal, run Claude Code through the proxy
teamclaude runYou can also import existing Claude Code credentials instead of logging in:
claude /login # Log into an account in Claude Code
teamclaude import # Import its credentialsAdding Accounts
OAuth Login (recommended)
The easiest way to add accounts — opens your browser for authentication:
teamclaude loginUses the same OAuth flow as Claude Code. Auto-detects the account email and subscription tier. Logging in with the same account again updates its credentials.
You can add accounts while the server is running — press R in the TUI to reload.
Import from Claude Code
If you already have Claude Code set up, you can import its credentials directly:
claude /login # Log into an account in Claude Code
teamclaude import # Import its credentialsRe-importing the same account updates its credentials. You can also import from a custom path:
teamclaude import --from /path/to/credentials.jsonCodex accounts
ChatGPT subscriptions (Plus, Pro, Team…) can be pooled for the Codex CLI alongside the Claude accounts. One port serves both: Claude Code posts to /v1/messages, Codex to /backend-api/codex/*, and the path alone decides which pool answers — a Claude account never serves a Codex request and vice versa, and each pool keeps its own active account.
teamclaude login --codex # browser sign-in to OpenAI — repeat once per account
# (a running server picks each one up immediately)
teamclaude restart # the server routes the Codex CLI by itself (no extra step)
codex # now draws from the pool- Routing (automatic) — like Claude Code, the Codex CLI is routed by the running server: while
teamclaude server(orrestart) listens with at least one Codex account in the pool,~/.codex/config.tomlcarries a clearly marked block (model_provider = "teamclaude"+ a[model_providers.teamclaude]table whosebase_urlishttp://127.0.0.1:<port>/backend-api/codex, withrequires_openai_auth = true— without that key the Codex CLI offers noimage_gentool to a custom provider; a block written by an older version is rewritten on the next start), and a graceful stop removes it again — only while it still points at that server's port, so another proxy's route or a hand edit is never touched. Adding or removing a Codex account while the server runs re-syncs it (no restart) — including one the cloud's live sync adds or drops after a dashboard group change; with no Codex account the block stays out (an empty pool would only answer every Codex request with a 503). It never overwrites amodel_provideryou set yourself — the server logsCodex auto-routing skipped (…)instead. Opt out with"codexAutoRoute": falsein the config (the Claude-side"autoRoute": falseturns both off).teamclaude codex on|off|statusremain as manual controls;offremoves exactly that block and nothing else. The previous file is kept asconfig.toml.bak-teamclaude. (OPENAI_BASE_URLdoes not work — a ChatGPT-logged-in Codex ignores it.) - Display — the TUI, the server banner,
teamclaude statusandteamclaude accountsshow Claude and Codex as separate sections, each with its own count and numbering from 1; Claude subscription rows readclaude, Codex rowscodex. Each pool also keeps its own order (oranks#1 …within the section;aresets just that section). - What the proxy changes — only the credential:
Authorization: Bearer <pooled token>andChatGPT-Account-Id. The request body is forwarded byte-for-byte tohttps://chatgpt.com; this is a passthrough, not a translation between the Anthropic and OpenAI protocols. Only the HTTP endpoints the Codex CLI appends tobase_urlare relayed —/responses,/models, and/images/generations·/images/edits(the built-inimage_gentool, so$imagegenworks through the pool — the Codex CLI offers that tool only when it has a ChatGPT login of its own on that PC (codex login); the proxy replaces that login's credential with the pooled account's on every request, but a PC whose Codex accounts come only from the cloud, with no localcodex login, is not offeredimage_gen); anything else under/backend-api/codex/(including/realtime, a WebSocket this HTTP relay can't carry) and any path that merely decodes to a Codex path (encoded slashes,.., backslashes) is refused before a credential is attached. Aset-cookiefrom the ChatGPT backend belongs to the pooled account's session and is not passed back to the client. - Quota — every Codex reply carries
x-codex-*rate-limit headers; they fill the same session/weekly fields as a Claude account, so the 98% switch threshold, use-or-lose ordering (weekly reset soonest first), the TUI bars andteamclaude statusall work unchanged. A spent account-wide window throttles the account and fails the request over to the next Codex account; a spent model-scoped bucket only fails that request over. Idle Codex accounts are measured at startup, every warm-up interval, and on R through the read-only usage endpoint (/backend-api/wham/usage, what Codex's/statusshows) — no inference request, no quota spent. - Tokens — a local Codex login is refreshed against
auth.openai.comshortly before it expires (and once on a 401). - Cloud sync —
cloud push(and the dashboardpkey) uploads Codex logins made withteamclaude login --codexalongside the Claude ones, andcloud pullhands them to every PC in the key's group, exactly like a shared Claude account: once shared, a Codex login is refreshed only by the cloud (one rotation, against OpenAI) and every PC takes the result — a PC never rotates a shared token itself. A Codex login imported from the Codex CLI's own~/.codex/auth.json(or an entry with no stored token, which reads that file) is never uploaded — the Codex CLI rotates that refresh token on its own and would kill the cloud copy for everyone;cloud pushnames it and skips it. The cloud knows a Codex account by a UUIDv5 of its ChatGPT account id + user id, recomputed server-side from the uploaded token's own claims after checking the token's OpenAI signature (a push naming another identity, or carrying a token OpenAI didn't sign, is refused). A PC running TeamClaude older than 1.15.0 never receives Codex rows (it would treat them as Claude accounts). On a PC where the same ChatGPT login is pooled from the Codex CLI file, the cloud copy is left alone (that PC keeps using the CLI's login). The dashboard shows Claude and Codex accounts in separate sections; groups, keys, enable/disable and usage work the same for both. A Codex account counts toward the plan's account limit like any other. - Pooling the login Codex already has —
teamclaude import --codex [--from ~/.codex/auth.json]adds that login without a browser. It shares the refresh token with the Codex CLI, and OpenAI refresh tokens are single-use, so TeamClaude re-reads the file before every refresh and follows a rotation the CLI made (it takes the file's tokens only when they are newer than its own); TeamClaude never writes that file, so if it rotates first, the CLI may later needcodex login. A separateteamclaude login --codexfor the same account never conflicts and is the safer choice. - Identity — a Codex account is its ChatGPT account (workspace) id and user id, both required. Re-signing in as the same person updates the entry; another member of the same workspace becomes a separate entry and never overwrites yours; a login that doesn't state both ids is refused (it can't be told apart from a teammate). A pooled login file that now holds someone else is ignored, not adopted.
- Names — an account defaults to its login email; if a Claude account already uses that name the Codex one becomes
<email>#codex, soenable/disable/priority <name>stay unambiguous.
API Key
For Anthropic API key accounts (billed via Console):
teamclaude login --apiUsage
Start the proxy server
teamclaude serverWhen running from a TTY, shows an interactive TUI with:
- Account table with numbered rows and session/weekly quota progress bars (usage % overlaid, plus a reset countdown when space allows); wide terminals add a third
Fblbar with the model-scoped weekly limit (the separate "Fable" weekly limit from Claude's usage UI). Ranked accounts are listed first, then theautoaccounts in their actual drain order (weekly reset soonest first) - Real-time activity log with request tracking
- Keyboard shortcuts (see below)
Falls back to plain log output when not a TTY (e.g. running as a service).
If the configured port is already in use — for example another TeamClaude proxy is already running — the server prints a clear message and exits instead of crashing with an unhandled error. Inspect the existing one with teamclaude status, or find the listener with lsof -nP -iTCP:<port> -sTCP:LISTEN.
TUI Keyboard Shortcuts
| Key | Action |
|-----|--------|
| ↑/↓ | Move the selection cursor over the accounts |
| s | Switch active account (to the selected one) |
| e | Enable / disable the selected account |
| o | Order the selected account within its pool (Claude / Codex): ↑/↓ move its rank, a resets that section's WHOLE order to auto (weekly-reset ordering), c clears just this account's rank |
| a | Add account (import or API key) |
| d | Delete an account (with confirmation) |
| R | Reload accounts from config and re-measure every account's quota — revives lapsed OAuth tokens first, includes the model-scoped Fable window, and reports an honest M/N when some accounts fail or are skipped |
| q | Quit |
In selection mode, use j/k or arrow keys to navigate, Enter to confirm, Esc to cancel.
Stop / restart the server
teamclaude stop # SIGTERM the running server (escalates to SIGKILL if needed)
teamclaude restart # stop the running server (if any) and start a fresh oneThe running server is discovered via its state file (<config>.server.json), which records its configuration, port, PID, and instance identifier. stop/restart verify that ownership before sending a signal, including after a config port change. Every profile also probes its configured port, so a running server is still found when its state file is missing; if its ownership can't be verified it is reported but never signalled, so one profile cannot stop another profile's server. Quota state is restored on restart (see below), so a restart doesn't lose the dashboard.
Only the main configuration automatically sets the global Claude API address by default. A different TEAMCLAUDE_CONFIG or XDG_CONFIG_HOME must explicitly set autoRoute: true to opt in; test servers otherwise leave the global address untouched.
Note: if a Claude Code session is itself routed through the proxy (
teamclaude run), runningteamclaude stopinside that session severs its own API connection (Unable to connect to API (ConnectionRefused)). Stop or restart the proxy from a separate terminal instead — withrestart, an in-flight session recovers on its own retries.
Account order & manual controls
By default every account is on auto ordering (use-or-lose: weekly reset soonest is drained first). You can layer manual controls on top:
teamclaude disable <name> # exclude from rotation (in-flight requests drain)
teamclaude enable <name> # re-enable
teamclaude priority <name> <n|auto> # pin explicit order (lower = preferred); "auto" clears itIn the TUI, ↑/↓ select an account, e toggles enable/disable, and o grabs the selected account into order mode: ↑/↓ move its rank, a resets the WHOLE order of that account's section (Claude or Codex) back to auto, c clears just that account's rank, Enter/Esc done. Ranked accounts render as #1 #2 … and are preferred first; everything unranked stays on the automatic ordering — so you can pin a few accounts and let the rest rotate.
CLI changes made while the server is running are picked up with R (reload) in the TUI or teamclaude restart.
Run Claude Code through the proxy
teamclaude runOr manually set the environment:
eval $(teamclaude env)
claudeOther commands
teamclaude accounts # List accounts with subscription tier and token status
teamclaude accounts -v # Also show token expiry times
teamclaude status # Show live proxy status (requires running server)
teamclaude stop # Stop the running proxy server
teamclaude restart # Stop the running server and start a fresh one
teamclaude remove <name> # Remove an account
teamclaude disable <name> # Disable an account (excluded from rotation)
teamclaude enable <name> # Re-enable a disabled account
teamclaude priority <name> <n|auto> # Pin selection order (lower = preferred; "auto" clears)
teamclaude api <path> # Call an API endpoint with account credentials
teamclaude help # Show all commandsRequest logging
Log full request/response details to a directory (one file per request):
teamclaude server --log-to /tmp/requestsConfiguration
Config is stored at ~/.config/teamclaude.json (or $XDG_CONFIG_HOME/teamclaude.json). A random proxy API key is generated on first use.
Override the config path with TEAMCLAUDE_CONFIG:
TEAMCLAUDE_CONFIG=./my-config.json teamclaude serverConfig format
{
"proxy": {
"port": 3456,
"apiKey": "tc-xxxxxxxxxxxxxxxxxxxxxxxx"
},
"upstream": "https://api.anthropic.com",
"switchThreshold": 0.98,
"accounts": [
{
"name": "[email protected]",
"type": "oauth",
"accountUuid": "...",
"accessToken": "sk-ant-oat01-xxxx...",
"refreshToken": "sk-ant-ort01-xxxx...",
"expiresAt": 1774384968427,
"enabled": true,
"priority": 0
}
]
}| Field | Description |
|-------|-------------|
| proxy.port | Local port the proxy listens on |
| proxy.apiKey | API key clients use to authenticate with the proxy |
| upstream | Upstream API base URL |
| switchThreshold | Quota utilization (0–1) at which an account is considered full and skipped |
| reevalIntervalMs | How often (ms) to re-rank accounts by priority while the active one is healthy (optional, default 300000 = 5 min). Set to 0 to disable the timer entirely — the active account then only changes when it becomes unavailable or via per-request 429 failover |
| activeWarmup | Probe unmeasured accounts after a restart to populate quota (optional, default true) |
| warmupIntervalMs | How often (ms) the active warm-up re-probes accounts whose quota window reset (optional, default 300000 = 5 min; 0 = startup-only) |
| exhaustedHoldMs | When every account of a pool is quota-blocked, how long (ms) a request is held waiting for one to recover before a 429 is answered (optional, default 120000 = 2 min — under Claude Code's first-byte window; 0 = answer 429 at once, the previous behaviour) |
| exhaustedReprobeMs | While requests are held, how often (ms) the blocked accounts are re-probed upstream for a fresher verdict (optional, default 60000; 0 = rely on throttle/reset timers only) |
| exhaustedLastResort | When nothing is available, try an account that is over the switch threshold but whose last upstream verdict was still allowed instead of refusing (optional, default true). A reading restored from the quota snapshot after a restart does not count as a verdict while the re-probe can refresh it — the probe speaks first, then the account is tried |
| accounts[].enabled | Set false to exclude the account from rotation (optional, default true) |
| accounts[].priority | Explicit selection rank (lower = preferred first; optional — unset means automatic use-or-lose ordering) |
How It Works
- Claude Code connects to the local proxy instead of
api.anthropic.com - The proxy selects an account and forwards the request with that account's credentials. A request that states a Claude Code session (
x-claude-code-session-id, ormetadata.user_id) keeps its session on one account — a new session is placed on the usable account with the fewest sessions (then fewest in flight, then the use-or-lose order below), so parallel sessions spread over the fleet instead of stacking on one account (sessionSpread: falseturns the placement off; the session still sticks to its account). The per-account concurrency cap is a preference: when every usable account is at its cap the request is still sent to the least-loaded one (concurrencyCap: "soft", default) rather than queued and refused ("hard") - OAuth tokens expiring within 5 minutes are automatically refreshed and persisted to config
- Rate limit headers from the API (
anthropic-ratelimit-unified-*) track session (5h) and weekly (7d) quota utilization. Model-scoped weekly windows (7d_oi— the separate "Fable" weekly limit) are tracked and displayed too, but never affect routing: an account over its Fable weekly limit still serves every other model - Cold-start warm-up: quota is only known after a request flows through an account, so at startup the proxy first routes requests to any unmeasured account until every account has been measured once. An active warm-up additionally probes unmeasured accounts directly — a minimal 1-token request reusing the shape of the first real request — so the whole fleet is measured within seconds of the first post-restart request instead of waiting for traffic to reach each account (
activeWarmup: falsedisables it). Then account selection becomes use-or-lose: among accounts still under the threshold, it prefers the one whose weekly (7d) quota resets soonest (tie-breaks: soonest session reset, then lowest usage), so quota about to renew unused is drained first. Explicitly ranked accounts (priority/ TUIo) are preferred before all of that; disabled accounts are excluded entirely. The active account stays sticky to keep its prompt cache warm; priority is re-evaluated everyreevalIntervalMs(default 5 min; set0to disable timer-based switching), and on reaching the threshold it switches immediately to the next-highest-priority account - On a 429 the proxy classifies it (never sleeping holding the client connection):
- Account-quota exhaustion (upstream reports the account is over its limit) → marks that account rate-limited for its
retry-after(clamped to 1s–5m) and immediately re-dispatches to the next available account. If every account is throttled it returns 429 with a computedretry-after. (This also keeps cold-start warm-up fast: an exhausted account is skipped in one round-trip.) - Rate/concurrency or transient 429 (account has token quota left but was hit too fast, or a transient limit) → the request fails over to another available account (per-request, without throttling the account), so concurrent overflow spreads to an idle account instead of erroring. If every account has been tried for the request (→ effectively global), the 429 is passed through — still without throttling any account, so the fleet isn't poisoned. Its
retry-afteris capped at 60 s exactly like the exhausted-fleet 429 in 8 (upstream's own value, up to 5 min, would end the turn — a fleet-wide Fable weekly 429 did exactly that); the honest estimate — the request model's weekly reset when the accounts are marked for it, else upstream's value — travels inx-teamclaude-recovery-estimateand the message, and the client's retry is then held and re-probed like any exhausted-fleet request.
- Account-quota exhaustion (upstream reports the account is over its limit) → marks that account rate-limited for its
- Transient network errors (connection reset, timeout) drop the connection so the client can retry
- If all accounts are exhausted, the proxy recovers on the client's behalf instead of failing the turn. Claude Code abandons a 429 whose
retry-afterexceeds 60 s (API Error: Server is temporarily limiting requests … All 13 accounts exhausted. Retry in 672s— and nothing retries), and the proxy's stored "exhausted" readings are only as fresh as each account's last response. So the request is held (up toexhaustedHoldMs, default 2 min) while three things can free an account: a throttle lapsing or a quota window resetting (picked up by a half-second poll), a re-probe of the blocked accounts upstream everyexhaustedReprobeMs(a freshallowedverdict drops a stale throttle at once — the same minimal probe as warm-up / R), and a last resort: an account over the proxy's switch threshold whose last upstream verdict was stillallowedis tried rather than refused (the threshold is a proactive switch point, not upstream's limit — upstream decides with a real 429, which throttles it). Right after a restart the restored snapshot readings carry no verdict, so the re-probe runs first (one 1-token probe per blocked account, in parallel) and the last resort is judged on its fresh answer — the client's request is never used as the probe account by account. An account that comes back during the hold and is refused again does not end the request while the hold still has time (at most two attempts per account per request). Only when the hold runs out is a 429 answered, and itsretry-afteris capped at 60 s so Claude Code's own retry comes back and is held again; the honest soonest-reset estimate (the real unified 5h/7d or standard reset of whichever over-threshold window is binding — or, for the request's model, its model-weekly7d_oireset) travels inx-teamclaude-recovery-estimateand the message. A merely concurrency-capped (but quota-healthy) fleet is never refused by default (concurrencyCap: "soft"sends over the cap); under"hard"it waits on the overflow queue and its 429 says the fleet is busy ("at their concurrency cap",retry-after: 5), never "exhausted" — the 2026-10-13 report ofAll 13 accounts exhausted. Retry in 60swas exactly a busy fleet (3 usable accounts × cap 3) mislabelled - Quota survives restarts: the server snapshots per-account quota/throttle state — plus the committed warm-up probe template — to
<config>.quota.json(every minute and on exit) and restores both at startup, so a restart doesn't blank the dashboard, blind the use-or-lose ordering, or leave warm-up probes and forced re-measure (TUI R) without a known-accepted request shape until traffic flows again. A restored template is provisional: the first freshly accepted request shape replaces it (the snapshot's model may have been retired since). Expired windows are swept lazily and re-measured from live traffic - Client token refresh requests (
/v1/oauth/token) are relayed to upstream untouched — the proxy and client manage their own token lifecycles independently
License
MIT
