Use multiple ChatGPT Codex OAuth accounts from one openai provider in opencode.
codex-pool keeps normal Codex behavior for your primary account, then automatically spreads requests across extra accounts based on available quota. Here, the primary Codex account means your default openai account in opencode. The plugin is built for people who already use opencode and want smoother throughput, fewer hard stops on 429, and less manual account switching.
- Keep using opencode's built-in Codex flow for your main account
- Add extra ChatGPT accounts as overflow capacity
- Route requests to the healthiest account automatically
- Retry on
429by moving to the next available account - Preserve short-term session affinity so prompt-cache warmth is not thrown away unnecessarily
- Auto-refresh expired tokens and coordinate that safely across multiple opencode processes
After setup, you keep using opencode normally.
- Your default
openaiaccount in opencode stays the primary Codex-compatible account - Extra accounts live in a shared SQLite store as pool accounts
- Before each prompt attempt, the plugin picks the best currently available account
- If one account is rate-limited, the request can fail over to another account
- opencode shows a small toast explaining which account was chosen and why
You do not need to manually rotate accounts during normal use.
bun installbun run buildAdd the built plugin entry to your opencode config:
{
"plugin": ["file:///path/to/codex-pool/dist/index.js"]
}For local development, you can also point opencode at the source entry directly:
{
"plugin": ["file:///path/to/codex-pool/src/index.ts"]
}This plugin adds these auth actions:
Login primary Codex account (browser)Login primary Codex account (headless)Add pool account (browser)Add pool account (headless)Edit pool accounts
Recommended order:
- Log in your main opencode
openaiaccount as the primary Codex account - Add one or more extra accounts as pool accounts
- Start using opencode normally
Edit pool accounts lets you remove a non-primary pool account from the SQLite store.
If you already have a valid OAuth login for the default openai account in opencode, codex-pool bootstraps that account into its database automatically the first time it starts.
At a high level, routing is simple:
- Every available account gets a quota score
- Higher remaining useful capacity means a better score
- The plugin picks the best account for the next request
- If a request gets
429, that account cools down and the next eligible account is tried - If a request gets
401, the plugin refreshes the token and retries once
The scoring is quota-aware, not round-robin. That means the plugin tries to spend capacity where it is most available instead of rotating blindly.
Untouched dormant windows are handled as a separate one-shot rule rather than as a score boost:
- If an account has any untouched dormant
rate_limitwindow, that account is promoted ahead of normal score ordering for one successful request - A dormant window means
used_percent = 0andreset_after_seconds === limit_window_seconds - After one successful request on that account, the same dormant window stops receiving priority for 30 minutes
- That touch suppression is stored in SQLite, so other opencode processes do not keep re-prioritizing the same cached dormant window
primary: your defaultopenaiaccount in opencode, mirrored as the main OAuth account; this is what keeps opencode in Codex modepool: every additional non-primary account stored by the plugin
The primary account is special. It is mirrored back into opencode's openai auth so built-in Codex behavior stays active.
When the selected account still looks healthy, the plugin can add service_tier: "priority" to that outbound prompt attempt.
- This decision is made after account selection
- It does not change account ordering
- Caller-provided
service_tierorserviceTieralways wins - If the account looks constrained, fast mode stays off
You will see the fast-mode decision in the pre-request toast, using a compact score summary like Fast: enabled +1.011 (+1.593 - guard 0.582) when guard pressure applies.
Different ChatGPT accounts do not share the same upstream prompt cache. Switching accounts too often can increase latency.
To reduce that, the plugin keeps short-lived per-session affinity:
- If a session already succeeded on one account, the plugin prefers to stay there briefly
- It only switches when another account is materially better, blocked, or unavailable
- The switch threshold is adaptive: with
SWITCH_MARGIN = 0.35, a competing account usually needs about17.5%to35%more score to break affinity
This gives you better cache reuse without ignoring quota health.
- Database path:
~/.local/share/opencode/codex-pool.db - SQLite is the runtime source of truth for accounts, cooldowns, token refresh locks, and quota cache
- SQLite also stores dormant-window touch suppression shared across processes
- WAL mode is enabled so multiple opencode processes can share the same state
- Quota data is cached and reused across processes
- When cached quota data is reused, guard calculations age the cached window by the cache elapsed time before applying guard pressure
In short: one shared local database coordinates the whole pool.
Before a prompt is sent, the plugin shows a compact toast that includes:
- the selected account
- a short reason for the choice
- quota score details
- whether fast mode is enabled or disabled
Reduced multi-window account scores are shown as <score> (<base> * guard x<factor>). The guard factor is the guard window's current score ratio against its balanced same-window baseline (exp(ln(raw / balanced)), capped at 1), so ahead-of-pace short windows suppress selection more aggressively than the previous reciprocal debt transform. Dormant-window priority is applied before this normal score ordering and is not folded into the score itself.
If stale quota cache is temporarily reused, the toast also shows the cache age. Guard-based ranking and fast-mode guard pressure both age cached windows by that elapsed cache time instead of treating the cached reset time as brand new.
- If all available accounts are rate-limited, the last
429response is returned - Accounts are only disabled after a durable auth failure: a request still returns
401after refresh and one retry - Failed usage fetches do not permanently poison routing; the plugin keeps existing state and retries later
- Request bodies are snapshotted before retries so failover and refresh can safely replay the same payload
For implementation details and internal design notes, see docs/architecture.md.