Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

[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

Closed
#6,749 1 comment 0 reactions 0 assignees View on GitHub

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

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

account-pool bug
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):

  1. Pool of two or more Anthropic OAuth accounts, anthropicAccountPool.enabled, stickyLimit: 1.
  2. Several Claude Code sessions run through ANTHROPIC_BASE_URL=http://127.0.0.1:10100; each is bound to one account by affinity.
  3. Anthropic revokes the access token of the account that sessions are bound to (here: account suspension).
  4. 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.ts rotateAnthropicAccountOnResponse: handles 429, and 403 only for four whole-message entitlement/billing bodies.
  • src/oauth/anthropic-routing.ts recordAnthropicAccountRefusal(status: 429 | 403): the type itself excludes 401.
  • The token is revoked server-side but not expired, so no refresh is attempted and needsReauth is 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 same rotateAnthropicAccountOnResponse, 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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from lidge-jun/opencodex

All issues in lidge-jun/opencodex

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.