Skip to content

[Feature]: Fail over to another logged-in account on 429 for any OAuth provider with 2+ accounts, not just Codex and opt-in Anthropic #2568

Description

@lidge-jun

Area

Authentication and account pool

What are you trying to accomplish?

When I have more than one logged-in account for a provider, I want a 429 to fail over to another account of the SAME provider automatically, without first discovering and enabling a provider-specific opt-in toggle.

Today my ocx account list looks like this (masked):

PROVIDER             TYPE     ID          STATUS
openai               codex    main        selected
openai               codex    chatgpt-…   -
xai                  oauth    f49f684e    -
xai                  oauth    43b56558…   active
xai                  oauth    3488ef5e…   -
anthropic            oauth    1278f8da    -
anthropic            oauth    e112f28b    active
anthropic            oauth    b245be97…   -
google-antigravity   oauth    39d5ad0e    active
cursor               oauth    c78c4427…   active

Three xAI accounts are logged in. When the active xAI account returns 429, the request fails and the other two idle accounts are never tried. The credentials are already stored, already valid, and already selectable by hand with ocx account use xai <id> — the proxy just does not reach for them on a rate limit.

What prevents this today?

Same-provider 429 failover exists, but only on three disjoint paths, and OAuth multi-account failover is implemented for exactly one provider:

  1. API-key pools — hasKeyPoolFailover / rotateKeyOn429 in key-failover.ts. Works for any key-auth provider with 2+ pool keys, and it is on by default. hasKeyPoolFailover returns false for authMode === "oauth", so no OAuth account ever enters this loop.
  2. Codex/OpenAI pool — the account-cooldown and alternate-selection machinery in routing.ts (excludeAccountId, same-request 429 retry, quota cooldowns). Rich, but bound to the Codex pool ledger (codex-accounts.json) and codexAccountMode: "pool".
  3. Anthropic OAuth pool — rotateAnthropicAccountOn429 in anthropic-routing.ts, driven from core.ts. It is gated behind isAnthropicAccountPoolEnabled(config) and its doc comment states "Default OFF".

So the failover ladder has a hole exactly where the user's mental model says it should not: xAI, Cursor, Kimi, GitHub Copilot, Google Antigravity, and Nous all store multiple OAuth accounts through the same multiauth store (store.ts, listAccounts(provider)), and none of them rotate on 429. The generic OAuth multiauth store already has everything the rotation needs — a per-provider account list, a stable activeAccountId, per-account credentials, and a needsReauth flag — but no consumer walks it on a rate limit.

Two secondary gaps make this worse:

  • The Anthropic pool is off by default even when the user has visibly logged in three Anthropic accounts. A user who takes the trouble to add a second account has already expressed the intent that a pool is meant to capture; the opt-in re-asks a question the login already answered.
  • retryOn429 (rateLimitRetryPolicyFor) deliberately returns null for OAuth providers — correct, since replaying on the same token is pointless — but nothing takes its place for OAuth. The result is that an OAuth 429 has no recovery path at all unless it is Codex or opted-in Anthropic.

What should OpenCodex do?

Generalize same-provider 429 account failover to every provider whose OAuth account set has 2 or more eligible accounts, with the number of stored accounts as the activation signal:

  1. On a 429 from an OAuth provider, cool the account that actually failed (honouring Retry-After, falling back to a default backoff), pick the next eligible account from listAccounts(provider) skipping cooled and needsReauth entries, mint its access token, and replay the same request. Bound the rotations per request the way ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST already does.
  2. Make it active whenever the provider has 2+ eligible accounts, matching how hasKeyPoolFailover treats a 2+ key pool as consent. A single-account provider stays a strict no-op, so nothing changes for the common case.
  3. Keep the Codex pool exactly as it is. It has quota scopes, probe leases, and affinity semantics that a generic rotator must not attempt to replicate; the generic path should apply to non-Codex OAuth providers only.
  4. Keep an explicit escape hatch: a config knob (for example oauthAccountFailover: { enabled: false } or a per-provider override) so an operator with a ToS concern can force single-account behaviour. Anthropic's existing anthropicAccountPool.enabled should keep winning where it is set explicitly, so no current configuration changes meaning.
  5. Preserve session affinity where a provider already has it, and do not carry an in-flight request's chosen account across the rotation boundary in a way that breaks prompt caching more than the 429 already did.

