Model Fallback

Keep your agent answering when a model is overloaded by automatically falling back to alternates

Model Fallback keeps your agent answering by automatically retrying on alternate models when the primary model is overloaded or unavailable.

python
from praisonaiagents import Agent
from praisonaiagents.config import LLMConfig

agent = Agent(
    name="assistant",
    instructions="You are a helpful assistant",
    model=LLMConfig(
        model="gpt-4o",
        fallback_models=["anthropic/claude-3-5-sonnet", "gpt-4o-mini"],
    ),
)
agent.start("Answer even when the primary model is overloaded")

The user sends a prompt; the agent retries on fallback models when the primary returns errors.

Diagram

Model Fallback

503 / overloaded

still failing

ok

User

Agent

gpt-4o

claude-3-5-sonnet

gpt-4o-mini

Reply

Quick Start

  1. One-line resilience
    python
    from praisonaiagents import Agent
    from praisonaiagents.config import LLMConfig
    
    agent = Agent(
        instructions="You are a helpful assistant",
        model=LLMConfig(
            model="gpt-4o",
            fallback_models=["claude-3-5-sonnet", "gpt-4o-mini"],
        ),
    )
    agent.start("Summarise today's news")
  2. Cross-provider chain

    Use LiteLLM-style prefixes when mixing providers:

    python
    from praisonaiagents import Agent
    from praisonaiagents.config import LLMConfig
    
    agent = Agent(
        model=LLMConfig(
            model="openai/gpt-4o",
            fallback_models=["anthropic/claude-3-5-sonnet", "openai/gpt-4o-mini"],
        ),
    )
  3. Notice the switch

    Subscribe to HookEvent.MODEL_FALLBACK to react the moment the primary model is swapped out:

    python
    from praisonaiagents import Agent
    from praisonaiagents.config import LLMConfig
    from praisonaiagents.hooks import HookRegistry, HookEvent, HookResult
    
    registry = HookRegistry()
    
    @registry.on(HookEvent.MODEL_FALLBACK)
    def notice(evt):
        print(f"[fallback] {evt.from_model} → {evt.to_model} ({evt.reason_category})")
        return HookResult.allow()
    
    agent = Agent(
        instructions="You are a helpful assistant",
        model=LLMConfig(
            model="gpt-4o",
            fallback_models=["claude-3-5-sonnet", "gpt-4o-mini"],
        ),
        hooks=registry,
    )
    agent.start("Answer even during a provider outage")

How It Works

On transient errors (503, timeout, model overloaded), the agent retries the same turn against the next model in fallback_models. Successful calls stay on the primary model.

Failover fires on retryable errors classified by the LLM error classifier and covers every turn shape — non-streaming, streaming, tool-iteration turns, reflection turns, and their async equivalents. A 503 on a streaming chunk pushes the same turn to the next model in fallback_models; the user sees continuous output, not a failure.

Diagram
Fallback LLMPrimary LLMAgentUserFallback LLMPrimary LLMAgentUsererror classifier: should_fallback_modelpromptchat completion503 overloadedretry same messagesresponsereply

Observing the Switch

Subscribe to HookEvent.MODEL_FALLBACK (or the MODEL_FALLBACK stream event) to react the moment the primary model is swapped out — for alerts, metrics, a UI notice, or a per-user notification.

Diagram
Fallback (claude-3-5-sonnet)Primary (gpt-4o)AgentUserFallback (claude-3-5-sonnet)Primary (gpt-4o)AgentUseremit MODEL_FALLBACK(from_model, to_model, reason_category, fallback_index)promptchat completion503 unavailableretry same turnresponsereply

ModelFallbackInput fields

FieldTypeDefaultDescription
from_modelstr""Model id the turn was using when the failure hit (e.g. "gpt-4o").
to_modelstr""Next entry in the fallback_models chain that the turn continues on.
reason_categorystr""Failure classification from the LLM error classifier (e.g. "rate_limit", "provider_down", "timeout"). Provider internals stay redacted.
fallback_indexint00-based index into fallback_models of the entry now in use.
session_id, cwd, event_name, timestamp, agent_name——Standard HookInput fields.

