[Bug]: Anthropic OAuth pool never ejects an account whose token was revoked (401 "OAuth access token has been revoked."); affinity-bound Claude Code sessions loop on 401 for hours
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 48/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- typescript
- Domain
- authentication, backend-api-design
Research direction
Start with src/server/messages-native.ts, then trace rotateAnthropicAccountOnResponse in src/oauth/anthropic-account-refusal.ts and the refusal handling in src/oauth/anthropic-routing.ts. The issue describes the expected revoked-token behavior and calls for checking the shared function's Responses-API callers in adapter-dispatch.ts, adapter-continuation.ts, and sidecar-execution.ts. Done means a revoked-token 401 can fail over, clear affinity, and mark the account needsReauth, with regression coverage for both paths and unchanged behavior for other 401s.
Written by the indexing model from the issue text.
Description
Client or integration
Claude Code
Area
Authentication and account pool
Summary
A pooled Anthropic OAuth account was suspended by Anthropic. From that second on, every request that session affinity had bound to it came back 401 authentication_error: OAuth access token has been revoked., and OpenCodex kept routing the same sessions to the same account. In the request log that is 1,030 consecutive 401s over 3 h 08 min across 7 Claude Code sessions, while no other pool account served a single request. Traffic moved off it only when the account was paused by hand (it shows as paused in the pool afterwards).
Nothing in the pool treats a 401 on a pooled Anthropic OAuth account as an account-level failure: it does not fail over, does not clear affinity, and does not mark the account needsReauth. Static key pools do rotate on 401 (rotateProviderTransportOn401), so the OAuth pool is the odd one out.
Expected: a pre-output 401 authentication_error with the revoked-token message on a pooled Anthropic OAuth account (matched as a whole message, the way the 403 account refusals are) fails the request over to another eligible account, clears that account's session affinity and marks it needsReauth through the generation-fenced path (markAccountNeedsReauthIfGeneration). Every other 401 (expired token, unknown body, no alternate account) stays on the current path. With no alternate the client still gets the 401, but the account is still flagged so it stops being selected.
This is the concrete Anthropic native-Messages case of the "Failover classes are too narrow … a 401 … terminal for the pool" gap listed in #3375.
Reproduction
What happened (observed, not deliberately provoked):
- Pool of two or more Anthropic OAuth accounts,
anthropicAccountPool.enabled,stickyLimit: 1. - Several Claude Code sessions run through
ANTHROPIC_BASE_URL=http://127.0.0.1:10100; each is bound to one account by affinity. - Anthropic revokes the access token of the account that sessions are bound to (here: account suspension).
- The next turn of every bound session returns
401 OAuth access token has been revoked.and the following turns of the same sessions go to the same account again. Nothing changes until the account is paused manually.
Deterministic repro without a real suspension: point the first account at an upstream stub that answers its bearer with 401 {"type":"error","error":{"type":"authentication_error","message":"OAuth access token has been revoked."}} and keep a second account answering 200. Expected: the turn succeeds on the second account. Actual: the client gets the 401. A regression test for this will follow in a PR.
Version
@bitkyc08/opencodex 2.78.0 (the version running at the time). Source also checked at 2.80.0 and dev aeebf11: identical.
Operating system
Windows 11
Provider and model
anthropic (OAuth pool) / claude-opus-5-5, claude-sonnet-5-5, native Messages path
Logs or error output
~/.opencodex/usage.jsonl, times UTC+9:
18:12:31 Anthropic email "Your account has been suspended"
18:12:37 first 401 "OAuth access token has been revoked." (6 s later)
18:12:37 -> 21:20:48 1,030 requests, all 401 (1,008 claude-opus-5-5, 22 claude-sonnet-5-5),
7 conversations, each retrying every 2-19 s (median per conversation)
18:12:37 -> 21:21 rows from any other provider slot: 0 (only this slot: 1,030 x 401, 2 x 502)
21:25:47 the longest-looping conversation continues with 200s on another pool account
Before 18:12 the same slot had served 12,155 requests with 200. One 401 row (abridged):
{"provider":"anthropic-<slot>","surface":"claude","inboundProtocol":"messages","status":401,"durationMs":275,
"upstreamError":"OAuth access token has been revoked.",
"attempts":[{"ordinal":1,"adapter":"anthropic","status":401,"sendCount":1,"recoveryKinds":[],
"errorCode":"invalid_api_key","failureStage":"headers-only","failureCause":"credential-rejected"}]}
All 1,030 rows have exactly one attempt and empty recoveryKinds: no replay, no failover, no refresh.
Why, in source (identical in 2.78.0, 2.80.0 and dev):
src/server/messages-native.ts:while (oauthBinding && (response.status === 429 || response.status === 403)). A 401 never enters the loop and is passed to the client.src/oauth/anthropic-account-refusal.tsrotateAnthropicAccountOnResponse: handles 429, and 403 only for four whole-message entitlement/billing bodies.src/oauth/anthropic-routing.tsrecordAnthropicAccountRefusal(status: 429 | 403): the type itself excludes 401.- The token is revoked server-side but not expired, so no refresh is attempted and
needsReauthis never set.getEligibleAnthropicAccounts()still lists the account and the affinity binding survives, so each session picks it again and Claude Code retries on the 401.
Redacted configuration
{ "anthropicAccountPool": { "enabled": true, "strategy": "quota", "stickyLimit": 1, "autoSwitchThreshold": 80, "quotaWindow": "max-utilization" } }
Checks
- I searched existing issues and documentation.
- I removed secrets, tokens, account details, request credentials, and personal data.
Notes
- I can't tell from the log whether the refresh token was revoked as well.
- This issue is only about what the pool does after Anthropic revokes a token. It does not claim OpenCodex caused the suspension; see the separate cold-start and handoff observation in #6752.
- The Responses-API path (
adapter-dispatch.ts,adapter-continuation.ts,sidecar-execution.ts) calls the samerotateAnthropicAccountOnResponse, so a fix in the shared function should be checked there too. I only observed the native Messages path.
- Dominant language
- TypeScript
- Stars
- 16.9k
- Forks
- 1.3k
- Avg merge
- 7h 34m
- Merged PRs (30d)
- 667
Getting set up
This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from lidge-jun/opencodex
-
Transient OAuth login failure persists in getLoginStatus and masks active account in dashboardMay be free again A pull request for this issue was closed without being merged. Openaccount-pool bug gui
Difficulty 2/5 1-3 hours Newbie friendliness 67/100
lidge-jun/opencodex#7378 · 2 comments ·
Maintainers usually reply within 1 day
-
account-pool enhancement needs-design proxy
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
lidge-jun/opencodex#6737 · 1 comment ·
Maintainers usually reply within 1 day
-
fix(google-antigravity): discoverAntigravityProject fails with missing_project when project is not auto-discovered, rejecting valid tokensPossibly taken @yobushka claimed this today. Openaccount-pool bug
Difficulty 3/5 1-2 days Newbie friendliness 35/100
lidge-jun/opencodex#7382 · 2 comments ·
Maintainers usually reply within 1 day
-
account-pool bug needs-design provider proxy
Difficulty 4/5 3-5 days Newbie friendliness 42/100
lidge-jun/opencodex#7380 · 3 comments ·
Maintainers usually reply within 1 day
-
account-pool bug
Difficulty 4/5 3-5 days Newbie friendliness 48/100
lidge-jun/opencodex#7377 · 2 comments ·
Maintainers usually reply within 1 day
All issues in lidge-jun/opencodex
Similar issues
-
[测试] 等待仓库加载后再记录多仓库排序基线Possibly taken @myot233 claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
Maintainers usually reply within 1 day
-
area:arcade p3-medium ready-for-agent testing type:docs
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
fderuiter/deruiter.dev#2369 ·
Maintainers usually reply within 1 day
-
area:devenv origin:agent type:bug
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
overengineeringstudio/effect-utils#1949 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
ccch1mneyyy/dsh-TUI#1631 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
MiniMax-AI/minimax-code#507 ·
Maintainers usually reply within 1 day