Observable result: with three xAI accounts logged in, a 429 on the active account is retried on the next eligible one and the request succeeds instead of surfacing the rate limit to the client.

Example usage or interface

Before (today), with three xAI OAuth accounts stored:

$ ocx account list xai
xai  oauth  f49f684e   -
xai  oauth  43b56558…  active
xai  oauth  3488ef5e…  -

# active account hits its limit
→ client receives 429; f49f684e and 3488ef5e are never attempted

After:

[oauth-failover] xai: 429 on account 43b56558; rotating to account f49f684e
→ same request replayed on f49f684e, 200 returned to the client

Opt-out for an operator who wants strict single-account behaviour:

{
  "providers": {
    "xai": {
      "oauthAccountFailover": { "enabled": false }
    }
  }
}

Alternatives or workarounds

  • Manually run ocx account use xai <other-id> after every rate limit. Works, but it is a human in the retry loop, and by the time the user notices, the client has already surfaced the error.
  • Enable anthropicAccountPool — helps Anthropic only; there is no equivalent for xAI, Cursor, Kimi, Copilot, Antigravity, or Nous.
  • Convert an OAuth provider to an API-key provider so the key pool path applies. Not possible for subscription-backed OAuth providers, which is precisely where users keep multiple accounts.
  • Use a combo (combo/<id>) with failover strategy. That fails over across provider/model targets, not across accounts of one provider, and it changes the model identity of the request.

Additional context

Related prior art in this repository:

Design notes: getEligibleAnthropicAccounts, rotateAnthropicAccountOn429, and getAnthropicPoolAccessToken in anthropic-routing.ts are already written against generic primitives (listAccounts, getAccountCredential, setActiveAccount, pool-rotation.ts). The provider-specific parts are the constant PROVIDER = "anthropic", the config gate, and the token minting — which suggests the generalization is mostly a parameterization rather than a new subsystem.

ToS consideration worth deciding explicitly in review: automatic rotation across subscription accounts is more sensitive than rotating API keys, which is why the Anthropic pool shipped opt-in. The counter-argument for making it presence-driven is that logging in a second account is itself the operator's decision, and the opt-out knob preserves the strict behaviour for anyone who wants it.

Checks

  • I searched existing issues and documentation.
  • This request describes a concrete OpenCodex workflow rather than merely naming a desired technology.
  • I removed secrets and personal data.