Hook subscription

python
from praisonaiagents import Agent
from praisonaiagents.config import LLMConfig
from praisonaiagents.hooks import HookRegistry, HookEvent, HookResult

registry = HookRegistry()

@registry.on(HookEvent.MODEL_FALLBACK)
def on_fallback(evt):
    print(f"[fallback] {evt.from_model} → {evt.to_model}")
    print(f"           reason={evt.reason_category} index={evt.fallback_index}")
    return HookResult.allow()

agent = Agent(
    instructions="You are a helpful assistant",
    model=LLMConfig(
        model="gpt-4o",
        fallback_models=["claude-3-5-sonnet", "gpt-4o-mini"],
    ),
    hooks=registry,
)
agent.start("Answer even during a provider outage")

Stream event

When a stream callback is active, the same swap emits a StreamEventType.MODEL_FALLBACK event carrying the four fields in metadata, plus agent_id, session_id, and run_id.

python
from praisonaiagents import Agent
from praisonaiagents.config import LLMConfig
from praisonaiagents.streaming import StreamEvent, StreamEventType

agent = Agent(
    instructions="You are a helpful assistant",
    model=LLMConfig(
        model="gpt-4o",
        fallback_models=["claude-3-5-sonnet", "gpt-4o-mini"],
    ),
    stream=True,
)

def on_event(event: StreamEvent):
    if event.type == StreamEventType.MODEL_FALLBACK:
        m = event.metadata
        print(f"[stream] switched {m['from_model']} → {m['to_model']} ({m['reason_category']})")

agent.stream_emitter.add_callback(on_event)
agent.start("Answer even during a provider outage")

Sync and async paths

Both call sites emit — the sync _chat_completion path and the async _handle_async_llm_error path — so the hook fires whether you run agent.start(...) or await agent.astart(...).

Sync path

python
@registry.on(HookEvent.MODEL_FALLBACK)
def on_fallback(evt):
    print(f"[fallback] {evt.from_model} → {evt.to_model}")
    return HookResult.allow()

agent.start("Answer even during a provider outage")

Async path

python
@registry.on(HookEvent.MODEL_FALLBACK)
async def on_fallback(evt):
    print(f"[fallback] {evt.from_model} → {evt.to_model}")
    return HookResult.allow()

await agent.astart("Answer even during a provider outage")

Shared-Agent async safety

On a shared Agent, a fallback on one turn does not affect the model, cost, or observability attribution of concurrent turns.

Many users share a single Agent across asyncio.gather (see Thread-Safe Agent State → Chat History). Before this fix, a mid-fallback shared-state mutation could silently flip a concurrent turn to the fallback model — wrong cost, wrong logs, wrong hook payloads. Both paths now keep each turn on its own model.

python
import asyncio
from praisonaiagents import Agent
from praisonaiagents.config import LLMConfig

agent = Agent(
    instructions="You are helpful.",
    model=LLMConfig(
        model="gpt-4o",
        fallback_models=["claude-3-5-sonnet"],
    ),
)

async def main():
    # Same Agent, two concurrent turns.
    # If turn A hits a 503 and falls back to claude-3-5-sonnet,
    # turn B still dispatches on gpt-4o with correct cost/observability.
    await asyncio.gather(
        agent.astart("Summarise today's news"),
        agent.astart("Draft a follow-up email"),
    )

asyncio.run(main())
Diagram
claude-3-5-sonnetgpt-4oShared AgentUser (Turn B)User (Turn A)claude-3-5-sonnetgpt-4oShared AgentUser (Turn B)User (Turn A)build throwaway dispatcherbound to claude-3-5-sonnetshared self.llm / dispatcher untouchedturn B keeps gpt-4o attributionpar​prompt Aprompt Bchat completion (turn A)503 unavailableretry turn A (per-call dispatcher)reply Achat completion (turn B)reply Breply Areply B

