feat(auth): OAuth Marathon - multi-account credential rotation
OAuth Marathon 🏃
Keep running when you hit the wall. This PR adds automatic credential rotation for OAuth providers - when one account hits rate limits or auth errors, opencode seamlessly switches to your next available credential within the same provider.
Note: This rotates between multiple accounts within the same provider (e.g., two OpenAI logins), not between different providers.
Closes #8591
Works for all OAuth providers — both core providers and plugins.
For plugin authors: Plugins that manage their own multi-account pools internally must register each account via
Auth.addOAuth()to benefit from this feature.
The Problem
Using OAuth providers with personal subscriptions often means hitting rate limits mid-session. Currently, when this happens, your request fails and you're stuck waiting.
The Solution
Register multiple OAuth accounts for the same provider, and opencode will automatically:
- Rotate on 429 - Rate limited? Next credential steps in
- Retry on 401/403 - Force token refresh, failover if still failing
- Recover from network errors - Transient failures don't stop the run
- Track health - Cooldown periods prevent hammering exhausted credentials
- All exhausted - Returns the last error response gracefully
How to Add Multiple Accounts
Run opencode auth login multiple times for the same provider:
opencode auth login # Login with first account
opencode auth login # Login with second account (adds, doesn't replace)
Architecture Overview
flowchart TD
A[Provider.getSDK] --> B[createOAuthRotatingFetch]
B --> C{fetchFn}
C -->|429 Rate Limit| D[moveToBack + notifyFailover]
C -->|401/403 Auth| E[markAccessExpired + retry]
C -->|Network Error| F[recordOutcome + notifyFailover]
C -->|200 OK| G[recordOutcome success]
D --> H[Try Next Credential]
E -->|Still fails| H
F --> H
H --> C
Demo
OpenAI Account 1 → 429 Rate Limited
↓ (automatic)
OpenAI Account 2 → 200 OK ✅
↓
Toast: "Rate limited on openai. Switching OAuth credential..."
Configuration (Optional)
Per-provider settings in opencode.json. Sensible defaults are used if omitted:
{
"provider": {
"openai": {
"oauth": {
"maxAttempts": 3, // default: number of accounts
"rateLimitCooldownMs": 60000, // default: 30000
"authFailureCooldownMs": 300000, // default: 300000
"toastDurationMs": 5000 // default: 8000
}
}
}
}
Changes
-
src/auth/rotating-fetch.ts- Core rotation logic -
src/auth/context.ts- AsyncLocalStorage for request scoping -
src/auth/credential-manager.ts- Toast notifications -
src/auth/index.ts- OAuth pool management & persistence -
src/config/config.ts- Newoauthconfig schema -
test/auth/oauth-rotation.test.ts- 10 test cases
Verification
How I tested:
- 10 unit tests covering core scenarios and edge cases
- All tests pass
Test Coverage
- ✅ 429 rotation with Retry-After header
- ✅ Retry-After HTTP date format support
- ✅ 401/403 with forced refresh recovery
- ✅ 401/403 failover when refresh fails
- ✅ Sticky credential until rate limited
- ✅ Non-replayable bodies (streaming)
- ✅ All credentials exhausted
- ✅ Network error failover
- ✅ Request.clone failure handling
- ✅ Refresh token record matching