One local proxy. Every coding account. No interrupted sessions.
Run Claude Code and OpenAI Codex CLI through independent multi-account pools
with quota-aware routing, instant failover, and a live terminal dashboard.
Quick Start · Codex Setup · Dashboard · Architecture
Note
Claude and Codex use separate configs, ports, and account pools. Both proxies can stay online together, while Codex CLI and Hermes Agent keep using one stable local endpoint.
npm i -g teamcodex
teamcodex import # pick up your existing Claude Code login
teamcodex codex import # pick up your existing ~/.codex/auth.json
teamcodex server # start the proxy, then `teamcodex run`The command is teamcodex. This package deliberately does not install a teamclaude
binary so it cannot collide with upstream's package of that name.
Prefer installing straight from the repository? npm i -g github:sangrokjung/teamclaude
works too and always tracks the default branch.
No. This does not share, resell, or pool accounts between people.
It routes your own authenticated sessions from one machine, which is exactly what you would do by switching accounts by hand, minus the manual re-login. Every request is signed with that account's own OAuth token, and nothing is proxied on behalf of third parties. Credentials are stored locally and only ever sent to the vendor's own endpoints, exactly as the CLI would send them. No third party sees them.
It does not increase your quota and it does not bypass any limit. It stops the quota you already paid for from expiring unused.
Worth noting: Claude Code's own /extra-usage flow already offers to sign into a
different account you own when you hit a limit. "Switch to another of my accounts
to keep working" is something the first-party client itself surfaces. This automates
that same switch instead of making you click through it by hand.
If you work in a team, every member still authenticates with their own subscription. Using this to let several people share a single seat is not supported, and if a vendor states that this class of tool is disallowed, this project will be changed or retired accordingly.
Fork chain: KarpelesLab/teamclaude → jung-wan-kim/teamclaude → this repository. The fork badge at the top of this page shows the immediate parent, which is why it reads jung-wan-kim rather than the original author.
This started as a fork of KarpelesLab/teamclaude, which does the Claude side very well and is worth using on its own. This fork went a different direction when it needed Codex (ChatGPT OAuth) account pooling, which upstream does not cover, plus a model fallback chain and network-level failover. Upstream has features this fork does not, so pick whichever fits your setup.
Actual TeamCodex TUI layout rendered with sanitized demo accounts.
AI coding subscriptions have independent session and weekly limits. A long-running terminal should not die just because one account reaches its cap. TeamClaude and TeamCodex keep the client connected to a stable local endpoint and move new work to the best available account automatically.
|
⚡ Seamless failover Switch accounts on quota, rate, network, or upstream failures without changing the client command. |
🧭 Quota-aware routing Spend the account whose weekly allowance resets soonest, preserving quota that would otherwise expire unused. |
|
🧠 Cache-friendly affinity Keep sequential turns on the same account while spreading concurrent overflow across the pool. |
🖥️ Operable by humans Inspect usage, switch accounts, disable unhealthy entries, and reorder priorities from the TUI. |
What the QJC resilient-routing fork adds
This is the qjc/resilient-routing fork, published to npm as teamcodex, of
jung-wan-kim/teamclaude, based on
upstream v1.2.3.
- Codex subscription pooling with isolated official OAuth login sessions.
- Top-tier weekly-window model routing for model-scoped
7d_oiquota. - Model fallback chains through configurable
modelFallbacks. - Bounded graceful shutdown for reliable launchd/systemd restarts.
- Method-aware network recovery with bounded failover for replay-safe requests and no hidden replay of ambiguous POSTs.
- Host CPU and RAM tracking in status JSON, CLI output, and the TUI.
- Hermes Agent compatibility through the stable TeamCodex endpoint.
Install this fork with one command:
npm install -g teamcodexFrom a local checkout, prefer
npm pack && npm install -g ./teamcodex-<version>.tgz. A plain
npm install -g <dir> symlinks the checkout, which can break supervisors that
cannot read that path.
- 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 - Codex subscription pooling —
teamclaude codex ...manages a separate ChatGPT OAuth account pool, injects each account's bearer token andChatGPT-Account-ID, tracks the officialx-codex-primary-*/x-codex-secondary-*windows, and fails exhausted requests over to the next Codex subscription - 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) tries up torateLimitFailoversalternate accounts so concurrent overflow spreads instead of erroring. After that budget, transient/global 429s keep the original model, never throttle the fleet, and are retried internally within the bounded continuity deadline - Interactive TUI — real-time dashboard with numbered account rows, 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 — general 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. Model-scoped usage is deliberately not restored: every Fable/Mythos window starts unknown and is re-measured from runtime traffic - Account-first Fable/Mythos routing — only an account with a fresh, finite, full model-scoped window is skipped for that top-tier request. Any generally available account (enabled, auth-healthy, and under its general 5h/7d limits) whose model window is unknown, expired, or ready keeps the original model eligible; Opus, Sonnet, and Haiku eligibility is never removed by a Fable/Mythos window
- Active warm-up — after a (re)start the proxy probes eligible unmeasured accounts with a minimal request (reusing the last accepted request shape), so response-derived quota data populates without waiting for normal 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 best-effort re-measures every idle account, including disabled accounts for display, and reports an honestM/N - Account deduplication — detects duplicate accounts by UUID and keeps the most recent
- Request logging — optional full request/response logging for debugging
- Host CPU / RAM tracking — live host CPU%, 1/5/15-min load average, and RAM usage in the TUI header,
teamclaude status, and the/teamclaude/statusJSON (hostfield); measured with Node built-ins only - Zero dependencies — uses only Node.js built-in modules
Requires Node.js 18+.
# Install from npm
npm install -g teamcodex
# 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 runImportant: a running proxy does not automatically capture a plain
claudeprocess. Start Claude Code withteamclaude run; otherwise it connects directly with its single logged-in account and cannot rotate when that account reaches a usage limit.
You can also import existing Claude Code credentials instead of logging in:
claude /login # Log into an account in Claude Code
teamclaude import # Import its credentialsCodex uses a separate config (~/.config/teamcodex.json) and port (3457), so
the Claude and Codex proxies can run at the same time.
# Add accounts with isolated official Codex OAuth sessions.
# Each login uses a temporary CODEX_HOME, so its refresh token is owned only by
# TeamCodex after import and cannot race the normal ~/.codex/auth.json.
teamclaude codex login --name codex-pro-1
teamclaude codex login --name codex-pro-2
# Start the Codex proxy
teamclaude codex server
# In another terminal, run the interactive Codex CLI through the account pool
teamclaude codex run
# Non-interactive Codex commands are forwarded after `--`
teamclaude codex run -- exec "summarize this repository"You can import the account currently logged into the official Codex CLI instead:
codex login
teamclaude codex import --name codex-pro-1The isolated teamclaude codex login flow is recommended. A direct import copies
the same rotating refresh token used by ~/.codex/auth.json; running plain
codex afterward can rotate that token outside the proxy. If that happens,
re-import the account or log it in again through teamclaude codex login.
teamclaude codex run starts an HTTP-only Responses provider that still uses
Codex's first-party ChatGPT auth path (requires_openai_auth = true) and
redirects chatgpt_base_url to the local proxy. This preserves the
subscription-only model catalog while preventing the default Responses
WebSocket from bypassing the HTTP proxy. The proxy discards the client's
incoming bearer token and account ID before forwarding, then injects the
selected pool account's credentials. The official Codex CLI must still have a
normal ChatGPT login to initialize its first-party auth path, but that
credential is never forwarded upstream by TeamCodex.
Codex usage is learned from response headers as traffic flows, so newly added accounts show unmeasured quota until each account handles a request.
Common controls mirror the Claude pool:
teamclaude codex status
teamclaude codex accounts
teamclaude codex disable codex-pro-1
teamclaude codex enable codex-pro-1
teamclaude codex priority codex-pro-2 0
teamclaude codex restartKeep the Codex proxy running and point Hermes at the stable local endpoint:
# ~/.hermes/config.yaml
model:
default: gpt-5.6-sol
provider: openai-codex
base_url: http://127.0.0.1:3457If Hermes has openai-codex entries in its credential pool, set each entry's
base_url to the same local endpoint as well. Restart the Hermes gateway after
changing its configuration. Hermes keeps talking to one stable URL while
TeamCodex selects, refreshes, and rotates the upstream Codex account.
Run teamclaude codex server in a TTY to open the Codex account dashboard.
It uses the Codex config and port independently from the Claude dashboard, so
both proxies can stay online at the same time. For a non-interactive health
check, use teamclaude codex status.
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.
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.jsonFor Anthropic API key accounts (billed via Console):
teamclaude login --apiteamclaude 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.
| 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: ↑/↓ move its rank, a resets the 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 best-effort re-measure idle accounts — includes disabled accounts for display, skips busy and auth-error accounts, and reports an honest M/N; Fbl refreshes only when the probe template returns a model-scoped weekly header |
q |
Quit |
In selection mode, use j/k or arrow keys to navigate, Enter to confirm, Esc to cancel.
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) with a port-probe fallback, so stop/restart work from any terminal — even after a config port change. Quota state is restored on restart (see below), so a restart doesn't lose the dashboard.
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.
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 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.
teamclaude runteamclaude run injects the proxy URL when the Claude Code process starts and
removes inherited ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN values so Claude
Code keeps its Max/Pro OAuth subscription instead of silently preferring API
credits. A CLAUDE_CODE_OAUTH_TOKEN, when intentionally supplied, is preserved.
If the proxy is not running, teamclaude run now starts it in the background
and waits for the listener before launching Claude Code. The server keeps that
public listener in a supervisor process, so a crashed proxy worker is replaced
while new connections wait instead of failing with ConnectionRefused.
With autoResumeClaude: true, the launcher gives a new Claude Code conversation
an explicit session ID and watches only that transcript for terminal API errors.
Request timed out and terminal rate/overload errors restart the same conversation
as --resume <session-id> continue, up to claudeAutoResumeMaxRetries, so an
interactive session does not wait indefinitely at the prompt for a person.
codexFallbackOnExhaustion: true additionally hands the conversation to TeamCodex
only when every enabled Claude account has a fresh, finite general quota window
at or above switchThreshold. A Fable-only 7d_oi exhaustion, a transient queue,
or unknown/partial quota evidence cannot trigger the provider switch. The launcher
writes a credential-protected, provider-neutral transcript summary under
~/.config/teamclaude-handoffs/, stops the Claude child, and starts one Codex CLI
with that handoff. Tool inputs and tool results are excluded from the handoff.
When launchModel is configured, teamclaude run also checks the proxy's latest
quota status before launch. It starts Claude Code directly on the first configured
fallback only when every generally available account has a fresh, measured,
finite model-scoped window that is full. An unknown or expired window preserves
the configured top-tier model so startup cannot downgrade before the fleet is
measured. For claude-opus-4-8, the launch argument is rendered
as claude-opus-4-8[1m], so Claude Code's header and /status show the model and
context window actually serving the session. An explicit non-fallback --model
or ANTHROPIC_MODEL remains authoritative.
Starting or restarting TeamClaude later does not reroute an already-open
direct session, which can still show "out of usage credits" for its single
logged-in account while the proxy itself is healthy.
Exit that direct session and resume it through TeamClaude from the same working directory:
teamclaude run -- --continue
# Or resume a specific conversation:
teamclaude run -- --resume <session-id>Or manually set the environment:
eval $(teamclaude env)
claudeUnable to connect to API (ConnectionRefused) means Claude Code could not reach
the local TeamClaude supervisor. New sessions started with teamclaude run
automatically start a missing supervisor; a worker-only crash keeps the listener
bound and is recovered automatically. If the supervisor itself was stopped,
check it from a separate terminal:
teamclaude status
lsof -nP -iTCP:3456 -sTCP:LISTEN
teamclaude restart
# Resume the conversation through the recovered proxy
teamclaude run -- --continueThe PID shown by lsof is the stable TeamClaude supervisor. Its worker PID may
change after a crash without creating a no-listener window. Do not run
teamclaude stop from inside the affected proxied Claude Code session.
teamclaude status prints a Host: line for the machine the proxy runs on:
Host: CPU 43.3% (load 18.99 / 16 cores) RAM 63.2GB/64.0GB (98.8%)
CPU% is measured between two status calls (the counters are cumulative), so the
very first call after a server start shows -. The TUI header shows the same
numbers compactly (CPU 43% · RAM 98% · Port 3456 ▲), turning yellow at 70%
and red at 90% — a host drowning in Claude Code sessions kills the proxy along
with everything else, so it gets a gauge right next to the quota bars. The raw
values (including the full 1/5/15-minute load-average triple and byte counts)
are in the host field of GET /teamclaude/status.
teamclaude status and the TUI show the latest quota headers observed by the
proxy, not an on-demand account usage query. They can lag usage spent in another
Claude Code session or on another device. Pressing R best-effort re-probes
eligible idle accounts and refreshes only the headers returned by the captured
probe template. Disabled accounts are included for display without being
re-enabled; busy and auth-error accounts are skipped. Fbl can remain stale
until a top-tier request shape returns the model-scoped weekly header.
Current Claude Code OAuth builds also expose their own account usage view. It uses an OAuth-only implementation surface rather than a documented general Anthropic API, so TeamClaude does not depend on it for routing. When the views disagree, treat Claude Code's account usage view as the live account check and TeamClaude as the proxy's last observation.
Every applicable window matters. A Fbl bar below 100% does not mean Fable can
run when the general 5-hour or 7-day window is already at switchThreshold.
For example, 59% Fable usage with 99% 5-hour usage is unavailable at the default
98% threshold until the 5-hour window resets.
The per-account Total: … tokens counter folds all three input families —
input_tokens (uncached prompt), cache_creation_input_tokens, and
cache_read_input_tokens — plus output tokens. Anthropic's input_tokens
excludes the prompt cache, and Claude Code keeps almost the whole context cached,
so counting input_tokens alone made the total accumulate only a few hundred
tokens per request; sumInputTokens in server.js sums the cache fields so the
displayed total reflects real volume (qjc fork).
QJC account-first guard:
modelWeeklyis response-derived and is not restored after restart, so each model-scoped window starts unknown. At runtime, a fresh, finite window at or aboveswitchThresholdskips only that account for the matching Fable/Mythos request. If any generally available account has an unknown, expired, or ready window, the original top-tier model remains account-first; a cached-fleet fallback is allowed only when every such account is fresh, measured, and full. Runtime evidence can also trigger fallback only after a labeled model-tier 429 reaches every eligible account. Unlabeled/global 429s keep the original model. These windows never remove an account's Opus/Sonnet/Haiku eligibility. If a global installation carries additional local patches, validate or reapply them in the service startup path because an npm or Node upgrade can replace globally installed source files.
teamclaude accounts # List accounts with subscription tier and token status
teamclaude accounts -v # Also show token expiry times
teamclaude status # Show the proxy's last-observed quota 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 commandsLog full request/response details to a directory (one file per request):
teamclaude server --log-to /tmp/requestsConfig 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 server{
"proxy": {
"port": 3456,
"apiKey": "tc-auto-generated-key"
},
"upstream": "https://api.anthropic.com",
"switchThreshold": 0.98,
"accounts": [
{
"name": "user@example.com",
"type": "oauth",
"accountUuid": "...",
"accessToken": "sk-ant-oat01-...",
"refreshToken": "sk-ant-ort01-...",
"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) |
tokenRefreshIntervalMs |
How often (ms) to refresh expiring OAuth tokens across the fleet, including disabled accounts (optional, default 300000 = 5 min; positive values are clamped to at least 60000; 0 = disabled) |
continuityMode |
Hold requests in the proxy while quota or transient/global 429 limits recover within the continuity deadline; HTTP 529/5xx, network errors, and incomplete SSE attempts are retried internally only for replay-safe methods, never for an ambiguous POST (optional, default true) |
streamRecovery |
Frame Anthropic SSE responses and, with continuity mode, publish only a terminally complete attempt; broken replay-safe attempts may retry transparently, while an ambiguous POST is returned as a retryable error without hidden replay (optional, default true) |
maxResponseBytes |
Maximum bytes buffered per upstream response before returning 502; covers transactional SSE, non-SSE, and OAuth relay responses (optional, default 67108864 = 64 MiB) |
upstreamResponseTimeoutMs |
Total deadline for upstream response headers and buffered non-SSE response bodies (optional, default 300000 = 5 minutes) |
streamIdleTimeoutMs |
Maximum idle time between upstream SSE chunks or while waiting for a downstream client to drain; expiry cancels the stream and releases proxy capacity (optional, default 300000 = 5 minutes) |
streamTotalTimeoutMs |
Hard ceiling for one Anthropic SSE response even when keepalive/ping chunks keep resetting the idle timer; expiry becomes a retryable overload before Claude Code's own request timeout (optional, Claude default 900000 = 15 minutes; Codex default disabled) |
requestBodyTimeoutMs |
Total deadline for receiving a client request body before returning 408 and releasing admission capacity (optional, default 30000 = 30 seconds) |
maxBufferedRequestBytes |
Total request-buffer memory budget used to cap admission before buffering; supervised requests count both supervisor and worker copies (optional, default 268435456 = 256 MiB) |
continuityMaxWaitMs |
Total continuity deadline for internally recovering quota and transient/global 429 responses (optional, default 900000 = 15 minutes) |
continuityMaxSleepMs |
Maximum interval between continuity recovery probes (optional, default 30000 = 30 seconds) |
rateLimitFailovers |
Alternate accounts tried before treating a non-quota 429 as global (optional, default 1) |
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) |
modelFallbacks |
Fork only — per-model fallback chains applied when the cached generally available fleet is fresh-full or a live labeled model-tier 429 reaches every eligible account. Unknown/expired/ready cached windows and unlabeled/global 429s stay account-first; a fleet that is only locally capped or queued for concurrency never changes the model (optional, default {}; see below) |
launchModel |
Fork only — preferred Claude Code model for teamclaude run; launch directly on the first modelFallbacks target only when every generally available account is freshly measured full for that model (optional, default null) |
autoResumeClaude |
Watch the launched Claude transcript and restart the same session after terminal timeout/rate/overload errors (optional, default true) |
claudeAutoResumeMaxRetries |
Maximum same-session automatic resumes before leaving Claude interactive for manual control (optional, default 3) |
claudeAutoResumeBackoffMs |
Initial automatic-resume delay; retries use capped exponential backoff (optional, default 2000) |
codexFallbackOnExhaustion |
After a terminal Claude error, stop Claude and launch TeamCodex with a sanitized handoff only when every enabled Claude account has fresh general-quota exhaustion evidence (optional, default false) |
{
"launchModel": "claude-fable-5",
"modelFallbacks": {
"claude-fable-5": ["claude-opus-4-8"],
"claude-mythos-5": ["claude-opus-4-8"]
}
}The proxy rewrites the request body's model to the next entry of the chain and
retries under normal routing through either of two paths: the cached generally
available fleet is entirely fresh, finite, and full for the model tier (7d_oi),
or a live labeled model-tier 429 has reached every eligible account. A cached
fresh-full fleet falls back before continuity sleeps. Semantics:
- The chain is resolved once per request from the original model (fallbacks of fallbacks are not followed) and consumed in order; when it runs dry, the pre-existing 429/continuity behavior applies unchanged.
- For Claude Code advisor requests, a direct root
tools[]entry whosetypestarts withadvisor_supplies the governing model. Routing and 429 classification use that nested model, and fallback rewrites only that tool'smodel, leaving the top-level executor model unchanged. - Keys and targets must be plain API model IDs. A client-side bracket suffix (
claude-fable-5[1m]— the API rejects such IDs asnot_found_error) matches its suffix-stripped entry. launchModelkeeps those API IDs plain in config. Only the Claude Code launch display adds[1m]to Opus 4.8; Claude Code strips that suffix before sending the request to the proxy.- In the cached precheck, an account with an unknown, expired, or non-full model window is tried with the original Fable/Mythos model before fallback. A fresh-full window excludes only that account for that request and does not affect its Opus/Sonnet/Haiku eligibility.
- Independently of that cached precheck, fleet-wide live labeled model-tier 429 evidence can trigger fallback.
- Fallback runs before a continuity-mode sleep when the cached fleet is fresh-full: rewriting to a served model beats sleeping until a weekly reset.
- A fleet that is only locally capped or queued for concurrency keeps the original model. Unlabeled transient/global 429 recovery also keeps the original model and enters continuity after the account failover budget is handled, within
continuityMaxWaitMs; no account state is poisoned. - Mind quality expectations when composing chains: a background agent may be fine falling all the way to a small model, but an interactive session usually is not — this fork's author runs
fable → opusonly, preferring a surfaced 429 (client retries/waits) over silently degrading below Opus.
flowchart LR
CC["Claude Code"] --> TC["TeamClaude · :3456"]
CX["Codex CLI"] --> TX["TeamCodex · :3457"]
HA["Hermes Agent"] --> TX
TC --> CA{"Claude account pool"}
TX --> OA{"Codex account pool"}
CA --> C1["Account A"]
CA --> C2["Account B"]
CA --> C3["Account C"]
OA --> O1["Account A"]
OA --> O2["Account B"]
OA --> O3["Account C"]
C1 & C2 & C3 --> AN["Anthropic API"]
O1 & O2 & O3 --> OP["OpenAI Codex API"]
- Claude Code connects to the local proxy instead of
api.anthropic.com - The proxy selects the active account and forwards requests with that account's credentials
- OAuth tokens expiring within 5 minutes are automatically refreshed and persisted to config. A background keep-alive sweep also rotates idle and disabled accounts so their refresh-token chain does not lapse; set
tokenRefreshIntervalMs: 0to disable it - Rate limit headers from the API (
anthropic-ratelimit-unified-*) track the proxy's last-observed session (5h) and weekly (7d) quota utilization. Model-scoped weekly windows (7d_oi— the separate Fable/Mythos weekly limit) are discarded on restart and begin unknown. At runtime, only a fresh, finite, full window skips that account for the matching top-tier request; unknown, expired, and ready windows stay eligible, and Opus/Sonnet/Haiku routing is unaffected - 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:
- 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 → the request tries a bounded number of alternate accounts. Once that budget is exhausted, a remaining global limit keeps the original model, opens a shared continuity cooldown, and retries internally within
continuityMaxWaitMsinstead of multiplying the request across the fleet or surfacing 429 to Claude Code. - Requested-model fallback (fork) → a configured
modelFallbackschain rewrites the request when every generally available account's cached model window is fresh-full or when a live labeled model-tier 429 reaches every eligible account. Cached unknown/expired/ready accounts and unlabeled/global 429s remain account-first, and a fleet that is only locally capped or queued for concurrency does not change models. The cached fresh-full path runs before continuity sleep.
- Account-quota exhaustion (upstream reports the account is over its limit) → marks that account rate-limited for its
- Transient network errors and incomplete Anthropic SSE attempts fail over internally only for replay-safe methods (
GET/HEAD/OPTIONS). An ambiguous POST is never replayed inside the proxy after dispatch: it receives a complete retryable error so the client controls any retry. Completed streams preserve byte fidelity. Buffers larger than 1 MiB spill to a private temporary file; transactional SSE, non-SSE, and OAuth relay responses are capped bymaxResponseBytes. - If all accounts are exhausted, continuity mode keeps the request inside the proxy for up to
continuityMaxWaitMs(default900000= 15 minutes), probing no less often than thecontinuityMaxSleepMscap (default30000= 30 seconds). Transient/global 429s recover internally within that same deadline. HTTP 529/5xx backoff is likewise internal only for replay-safe methods; an unsafe request passes the upstream error through without replay. Persistent overload is bounded byTEAMCLAUDE_OVERLOAD_RETRIES(default6) and a client disconnect aborts sooner. SettingcontinuityMode: falserestores legacy handling for quota waits and global rate limits. - Quota survives restarts: the server snapshots general per-account quota/throttle state plus the committed warm-up probe template to
<config>.quota.json(every minute and on exit), so TUI R works before fresh traffic arrives. A restored template is provisional and the first freshly accepted request shape replaces it. Model-scoped weekly values are intentionally discarded on import and start unknown; runtime traffic re-measures them before a fleet-wide fallback can be justified - Client token refresh requests (
/v1/oauth/token) are relayed to upstream untouched — the proxy and client manage their own token lifecycles independently
MIT