Async path stays clean. The fallback retry runs against a throwaway per-call dispatcher bound to the fallback model; the shared model state is never mutated, so a concurrent turn keeps dispatching on its original model even while this turn is suspended on an await. No lock is taken on the async path for ordinary turns — non-fallback turns stay fully concurrent.

Sync path stays clean. The sync fallback still recurses through the full chat wrapper (which reads the current model for BEFORE_LLM hook attribution and budget), so it temporarily swaps the model. That mutate/restore window is now serialised by a lazily-created per-instance lock; only fallback turns contend on it — non-fallback turns never acquire it.

Custom LLMs & provider/model strings

Fallback works for every way of passing an LLM — provider/model strings, llm={...} dicts, agents built with a base_url, and model objects — on sync, async, and streaming turns.

provider/model string

python
from praisonaiagents import Agent
from praisonaiagents.config import LLMConfig

agent = Agent(
    instructions="You are a helpful assistant",
    llm=LLMConfig(
        model="anthropic/claude-3-5-sonnet",
        fallback_models=["openai/gpt-4o-mini"],
    ),
)
agent.start("Answer even during an Anthropic outage")

Custom base_url

python
from praisonaiagents import Agent
from praisonaiagents.config import LLMConfig

agent = Agent(
    instructions="You are a helpful assistant",
    llm=LLMConfig(
        model="model-a",
        fallback_models=["model-b"],
        base_url="http://localhost:11434/v1",  # kept for bare-name fallbacks
        api_key="ollama",
    ),
)
agent.start("Hello from the custom server")

Model object

python
from praisonaiagents import Agent
from praisonaiagents.llm import LLM

agent = Agent(instructions="You are a helpful assistant", llm=LLM(model="ollama/llama3"))
agent.fallback_models = ["openai/gpt-4o-mini"]  # set after construction for model objects
agent.start("Answer even if the primary is overloaded")
Diagram
openai/gpt-4o-minianthropic/claude-3-5-sonnetAgentUseropenai/gpt-4o-minianthropic/claude-3-5-sonnetAgentUsershould_fallback_model?build throwaway LLMemit MODEL_FALLBACK“Summarise today’s news”get_response503 overloadedretry same messagesresponseanswer from gpt-4o-mini
Diagram

string, dict, or base_url

Model object

How are you passing the model?

LLMConfig(model=…, fallback_models=[…])

agent.fallback_models = […] after Agent(llm=obj)

Configuration Options

Model Fallback is configured through LLMConfig — set model for the primary and fallback_models for the ordered backup chain.

python
from praisonaiagents import Agent
from praisonaiagents.config import LLMConfig

agent = Agent(
    instructions="You are a helpful assistant",
    model=LLMConfig(
        model="gpt-4o",
        fallback_models=["claude-3-5-sonnet", "gpt-4o-mini"],
    ),
)
agent.start("Hello!")
OptionTypeDefaultDescription
modelstr—Primary model the turn starts on.
fallback_modelsOptional[List[str]]NoneOrdered backup chain; the runtime advances to the next entry on a retryable failure.

For model-object agents (llm=my_llm), assign agent.fallback_models = [...] after construction. Entries can be model-id strings or ready-made LLM instances. A string entry becomes a fresh LLM(model=...) that reads credentials from the provider’s environment variable; pass an LLM(...) instance instead when the fallback needs its own base_url or api_key.

The decision diagram below shows which chain shape fits which scenario.

Diagram

No

Yes

Yes

No

Need resilience on provider outages?

Agent model=str

Different providers?

Cross-provider chain

Cost-degradation chain

LLMConfig API Reference

Full LLMConfig surface is covered by the auto-generated SDK reference — see Hook Events for the paired MODEL_FALLBACK hook.

Best Practices

Put a cheap same-provider fallback last

Useful for rate limits, not full provider outages — a cheap model on the same API may still fail if the provider is down.

Order by latency and cost

Fallback runs the same prompt; a much weaker model may return a worse answer, not a missing one.

Limit chain length to 2–3

Longer chains delay user-visible errors without improving success rates much.

Use provider prefixes when mixing

LiteLLM-style names (anthropic/..., openai/...) route credentials correctly across providers.

Related