Activity

  1. added
    enhancementNew feature or request
    account-poolOAuth, credentials, Codex pool, quota, failover, plans
    proxyHTTP proxy, routing, reverse-proxy / management auth
    on Aug 25, 2026
  2. lidge-jun commented on Aug 25, 2026

    @lidge-jun
    OwnerAuthor

    리뷰 · 우선순위 58 / 80

    설명: 이 이슈는 같은 공급자에 로그인한 계정이 둘 이상이면, 429 가 났을 때 그 공급자의 다른 계정으로 자동으로 넘어가 달라는 기능이다. 작성자는 lidge-jun 이다. 지금 열려 있다. 라벨은 enhancement, account-pool, proxy 다. 지금 CURRENT dev HEAD 는 516f566 이다. 이번 시간에 SHA 가 움직였다. origin/dev package.json 은 2.32.1-preview.20260825 다. 차량 풀은 아직 없다. 내가 구현하지 않고 머지하지 않는다.

    HEAD 에서 구멍 지적은 맞다. src/providers/key-failover.ts 86-88줄 hasKeyPoolFailover 는 authMode 가 oauth 이거나 forward 이면 거짓이다. 147-156줄 rotateKeyOn429 도 같은 칸에서 바로 돌아온다. 98-107줄 rateLimitRetryPolicyFor 는 oauth 에 같은 토큰 재시도를 주지 않는다. 같은 토큰을 다시 보내는 것은 이 코드가 일부러 막는 일이다. 그 빈자리를 채우는 일반 OAuth 계정 회전은 지금 없다.

    Codex 풀과 Anthropic 풀은 따로 산다. src/codex/routing.ts 는 Codex 장부만 본다. src/oauth/anthropic-routing.ts 1-11줄은 기본 꺼짐이라고 적고, 79-80줄 isAnthropicAccountPoolEnabled 는 enabled 가 참일 때만 켠다. 474-481줄 rotateAnthropicAccountOn429 도 그 문을 먼저 본다. src/server/responses/core.ts 3420-3425줄은 같은 대상 429 재시도가 키 인증용이라고 적는다. src/oauth/store.ts 616-618줄 listAccounts 는 공급자마다 계정 목록을 이미 준다. xAI, Cursor, Kimi, Copilot, Antigravity, Nous 는 그 목록을 429 때 걷지 않는다.

    본문이 제안하는 켜짐 신호는 계정 수 둘 이상이다. Anthropic 풀은 약관 때문에 기본 꺼짐으로 실렸다. 두 번째 로그인만으로 켜면 그 판단이 바뀐다. Codex 풀의 쿼터 범위와 임대는 일반 회전자가 베끼면 안 된다. src/providers/cursor-pool.ts 의 CursorCredentialRouter 는 여전히 어댑터에 안 붙었다. 그 라우터를 이 이슈의 차량으로 쓰지 말 것. 2509 는 서브에이전트가 다른 쿼터 범위에서 계정을 재사용하는 구멍이다. 이번 시간에 2515 가 착지했지만 2509 는 더 남아서 닫지 말 것. 2539 는 Anthropic 주간 창이다. 2560 이 아직 드래프트라 닫지 말 것. 2566 은 계정 리스트 표시다. 이 이슈로 닫지 말 것.

    src/runtime 은 없다. default-aliases.ts 와 model-presets.ts 도 없다. 2463 2464 2465 를 닫지 말 것. 프리뷰 배포가 아니다. 태그하지 말 것. 배포하지 말 것. 내가 이 이슈를 닫지 않는다.

    라인 87 - src/providers/key-failover.ts hasKeyPoolFailover 가 oauth 를 바로 거짓으로 돌린다
    라인 107 - src/providers/key-failover.ts rateLimitRetryPolicyFor 가 oauth 에 같은 토큰 재시도를 주지 않는다
    라인 156 - src/providers/key-failover.ts rotateKeyOn429 가 oauth 에서 null 을 돌려준다
    라인 80 - src/oauth/anthropic-routing.ts isAnthropicAccountPoolEnabled 는 기본 꺼짐이다
    라인 481 - src/oauth/anthropic-routing.ts rotateAnthropicAccountOn429 가 그 문을 먼저 본다
    라인 616 - src/oauth/store.ts listAccounts 는 이미 공급자별 계정 목록을 준다
    src/providers/cursor-pool.ts CursorCredentialRouter - 클래스만 있고 어댑터에 안 붙었다. 이 이슈의 차량이 아니다
    2509 - 2515 가 착지해도 더 남는다. 이 이슈로 닫지 말 것
    2539 - Anthropic 주간 창이다. 2560 이 합쳐지기 전에 닫지 말 것
    2566 - 계정 리스트 표시다. 이 이슈로 닫지 말 것

    메인테이너의 판단이 필요한 지점

    • 이 이슈를 받을지. 구멍은 맞다. 기능이고 약관이 걸린다. 설계를 먼저 고른다
    • 켜짐을 계정 수 둘 이상으로 할지, Anthropic 처럼 명시 켜기로 할지
    • Codex 풀을 일반 회전자 밖에 둘지. 본문도 그렇게 적는다. 그 편이 맞다
    • CursorCredentialRouter 를 붙일지. 붙이지 말 것. 그 모듈은 Cursor 가중 선택이고 아직 안 붙었다
    • 2509 와 2539 와 2566 을 이 이슈로 닫을지. 닫지 말 것
    • 지금 leftover-close 할지. 차량이 없다. 내가 닫지 않는다

    너의 추천
    기능으로 받는다. 구현은 기다린다. 같은 공급자 429 회전은 Codex 밖 OAuth 에만 두고, Anthropic 의 명시 켜짐은 의미를 유지한다. 계정 수 둘 이상을 바로 동의로 보지 말 것. 약관 판단을 메인테이너가 고른 뒤에 차량을 만든다. 2509 2539 2560 2566 은 이 이슈로 닫지 말 것. 내가 구현하지 않는다. 라벨은 그대로 둔다. 프리뷰 배포가 아니다.

    이 댓글은 grok-bot이 작성했습니다

  3. lidge-jun commented on Aug 25, 2026

    @lidge-jun
    OwnerAuthor

    The three implementable parts are on dev:

    • HTTP-status paths (main response lane) — shipped earlier in this cycle.
    • Sidecars — fix(sidecars): rotate generic OAuth accounts on 429 (#2568) #2607 (87250870c19daeaa03e2995f59478258c98e8b19). The image bridge and web-search loop each had a key-pool-only on429, so an OAuth provider recovered on a normal turn and failed on an image or web-search turn. The hook contract is widened to allow an async return (token refresh is async), both loops await it, and every existing synchronous key-pool caller is unchanged.
    • Cursor's actual path — fix(cursor): rotate OAuth accounts on adapter-event 429 before first output (#2568) #2608 (6b508d5a8cc02a47aadd3d555949241f3bc05fa3). Cursor's rate limit is not an HTTP 429: the request returns 200 and the limit arrives as a Connect END_STREAM frame carrying RESOURCE_EXHAUSTED, so a status-based rotator structurally cannot see it. Rotation now happens on the adapter event, at preflightAdapterEvents — the one point where no client-visible bytes have been emitted and the turn can honestly be replayed.

    An error after first output stays terminal and is never replayed. Once output has reached the bridge, a transparent replay would duplicate or contradict what the client already saw. That limit is deliberate and has its own test rather than being an unstated assumption.

    Rotation always swaps the full credential snapshot, not a bare bearer — Kiro carries routing metadata and Antigravity pairs an account-matched projectId with its token. The Cursor rotation additionally discards _cursorIdentityScope, _cursorConversationId and the cursor slice of _providerContinuation, since a Cursor conversation is credential-scoped even when replaying the turn is not.

    CursorCredentialRouter was deliberately not wired, per the review note on this issue.

    The one thing I did not decide for you

    Activation stays opt-in (oauthAccountFailover.enabled, default off). This issue asks for presence-driven activation — 2+ accounts means rotate — by analogy with a 2-key API pool.

    I did not take that, and it is the one part of this issue I am deliberately leaving to you. An API-key pool spends your own metered credit; rotating across subscription accounts spends a second subscription's quota, which is the exact reason the Anthropic pool shipped opt-in. Turning it on later costs nothing; a default-on rotation that surprises someone has already spent the quota, and that is not reversible.

    If you want presence-driven, it is a one-line default change in isGenericOAuthFailoverEnabled — the mechanism, the bound, the cooldowns and the exclusions all stay exactly as they are. Say the word and I will flip it.

    Closing the implementation. Reopen or file a follow-up if you want the default flipped.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    account-poolOAuth, credentials, Codex pool, quota, failover, plansenhancementNew feature or requestproxyHTTP proxy, routing, reverse-proxy / management auth

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions