Skip to content

Conversation

@jroth1111
Copy link

@jroth1111 jroth1111 commented Dec 18, 2025

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 [FEATURE]: Allow storage of secrets in system credential store. #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 [FEATURE]: Allow storage of secrets in system credential store. #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.

@jroth1111
Copy link
Author

Heads up: I tightened this PR further to maximize mergeability and keep it focused on the core value (multi-account OAuth subscription failover w/ same-request rotation).

Key points:

  • Only OAuth secrets use Bun.secrets (service opencode); API-key + wellknown creds stay in auth.json to avoid overlapping [FEATURE]: Allow storage of secrets in system credential store. #4318.
  • Same-request rotation: 429 (Retry-After cooldown) → rotate/retry; 401/403 → force refresh once, then rotate.
  • Refresh persistence: /auth/:providerID calls now update the correct OAuth record by matching the refresh token (so refresh/rotate doesn’t clobber the wrong account).
  • Tests cover 429 rotation + 401 refresh path.

If you’d prefer a different storage gate (flag/config) or want this to wait until #4318 lands, I can adjust.

@github-actions
Copy link
Contributor

Hey! Your PR title auth: multi-account OAuth subscription failover doesn't follow conventional commit format.

Please update it to start with one of:

  • feat: or feat(scope): new feature
  • fix: or fix(scope): bug fix
  • docs: or docs(scope): documentation changes
  • chore: or chore(scope): maintenance tasks
  • refactor: or refactor(scope): code refactoring
  • test: or test(scope): adding or updating tests

Where scope is the package name (e.g., app, desktop, opencode).

See CONTRIBUTING.md for details.

@jroth1111
Copy link
Author

Closing in favor of #8590 (OAuth Marathon).

Why superseded:

The core rotation logic and goals remain the same, but the implementation is more refined and maintainable.

@jroth1111 jroth1111 closed this Jan 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant