Skip to content

feat(routing): automatic multi-credential failover on 429 quota exhaustion via credential.activate #6

Closed

Description

@dezren39

Context & Background

In OpenCode upstream commit d78267503d (feat(plugin): expose credential.activate to server plugins (#52797)), upstream added a server-side API allowing plugins to programmatically activate and switch stored credentials for any provider.

In our fork (developing-today/opencode), we implemented intelligent model routing with tier awareness (cheap, standard, frontier), quota tracking, rate limit monitoring, and automated fallback. However, currently when a credential hits a 429 rate limit or its usage window is exhausted, routing can only failover to a different model or provider, or ask the user to switch accounts manually.

Feature Objective

Implement automatic credential failover within our routing engine: when a request fails with an HTTP 429 or quota limit, rather than immediately degrading to a lower-tier model or prompting the user, the router should check for secondary stored credentials for the same provider, activate the next available credential via credential.activate, and retry the request seamlessly within the same turn.


Technical Specifications

1. Credential Inventory & Status Detection

  • Upstream provides server.credential.list() and server.credential.activate(id) or plugin hooks under packages/plugin/src/ and packages/core/src/.
  • In our routing controller (packages/core/src/model-route.ts / packages/core/src/model-resolver.ts):
    • Inspect configured credentials for the target provider (e.g. anthropic, openai, google).
    • Track rate-limit backoff timers or quota windows per credential ID.

2. Automatic Failover Sequence in Runner

  • In packages/core/src/session/runner/llm.ts / packages/core/src/session/runner/step.ts:
    • When an LLM request returns an error tagged with 429 (error_status === 429 or error message contains rate-limit/quota exhaustion):
    • Check if alternative credentials exist for provider_id.
    • If an alternative valid credential is found:
      1. Call credential.activate(nextCredentialId).
      2. Record a failover note in route_attempt (failed_over_to or metadata indicating credential switch).
      3. Re-issue the request without interrupting the user turn.
    • If no alternative credential exists, fall back to the existing route fallback chain (alternate provider or prompt user).

3. Quota Cooldown & Cooldown Window Expiration

  • Maintain in-memory and persistent cooldown timestamps for credentials that hit rate limits (e.g. using route_health in packages/core/src/model-route-log/sql.ts).
  • Avoid switching back to a cooled-down credential until its window has reset (or after standard exponential backoff).

Key Files & References

  • packages/plugin/src/effect/plugin.ts & packages/plugin/src/promise/plugin.ts: credential.activate interface
  • packages/core/src/model-route.ts: Model route resolution, target selection, and fallback lists
  • packages/core/src/session/runner/llm.ts: Request dispatch, error handling, retry loops
  • packages/core/src/database/schema.gen.ts: route_attempt and route_health tables

Acceptance Criteria

  1. When multiple credentials exist for a provider and the active one receives a 429, the router activates an alternate credential without failing the turn.
  2. The retry attempt successfully completes and records failed_over_to: "credential:<id>" in route_attempt.
  3. If all credentials for the provider are exhausted, the router proceeds to the next provider in the route definition or triggers standard error handling.
  4. Unit tests in packages/core/test/ verify credential failover behavior with mocked provider responses.

Activity

  1. added
    kind/featureNew feature or capability request
    source/humanFiled directly by a human
    priority/normalOrdinary priority (the default)
    effort/lLarge: multi-day or open-ended; expect staged check-ins
    app/corePackage: core (pkgs/ or apps/ in this repo)
    meta/enrichedIntake review done: acceptance criteria, links, and normalized labels added
    on Oct 27, 2026
  2. dezren39 commented on Oct 27, 2026

    @dezren39
    Author

    Intake review

    Still live (verified this run): the upstream capability exists in the fork — packages/core/src/credential.ts implements credential activation (log lines "credential activated" / "credential activate failed") — but no caller in the routing layer uses it: searching packages/core/src finds no credential-activate integration in model routing or the session runner's retry path. Failover on 429 still degrades model/provider or waits for a manual switch, exactly as filed.

    Related: opencode #9 (prompt-cache optimization across pools — the other pool-behaviour item; cache state interacts with credential switching), opencode #519/#530 (route governance correctness — separate defect class, same subsystem).

    Notes / risks: design-first, effort/l. Two risks to settle in design before code: (1) retrying on a second credential within the same turn must not double-bill a request the provider actually processed; (2) quota state per credential must be tracked separately or the router will re-pick the exhausted credential. The body's acceptance criteria are already present and good.

    Suggested approach: design pass on where the failover decision lives (router vs runner retry), then implement behind the existing quota/rate-limit signals with per-credential attempt records in route_attempt.

    — fleet monitor intake (repo-fleet-monitor, 2026-10-26 20:19 CDT run)

  3. added
    queue/claimedA queue worker has claimed the task for this item
    work/assignedAn agent holds this item via the queue (claimed task)
    on Oct 28, 2026
  4. dezren39 commented on Oct 28, 2026

    @dezren39
    Author

    agent-note:v1 — task 2026 triage verdict: CONFIRMED, still live on current main (agent repo-queue-worker-shift-opencode, lane A)

    Re-checked at HEAD 8f2dced (current main, 2026-10-28):

    • The capability exists exactly as filed: packages/core/src/credential.ts:56 defines activate: (id: ID) => Effect.Effect<void>, wired at packages/core/src/integration.ts:805 and exposed to plugins at packages/core/src/plugin/host.ts:260.
    • No routing or retry caller uses it: searching packages/core/src for credential-activate callers outside credential.ts/integration.ts/plugin host finds none in model routing or the session runner.
    • What routing does on quota today: packages/core/src/model-route.ts records quota errors per target (lines 255-295, 361-374) and demotes/cool-downs the model/provider target (quotaCooldownMs, demote-to-end) — failover degrades to another model/provider or waits; it never switches the credential on the same provider. That is precisely the gap this issue describes.

    Disposition: NOT enqueuing a small_fix follow-up, deliberately. This is effort/l and design-first (see the 2026-10-26 intake review): before code, a design pass must settle (1) where the failover decision lives (router vs runner retry), (2) the double-billing guard for retrying on a second credential within the same turn, and (3) per-credential quota tracking so the router does not re-pick the exhausted credential. A bounded small_fix packet would mis-scope all three. The issue stays open with its existing acceptance criteria as the implementation brief once the design choices are made; recorded in the fleet backlog as awaiting a design pass.

  5. 53 remaining items

  6. dezren39 commented on Oct 29, 2026

    @dezren39
    Author

    Task 3290 resumed by lane A: PR #854 advanced to head 9033b30.

  7. added
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    status/parkedDeliberately paused with a recorded resume condition
    and removed
    queue/claimedA queue worker has claimed the task for this item
    on Oct 29, 2026
  8. dezren39 commented on Oct 29, 2026

    @dezren39
    Author

    Lane A resume check (task 3290), 2026-10-29: PR #854 head 9033b30 is MERGEABLE; Codex review completed with no unresolved findings. CI still pending (windows unit in progress, remaining checks queued amid repo-wide congestion), so the task is re-parked. One pre-merge item: a CodeRabbit minor thread on credentialFallback (leaves the last failed candidate active when all candidate loads fail) was verified and answered on the PR — the merging lane must disposition it (fix + test, or filed follow-up) before merging.

  9. dezren39 commented on Oct 29, 2026

    @dezren39
    Author

    Done: PR #854 merged as 427f20399e (merge commit) on 2026-10-29T18:13:48Z; this issue closes with it.

    Merge gate: all checks green on head 9033b3074c (typecheck, affected packages, unit linux/windows, e2e, standards, compliance; windows unit needed one rerun — single flaky PTY test "detached PTY hosts interrupt commands without closing the shell", unrelated). Codex review on the head had no unresolved findings; every review thread is resolved. The last open thread (CodeRabbit, credentialFallback leaves the last failed credential active when all candidates fail) was dispositioned as deferred to follow-up #870 item 6 (comment 6504470178) — not a blocker for this merge.

  10. removed
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    work/assignedAn agent holds this item via the queue (claimed task)
    user/reviewNeeds a user-look: Muse reviews first and handles what it can
    on Oct 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    app/corePackage: core (pkgs/ or apps/ in this repo)effort/lLarge: multi-day or open-ended; expect staged check-inskind/featureNew feature or capability requestmeta/enrichedIntake review done: acceptance criteria, links, and normalized labels addedmeta/protocol-gapAn agent touched this without the required agent-note comment or labels; needs review of whypriority/criticalDrop everything: main broken, security exposure, repo-wide outagesource/humanFiled directly by a humanstatus:done

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions