How It Works
Quick Start
If you already have an API key set in.env, Mibyan auto-discovers it as a 1-key pool. To benefit from pooling, add more keys:
← marks the currently selected credential. id= is the entry id accepted by
mibyan auth remove <provider> <target> when a label is ambiguous, and priority= is
the order the pool tries credentials in under the fill_first strategy.
Interactive Management
Runmibyan auth with no subcommand for an interactive wizard:
mibyan auth add openai-codex login becomes its own pool entry, but only different OpenAI accounts rotate independently: two logins of the same account share one token family upstream, so OpenAI revokes the older one and the second entry adds no quota. Mibyan warns at add time (warning: this login is the same OpenAI account as openai-codex credential #N) — log into a different account, or keep just one.
CLI Commands
For Nous,
auth refresh supports only the login’s device_code singleton.
Independent Nous pool accounts are rejected before refresh; their tokens and
cooldowns are preserved. Reauthenticate with mibyan auth add nous --type oauth
to update the singleton; this does not refresh an independent account. Other
providers retain their existing source-specific refresh support.
Rotation Strategies
Priority positions are zero-based and clamp to the pool’s ends; displayed targets are one-based indices, entry IDs, or unambiguous exact labels.auth add --priority
also places an existing entry updated by reauthentication. Anthropic keeps manual
credentials ahead of seeded credentials, so the command reports the effective
position when that rule changes it. Other strategies may override priority, and
reordering does not rebind credentials already held by a running session.
Every successful pool selection increments request_count, regardless of strategy.
Refresh-only lookups and peeks do not count. Status reads (mibyan doctor, the /model
picker’s provider rows, dashboard auth cards) are peeks: they never refresh, rotate, or
bench a pool credential, so a token endpoint hiccup while the picker is open cannot hide
a provider that is still serving requests. These are selection counters, not
billing totals or a count of every inference request: a cached credential can serve
multiple requests. Counts remain in memory until the next existing pool write
(for example rotation, exhaustion, refresh, or an administrative change); this does
not add a disk write per selection.
Configure via mibyan auth → “Set rotation strategy” or in config.yaml:
Demoting a healthy credential
The pool only benches a credential after the provider rejects it (429/402/401). To keep a credential you are actively using elsewhere — for example a Codex login whose weekly window you want to save for interactive work — out of the gateway’s first pick before it runs dry, move it to the back of thefill_first order instead of removing it:
mibyan auth priority <provider> <target> <priority> reorders one credential and renumbers the
others, then persists the new order to auth.json. The demoted entry stays healthy: it is not
exhausted, so it is never touched by the Codex quota-reset probe and there is nothing for
mibyan auth reset to clear; it is not refreshed on a timer, and it is still used once every
credential ahead of it is benched.
Sessions that already hold a credential keep it until they rotate; new sessions (and the next
gateway start) follow the new order.
Error Recovery
The pool handles different errors differently:
Provider-supplied
reset_at timestamps override these default cooldowns.
The has_retried_429 flag resets on every successful API call, so a single transient 429 doesn’t trigger rotation.
Quota benches are temporary for the live session too. When a 429/402 rotates a session off a
credential, that session checks at the start of each turn whether the benched credential is back in
rotation and moves back to it as soon as its cooldown lifts — the same choice a new session would make.
A long-running chat (the gateway keeps agents cached) therefore returns to a subscription seat once
its window reopens instead of billing the metered fallback for the rest of its life. A 401 bench
does not trigger this; an explicit /model switch cancels a pending switch-back.
Anthropic 429s are per model. Anthropic enforces its rate limits per model, so a generic 429 for
one Claude model cools that credential down for that model only — the same key keeps serving every
other Claude model, and ANTHROPIC_API_KEY / borrowed Claude Code tokens honour the same per-model
cooldown. Billing (402, usage-limit) and auth (401) failures still bench the whole credential.
A dead OAuth login is reported, not benched. When a refresh token is rejected for good
(invalid_grant, invalid_token, refresh_token_reused — the token was revoked, or another program
holding the same login rotated it first — or, for Nous, the profile holds no Portal login or token
pair to refresh with), the pool logs one WARNING naming the entry and the repair
command (mibyan auth add <provider>), and the credential leaves rotation — marked dead, or dropped
when it only mirrored a token file the pool has just cleared — until you sign in again. This applies to Anthropic, Codex, xAI
and Nous OAuth logins alike. A dead credential never re-enters rotation on a timer, so a lost login
shows up once in the log instead of failing quietly every hour.
Every Codex login in an always-on home is dead: sign in again, do not wait for adoption. Mibyan
imports the Codex CLI’s ~/.codex/auth.json automatically only to repair a login it already has
— when its own refresh of the openai-codex entry fails and auth.adopt_external_logins is on
(see Borrowed CLI logins). A pool whose Codex entries are
all dead (or removed) has nothing left to repair, so a headless gateway or cron profile stays
without a Codex credential until you run mibyan auth add openai-codex in that home
(mibyan -p <profile> auth add openai-codex for a named profile), which offers the Codex CLI import
interactively. Profiles that should share one login can point at the same mibyan_HOME instead of
each holding a copy of a single-use refresh token.
A cooling-down or dead credential is not a blank install. When a configured profile starts the
CLI while its only credential is benched or quarantined, startup prints the failure and, for a bench,
the remaining cooldown (or the mibyan auth add <provider> re-login for a dead one) — the first-run
“No inference provider is configured yet” wizard is offered only when the resolver finds nothing
configured at all.
Custom Endpoint Pools
Custom OpenAI-compatible endpoints (Together.ai, RunPod, local servers) get their own pools, keyed by the endpoint name from theproviders: dict in config.yaml (or the legacy custom_providers list, which is auto-migrated).
When you set up a custom endpoint via mibyan model, it auto-generates a name like “Together.ai” or “Local (localhost:8080)”. This name becomes the pool key.
auth.json under credential_pool with a custom: prefix:
Auto-Discovery
Mibyan automatically discovers credentials from multiple sources and seeds the pool on startup:
Auto-seeded entries are updated on each pool load — if you remove an env var, its pool entry is automatically pruned. Manual entries (added via
mibyan auth add) are never auto-pruned.
Several keys from the environment
Want more than one key for a provider without storing any of them inauth.json? Number them. Next to NVIDIA_API_KEY set NVIDIA_API_KEY_2, NVIDIA_API_KEY_3, … in your shell, .env, or secret manager (Bitwarden Secrets, Vault, …) and each becomes its own pool entry on the next load — no command, no config. Discovery stops at the first missing number, so a stray _5 with no _4 is ignored. Combine with credential_pool_strategies to rotate them:
auth.json boundary. Mibyan can use the resolved value in memory for the current run, but it persists only metadata such as the source ref, label, status, request counters, and a non-reversible fingerprint. Manual entries and Mibyan-owned OAuth/device-code state keep the durable tokens they need to refresh.
Delegation & Subagent Sharing
When the agent spawns subagents viadelegate_task, the parent’s credential pool is automatically shared with children:
- Same provider — the child receives the parent’s full pool, enabling key rotation on rate limits
- Different provider — the child loads that provider’s own pool (if configured)
- No pool configured — the child falls back to the inherited single API key
Thread Safety
The credential pool uses a threading lock for all state mutations (select(), mark_exhausted_and_rotate(), try_refresh_current(), mark_used()). This ensures safe concurrent access when the gateway handles multiple chat sessions simultaneously.
Across processes (many subagents, a gateway plus a CLI, cron jobs), OAuth refreshes are serialized through a file lock on auth.json. When one shared OAuth grant expires under many concurrent processes, exactly one process performs the refresh; the others detect that the on-disk token no longer matches the one that failed and adopt it instead of rotating the single-use refresh token again. A process that loses the lock race keeps its entry healthy and retries — lock contention is never recorded as a credential failure. A session or process still holding an older copy of the pool never writes that copy’s tokens back over a pair another one rotated since; it keeps the newer pair on disk and adopts it.
Architecture
For the full data flow diagram, seedocs/credential-pool-flow.excalidraw in the repository.
The credential pool integrates at the provider resolution layer:
agent/credential_pool.py— Pool manager: storage, selection, rotation, cooldowns;agent/credential_pool_admin.pyowns locked target resolution, reset, add, removal, and priority mutations;agent/credential_pool_model_cooldowns.pyowns the per-model Anthropic 429 cooldownsmibyan_cli/auth_commands.py— CLI commands and interactive wizardmibyan_cli/runtime_provider.py— Pool-aware credential resolutionagent/turn_api_error.py— Error recovery: 429/402/401 → pool rotation → fallback
Storage
Pool state is stored in~/.mibyan/auth.json under the credential_pool key:
auth.json. The manual Anthropic entry was intentionally added to Mibyan’ credential store, so its token remains persistable.
An env: row is re-hydrated from the environment on every load, and the variable name does not have to be one Mibyan declares for the provider: numbered siblings (OPENROUTER_API_KEY_2, see Auto-Discovery) appear here automatically, and a hand-written row pointing at any other variable is filled the same way, without the secret ever being written to auth.json.
Strategies are stored in config.yaml (not auth.json):

