Error contract reference for user-facing CLI behavior, JSON mode, the runtime rotation proxy, and exported helper call shapes.
0: successful execution1: usage error, invalid arguments, sync/persistence failure, or command failure
Command-specific notes:
why-selected:0when an account is selected,1when the pool is empty or every candidate is cooled down / blockedverify/verify-flagged:0when all selected modes pass,1otherwise (the exit code is the AND of the sub-reports)budget check:1when the checked budget currently blocks requestsusage:0for a successful summary or rotation,1for invalid options or write failures- Forced
--account/CODEX_MULTI_AUTH_FORCE_ACCOUNTfailures exit non-zero without launching Codex when the proxy is disabled or the selector matches no account
- Human-readable command output →
stdout - Argument/usage and failure diagnostics →
stderr - Invalid command/arguments print usage and exit non-zero
- unknown subcommand:
Unknown command: <name>plus usage switchmissing index:Missing index. Usage: codex-multi-auth switch <index>switchinvalid index:Invalid index: <value>
The authoritative list of common --json command surfaces is the Common Flags
--json row in commands.md:
limits, verify-flagged, verify, why-selected, best, forecast,
report, usage, budget, models, monitor, integrations, fix,
doctor, config explain, debug bundle, history.
Those commands accept --json / -j and emit pretty-printed JSON (for nested
families such as budget, the subcommands that take the flag).
Examples:
codex-multi-auth forecast --json
codex-multi-auth report --json
codex-multi-auth fix --json
codex-multi-auth doctor --json
codex-multi-auth limits --json
codex-multi-auth verify-flagged --json
codex-multi-auth verify --paths --json
codex-multi-auth why-selected --json
codex-multi-auth best --json
codex-multi-auth usage --json
codex-multi-auth budget list --json
codex-multi-auth models --json
codex-multi-auth monitor --json
codex-multi-auth integrations --json
codex-multi-auth config explain --json
codex-multi-auth debug bundle --json
codex-multi-auth history --json
codex-multi-auth history show <id> --jsonAdditive --json surfaces outside the compact Common Flags row:
codex-multi-auth list/status --jsoncodex-multi-auth bridge token ... --jsoncodex-multi-auth uninstall --jsoncodex-multi-auth rotation reset-rate-limits --jsoncodex-multi-auth rotation reset-runtime --jsoncodex-multi-auth account policy list --json
Compatibility guarantees:
- Output is valid JSON.
commandidentifies the command family when the payload is a command result object.- Documented top-level sections remain stable unless a migration note is provided.
- Upstream entitlement-like 404 payloads normalize to
403withentitlement_errorpayloads. - Entitlement errors are not treated as rate limits.
- Upstream usage-limit indicators normalize to rate-limit semantics.
handleErrorResponsemay return parsedrateLimit.retryAfterMsmetadata.
- Error responses normalize to JSON error payloads with a stable
error.messagefield. - Diagnostics may include request/correlation IDs when available.
The request layer's thrown errors use the typed hierarchy in lib/errors.ts
(base class CodexError extends Error, carrying a stable code string):
refreshAndUpdateTokenthrowsCodexAuthError(code: "CODEX_AUTH_ERROR", messageFailed to refresh token, authentication required) on any refresh failure. It carries aretryableboolean (transient network/lock failures retryable; invalid-grant style failures not) plus, where available,causeandcontext(refreshFailureReason,statusCode).- Catch sites may rely on
instanceof CodexAuthError(or the structuralcode) plusretryableto decide between re-attempting and forcing re-authentication. - HTTP error responses are returned as normalized
Responsepayloads, not thrown, so they intentionally have noErrorclass.
The default-on localhost Responses proxy returns JSON error payloads with a
stable error.code field.
| Code | HTTP status | Meaning |
|---|---|---|
runtime_rotation_proxy_not_found |
404 |
Path/method outside the supported Responses/model-discovery surface |
runtime_rotation_proxy_unauthorized |
401 |
Local request missing the per-process proxy client key |
runtime_rotation_proxy_payload_too_large |
413 |
Request body exceeded the proxy safety cap |
codex_runtime_rotation_pool_exhausted |
429 or 503 |
No managed account can currently service the request |
codex_pinned_account_unavailable |
503 |
A pin is in force (manual codex-multi-auth switch, or forced per-invocation via --account / CODEX_MULTI_AUTH_FORCE_ACCOUNT) but the pinned account stays rate-limited, cooling down, disabled, or policy-blocked after live blocker checks and bounded retries. Pins never rotate to another account |
codex_runtime_rotation_proxy_error |
500 |
Proxy failed before forwarding the request |
Pool-exhausted responses include reason, retry_after_ms, an
account_skip_reasons map keyed by account index, and a hint to run
codex-multi-auth rotation status. Pinned-account-unavailable responses mirror
that shape plus pinnedAccountIndex, a structured reason (for example
rate-limited, cooling-down:auth-failure, circuit-open, disabled,
workspace-disabled, policy-blocked, missing, auth-failure,
network-error, server-error, or null when none was recorded), and a
human-readable message that appends the blocker in parentheses (issue #486).
Pinned retry budget: for pinned requests the attempt budget is
min(retryAllAccountsMaxRetries + 1, 4) with 250ms/500ms/1s backoff, not the
raw setting — each retry is another copy of the same non-idempotent request to
the same upstream. A 16-selection-pass ceiling bounds the whole loop, including
branches that do not increment the transient-attempt counter.
Cooldown waiver: a pinned retry waives the pinned account's own
cooldown, and only the cooldown — every transient failure branch cools the
account before the next pass, so honoring it would end the loop after one
attempt. Rate limits, an open circuit, a disabled account, and workspace/policy
blocks still stop the retry; the cooldown stays on the account for other
requests and for retry_after_ms; and the waiver applies only from the second
pass onward — a pin already cooling down when the request arrives returns
codex_pinned_account_unavailable with no upstream call.
Recovery fields on pinned responses:
| Field | Type | Meaning |
|---|---|---|
pin_source |
"forced" | "manual" | null |
How the pin was set. manual came from codex-multi-auth switch and clears with unpin; forced came from the session launcher (--account / CODEX_MULTI_AUTH_FORCE_ACCOUNT) and unpin will NOT clear it — relaunch to select a different account. message carries the matching remedy |
reset_at |
ISO-8601 string | null |
When the pinned account next becomes selectable: the latest of its active rate-limit record for the request's family+model, any active cooldown, and its circuit breaker's next-admission deadline. null when nothing bounds recovery, deliberately so under permanent blockers (disabled, workspace-disabled, policy-blocked, missing, token invalidation) |
retry_after_ms |
number | null |
The same moment as ms from now. This is the latest bound for the single pinned account; codex_runtime_rotation_pool_exhausted reports the earliest recovery across the pool. Advisory only — never emitted as a Retry-After header |
Account policy pause/drain is enforced through runtime policy evaluation and
contributes to selection skip reasons such as policy-blocked.
The rate-limit contribution to reset_at / retry_after_ms is a local
revalidation deadline, at most one hour after the rejection, not a promise that
the subscription window has reset. Quota snapshots keep the server reset time
for display. A later cooldown or circuit-breaker deadline may still block use.
For selected exported helpers, options-object forms were added without removing positional signatures.
Supported dual-call forms:
selectHybridAccount(...)andselectHybridAccount({ ... })exponentialBackoff(...)andexponentialBackoff({ ... })getTopCandidates(...)andgetTopCandidates({ ... })createCodexHeaders(...)andcreateCodexHeaders({ ... })getRateLimitBackoffWithReason(...)andgetRateLimitBackoffWithReason({ ... })transformRequestBody(...)andtransformRequestBody({ ... })
Invalid named-parameter calls (missing or wrongly typed required fields, or
unknown keys) throw a native TypeError with a <helper> requires ...
message — for example, createCodexHeaders throws
TypeError: createCodexHeaders requires accountId and accessToken. This is a
deliberate shared convention across the dual-call helpers, not wrapped in a
CodexError subclass.
lib/errors.ts exports the CodexError hierarchy: CodexApiError,
CodexAuthError, CodexNetworkError, CodexValidationError,
CodexRateLimitError, StorageError, CodexUnavailableError. Every subclass
carries a stable string code (the ErrorCode constants) plus class-specific
fields, so callers branch on instanceof or code rather than message text.
Startup-validation guarantees backed by these types:
startRuntimeRotationProxythrowsCodexValidationErrorwithfield: "clientApiKey"when no client API key is supplied, and withfield: "host"(offending host incontext.host) for a non-loopback bind.savePluginConfigaborts withStorageError(code: "UNREADABLE",path= the config file, actionablehint, read-classifier message ascause) when the existing config file cannot be read.
Messages are unchanged from earlier releases; only the classes tightened.