Repository navigation
[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
Activity
- addedenhancementNew feature or requestNew feature or requestaccount-poolOAuth, credentials, Codex pool, quota, failover, plansOAuth, credentials, Codex pool, quota, failover, plansproxyHTTP proxy, routing, reverse-proxy / management authHTTP proxy, routing, reverse-proxy / management auth
on Aug 25, 2026 리뷰 · 우선순위 58 / 80
설명: 이 이슈는 같은 공급자에 로그인한 계정이 둘 이상이면, 429 가 났을 때 그 공급자의 다른 계정으로 자동으로 넘어가 달라는 기능이다. 작성자는 lidge-jun 이다. 지금 열려 있다. 라벨은 enhancement, account-pool, proxy 다. 지금 CURRENT
devHEAD 는 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이 작성했습니다
- added a commit that references this issue
on Aug 25, 2026 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-onlyon429, 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 ConnectEND_STREAMframe carryingRESOURCE_EXHAUSTED, so a status-based rotator structurally cannot see it. Rotation now happens on the adapter event, atpreflightAdapterEvents— 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
projectIdwith its token. The Cursor rotation additionally discards_cursorIdentityScope,_cursorConversationIdand the cursor slice of_providerContinuation, since a Cursor conversation is credential-scoped even when replaying the turn is not.CursorCredentialRouterwas 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.
- added a commit that references this issue
on Aug 26, 2026
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 listlooks like this (masked):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:
hasKeyPoolFailover/rotateKeyOn429in key-failover.ts. Works for any key-auth provider with 2+ pool keys, and it is on by default.hasKeyPoolFailoverreturnsfalseforauthMode === "oauth", so no OAuth account ever enters this loop.excludeAccountId, same-request 429 retry, quota cooldowns). Rich, but bound to the Codex pool ledger (codex-accounts.json) andcodexAccountMode: "pool".rotateAnthropicAccountOn429in anthropic-routing.ts, driven from core.ts. It is gated behindisAnthropicAccountPoolEnabled(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 stableactiveAccountId, per-account credentials, and aneedsReauthflag — but no consumer walks it on a rate limit.Two secondary gaps make this worse:
retryOn429(rateLimitRetryPolicyFor) deliberately returnsnullfor 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:
Retry-After, falling back to a default backoff), pick the next eligible account fromlistAccounts(provider)skipping cooled andneedsReauthentries, mint its access token, and replay the same request. Bound the rotations per request the wayANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUESTalready does.hasKeyPoolFailovertreats a 2+ key pool as consent. A single-account provider stays a strict no-op, so nothing changes for the common case.oauthAccountFailover: { enabled: false }or a per-provider override) so an operator with a ToS concern can force single-account behaviour. Anthropic's existinganthropicAccountPool.enabledshould keep winning where it is set explicitly, so no current configuration changes meaning.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:
After:
Opt-out for an operator who wants strict single-account behaviour:
{ "providers": { "xai": { "oauthAccountFailover": { "enabled": false } } } }Alternatives or workarounds
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.anthropicAccountPool— helps Anthropic only; there is no equivalent for xAI, Cursor, Kimi, Copilot, Antigravity, or Nous.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, andgetAnthropicPoolAccessTokenin anthropic-routing.ts are already written against generic primitives (listAccounts,getAccountCredential,setActiveAccount,pool-rotation.ts). The provider-specific parts are the constantPROVIDER = "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