[PR #5754] auth: multi-account OAuth subscription failover #11566

Closed
opened 2026-02-16 18:16:25 -05:00 by yindo · 0 comments
Owner

Original Pull Request: https://github.com/anomalyco/opencode/pull/5754

State: closed
Merged: No


Implements the missing piece of #5391 for OAuth-based subscription providers: store multiple OAuth accounts per provider and automatically fail over within the same user request when an account is rate limited or its session expires.

This PR is intentionally scoped to the core value (no TUI, no model discovery, no vault CLI).

Why

Today an OAuth login is effectively single-account-per-provider. When that account hits 429 or expires, the request fails and disrupts the user.

Goal: allow multiple “subscription” accounts (OAuth, not API-billing) and make inference resilient by retrying with the next account automatically.

Storage

  • OAuth secrets are stored in the OS credential store via Bun.secrets (service opencode); disk stores only non-secret OAuth metadata (labels, pool order, cooldowns, last status).
  • API-key + wellknown credentials remain stored in auth.json (unchanged) to avoid overlapping with #4318’s broader keyring work.

Existing auth.json is migrated to a v2 format on first read; OAuth secrets are moved into the keychain.

What changed

  • Multi-account OAuth records per provider (stored as metadata + keychain secret entries).
  • Fetch-level rotation wrapper:
    • 429 → Retry-After-aware cooldown + rotate to next account and retry in the same request
    • 401/403 → force a refresh (by clearing access/expires) and retry once; if still unauthorized, rotate
  • Refresh persistence: Auth.set() updates the correct OAuth record by matching the refresh token (works with /auth/:providerID).
  • opencode auth login OAuth paths now add accounts instead of overwriting.

Review order

  1. feat(auth): keychain-backed oauth records (store + migration)
  2. feat(auth): rotate oauth subscriptions on 429/401 (rotation + provider integration + CLI)
  3. test(auth): cover oauth failover
  4. fix(auth): keep api tokens in file, update oauth selection (avoid #4318 overlap + robust refresh targeting)
  5. test(auth): auth.set selects oauth record by refresh
  6. fix(auth): only require keychain for oauth migration
  7. test(auth): isolate oauth rotation provider id

How to test

  • Run opencode auth login twice for the same OAuth provider (e.g. Anthropic “Claude Pro/Max”, Copilot) to create multiple subscription accounts.
  • Trigger throttling/expiry and confirm it retries with the next account rather than surfacing an error.
  • From packages/opencode: bun test test/auth/oauth-rotation.test.ts

Notes

  • Mid-stream rotation (after tokens start streaming) still can’t be transparent; this is best-effort before streaming begins.
**Original Pull Request:** https://github.com/anomalyco/opencode/pull/5754 **State:** closed **Merged:** No --- Implements the missing piece of #5391 for **OAuth-based subscription providers**: store multiple OAuth accounts per provider and automatically fail over *within the same user request* when an account is rate limited or its session expires. This PR is intentionally scoped to the core value (no TUI, no model discovery, no vault CLI). ## Why Today an OAuth login is effectively single-account-per-provider. When that account hits `429` or expires, the request fails and disrupts the user. Goal: allow multiple “subscription” accounts (OAuth, not API-billing) and make inference resilient by retrying with the next account automatically. ## Storage - **OAuth secrets** are stored in the OS credential store via `Bun.secrets` (service `opencode`); disk stores only non-secret OAuth metadata (labels, pool order, cooldowns, last status). - **API-key + wellknown** credentials remain stored in `auth.json` (unchanged) to avoid overlapping with #4318’s broader keyring work. Existing `auth.json` is migrated to a v2 format on first read; OAuth secrets are moved into the keychain. ## What changed - Multi-account OAuth records per provider (stored as metadata + keychain secret entries). - Fetch-level rotation wrapper: - `429` → Retry-After-aware cooldown + rotate to next account and retry in the same request - `401/403` → force a refresh (by clearing access/expires) and retry once; if still unauthorized, rotate - Refresh persistence: `Auth.set()` updates the correct OAuth record by matching the `refresh` token (works with `/auth/:providerID`). - `opencode auth login` OAuth paths now **add** accounts instead of overwriting. ## Review order 1. `feat(auth): keychain-backed oauth records` (store + migration) 2. `feat(auth): rotate oauth subscriptions on 429/401` (rotation + provider integration + CLI) 3. `test(auth): cover oauth failover` 4. `fix(auth): keep api tokens in file, update oauth selection` (avoid #4318 overlap + robust refresh targeting) 5. `test(auth): auth.set selects oauth record by refresh` 6. `fix(auth): only require keychain for oauth migration` 7. `test(auth): isolate oauth rotation provider id` ## How to test - Run `opencode auth login` twice for the same OAuth provider (e.g. Anthropic “Claude Pro/Max”, Copilot) to create multiple subscription accounts. - Trigger throttling/expiry and confirm it retries with the next account rather than surfacing an error. - From `packages/opencode`: `bun test test/auth/oauth-rotation.test.ts` ## Notes - Mid-stream rotation (after tokens start streaming) still can’t be transparent; this is best-effort before streaming begins.
yindo added the pull-request label 2026-02-16 18:16:25 -05:00
yindo closed this issue 2026-02-16 18:16:25 -05:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: anomalyco/opencode#11566