Skip to content

[FEATURE]: Configurable model fallback on transient errors and timeouts #48991

Open

Description

@AndyS77

Feature hasn't been suggested before.

  • I have verified this feature I'm about to request hasn't been suggested before.

Describe the enhancement you want to request

Supersedes #7602 (Jan 2026) and the closed PR #20105 (Mar 2026, auto-cleaned). Incorporates lessons learned from #20105 and aligns with the current Effect-based session architecture.

Problem

When a model is unreachable, rate-limited, or times out, the session retry policy (session/retry.ts) retries the same model on the same provider with exponential backoff. After retries are exhausted, halt() is called and the session dies. There is no mechanism to fall back to a different model.

This causes long-running agent workflows to fail on transient provider issues that a different provider would handle fine.

Proposed Behavior

After the retry policy exhausts for a transient error (5xx, 429, network failure, timeout), instead of halting the session:

  1. Look up the configured fallback model(s) for the current model
  2. If a fallback exists and is available, switch to it and re-run the LLM stream
  3. Track which fallback was used for status reporting
  4. Emit a status event so the UI can show the model switch
  5. If the fallback also fails, try the next in the chain
  6. If all fallbacks are exhausted, halt as before

Triggers

  • Rate limits (429, Too Many Requests)
  • Server errors (500, 502, 503)
  • Network failures (ECONNREFUSED, ECONNRESET, ETIMEDOUT, fetch failed)
  • Timeouts (header timeout, stream timeout — the primary use case)
  • Model unavailable (404, model not found)

Not triggered by:

  • Auth errors (401 — different provider has different creds)
  • Context overflow (413 — needs compaction, not a model switch)
  • Validation/prompt errors

Config Schema

Aligns with the existing provider.models config structure:

{
  "provider": {
    "anthropic": {
      "models": {
        "claude-sonnet-4": {
          "fallback": ["openai/gpt-5", "google/gemini-3-pro"]
        }
      }
    },
    "openai": {
      "models": {
        "gpt-5": {
          "fallback": ["anthropic/claude-sonnet-4", "google/gemini-3-pro"]
        }
      }
    }
  }
}

Each entry in the fallback array is a "provider/model-id" string. The array defines an ordered fallback chain — tried in sequence until one succeeds.

Architectural Notes

PR #20105 implemented fallback as LLM middleware via wrapLanguageModel. This approach has two issues:

  1. Interacts badly with the session retry loop — middleware runs inside the AI SDK's own retry, which conflicts with the Effect-based SessionRetry.policy
  2. Only 1 fallback — hardcoded Copilot-to-Bedrock mapping, not a configurable chain

The recommended approach is to implement fallback at the session processor layer (processor.ts), after Effect.retry(SessionRetry.policy(...)) exhausts:

Effect.retry(SessionRetry.policy(...))
  -> Effect.catch(fallback)   // instead of Effect.catch(halt)
  -> Effect.catch(halt)       // if all fallbacks exhausted

This keeps fallback logic in the Effect-based session pipeline where the error context is richest.

References

Activity

  1. hd-o commented on Sep 16, 2026

    @hd-o

    How about setting the models on top-level or agent?

    {
      "model": [
        "anthropic/claude-sonnet-5",
        "openai/gpt-5"
      ]
    }
    {
      "agent": {
        "plan": {
          "model": [
            "anthropic/claude-sonnet-5",
            "openai/gpt-5"
          ]
        }
      }
    }
  2. tea0112 commented on Oct 22, 2026

    @tea0112

    I need this because I have many providers — why bother respecting rate limits when we can automatically switch to another?

  3. opencodejr commented on Oct 28, 2026

    @opencodejr

    Thanks for the detailed write-up, @AndyS77! You're asking for a configurable, ordered chain of fallback models per model. When retries on the current model run out for a transient error (rate limit, 5xx, network failure or timeout), OpenCode would switch to the next model in the chain instead of stopping the session, and show the switch in the UI.

    Today v2 retries the same model with backoff and then stops. The plugin session.retry hook can only decide whether to retry and how long to wait, not which model to use. So this can't be built cleanly as a plugin yet. A small addition would make it possible: letting the retry decision also name a replacement model for the next attempt.

    Maintainers will consider this request. Related issues, which you already mentioned:

  4. moritzWa commented on Oct 30, 2026

    @moritzWa

    Use case that isn't covered here: the fallback also needs to fire when the provider's auth refresh gets throttled or its credits/quota run out ("The usage limit has been reached"), not only on 5xx/429/timeouts. Same model on another provider has separate creds, so 401 is fine to fall back on when the subscription itself is the problem.

    Also useful: once a provider reports it's out of quota, skip it for a while (I use 30 min) instead of retrying it on every turn. Without that, every chat in a fallback chain burns a failed attempt on the dead provider first.

    I run this today as a local plugin (ordered chain, cooldown on exhausted providers) and can port it to core if the processor-layer approach in the description is still the plan.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions