Error contract reference for user-facing CLI and exported helper behavior.
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,1otherwiseusage:0for 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 does not match
- Human-readable command output is written to
stdout. - Argument/usage and failure diagnostics are written to
stderr. - On invalid command/arguments, usage text is printed with a non-zero exit code.
Examples:
- unknown subcommand:
Unknown command: <name>plus usage switchwith missing index:Missing index. Usage: codex-multi-auth switch <index>switchwith invalid index:Invalid index: <value>
The authoritative list of common --json command surfaces is the Common Flags
--json row in commands.md:
verify-flagged, verify, why-selected, best, forecast, report,
usage, budget, models, monitor, integrations, fix, doctor,
config explain, debug bundle, history.
Those commands support --json / -j and produce pretty-printed JSON objects
(or, for nested families such as budget, JSON payloads for the subcommands
that accept the flag).
Documented examples:
codex-multi-auth forecast --jsoncodex-multi-auth report --jsoncodex-multi-auth fix --jsoncodex-multi-auth doctor --jsoncodex-multi-auth verify-flagged --jsoncodex-multi-auth verify --paths --jsoncodex-multi-auth why-selected --jsoncodex-multi-auth best --jsoncodex-multi-auth usage --jsoncodex-multi-auth budget list --jsoncodex-multi-auth models --jsoncodex-multi-auth monitor --jsoncodex-multi-auth integrations --jsoncodex-multi-auth config explain --jsoncodex-multi-auth debug bundle --jsoncodex-multi-auth history --jsoncodex-multi-auth history show <id> --json
Additive JSON helpers also documented in the command reference (outside the compact Common Flags row):
codex-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.
commandfield identifies 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 are normalized 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 are normalized to JSON error payloads with a stable
error.messagefield. - Diagnostics may include request/correlation IDs when available.
The request layer's thrown errors are backed by the typed hierarchy in lib/errors.ts (base class CodexError, which extends Error and carries a stable code string):
refreshAndUpdateTokenthrowsCodexAuthError(code: "CODEX_AUTH_ERROR") with the messageFailed to refresh token, authentication requiredon any refresh failure. The error carries aretryableboolean (transient network/lock failures are retryable; invalid-grant style failures are not) and, where available,causeandcontext(refreshFailureReason,statusCode).- Catch sites may rely on
instanceof CodexAuthError(or the structuralcodeproperty) plusretryableto decide whether to re-attempt or force re-authentication. - HTTP error responses are returned as normalized
Responsepayloads (see above), 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 |
Request path or method is outside the supported Responses/model discovery surface |
runtime_rotation_proxy_unauthorized |
401 |
Local request did not include 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 runtime request |
codex_pinned_account_unavailable |
503 |
A pin is in force — either a manual pin (codex-multi-auth switch) or a forced pin set per invocation by the launcher (--account / CODEX_MULTI_AUTH_FORCE_ACCOUNT) — but the pinned account remains rate-limited, cooling down, disabled, or blocked by policy after live blocker checks and any bounded retry attempts. Healthy pins can retry the same account a bounded number of times (at most four upstream attempts, whatever retryAllAccountsMaxRetries is set to, with a 250ms/500ms/1s backoff between them and an absolute 16-selection-pass safety ceiling over the loop); they never rotate to another account. The remedy depends on pin_source: a manual pin clears with codex-multi-auth unpin, a forced pin does not and needs a relaunch |
codex_runtime_rotation_proxy_error |
500 |
Proxy failed before forwarding the request |
Pool exhaustion includes a reason, retry_after_ms, and a hint to run codex-multi-auth rotation status. Pinned-account-unavailable responses include a pinnedAccountIndex field identifying the pinned account, a structured reason field carrying the live blocker or final attempt verdict (for example rate-limited, cooling-down:auth-failure, circuit-open, disabled, workspace-disabled, policy-blocked, missing, auth-failure, network-error, or server-error) or null when no reason was recorded, and an account_skip_reasons map keyed by account index that mirrors the pool-exhausted response shape. The human-readable message appends the corresponding operator-facing blocker in parentheses when present (see issue #486).
For pinned requests the attempt budget is min(retryAllAccountsMaxRetries + 1, 4), not the raw setting: retryAllAccountsMaxRetries is the "retry when every account is rate-limited" POOL knob, and for a pin every unit of it would be another copy of the same non-idempotent request to the same upstream. The 16-selection-pass ceiling still bounds the whole loop, including branches that do not increment the transient-attempt counter.
A pinned retry deliberately waives the pinned account's own cooldown, and only the cooldown. Every transient failure branch cools the account down before the next selection pass, so honoring it would end the loop after a single upstream attempt and make the attempt budget unreachable. Rate limits, an open circuit, a disabled account, workspace and policy blocks all still stop the retry, the cooldown stays on the account for other requests and for this response's retry_after_ms, and the waiver applies only from the second pass onward — a pin that is ALREADY cooling down when the request arrives still returns codex_pinned_account_unavailable without an upstream call.
Pinned-account-unavailable responses also carry three recovery fields:
| 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 this session's launcher (--account / CODEX_MULTI_AUTH_FORCE_ACCOUNT) and unpin will NOT clear it — relaunch to select a different account. The message carries the matching remedy |
reset_at |
ISO-8601 string | null |
When the pinned account next becomes selectable: the latest of its still-active rate-limit record for the request's family and model, any active cooldown, and its circuit breaker's next-admission deadline. null when nothing bounds recovery, and deliberately null under a permanent blocker (disabled, workspace-disabled, policy-blocked, missing, token invalidation), where no timer clears the condition |
retry_after_ms |
number | null |
The same moment as milliseconds from now. Note this is the latest bound for the single pinned account, whereas codex_runtime_rotation_pool_exhausted reports the earliest recovery across the whole pool |
retry_after_ms is advisory; it is not 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.
For selected exported helper APIs, options-object forms were added without removing positional signatures.
Supported dual-call forms include:
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 and is not wrapped in a CodexError subclass.
lib/errors.ts exports a 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 can branch on instanceof or code instead of message text.
Startup-validation guarantees backed by these types:
startRuntimeRotationProxythrowsCodexValidationErrorwithfield: "clientApiKey"when no client API key is supplied, andCodexValidationErrorwithfield: "host"(offending host incontext.host) when asked to bind a non-loopback host. Messages are unchanged from earlier releases; only the class tightened.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 class tightened.