Multi-Provider Fallback
OpenAI is down. Anthropic has a 5xx storm. Your AI feature stays up because you fail over to whichever provider is healthy.
ReqLLM’s uniform provider:model API makes this a 20-line pattern: walk a list of model specs, return on first success, log every failure so you know which provider flaked. Combine it with a circuit breaker for production hardening.
defmodule MyApp.AI.Fallback do
@moduledoc """
Try a chain of models in order. Return the first success.
Reasonable defaults: a cheap-and-fast primary, a different-provider
secondary, and one last-resort tertiary. Different providers > same
provider's bigger model, because the most common failure is the
provider itself going down.
"""
require Logger
@default_chain [
"openai:gpt-4o-mini",
"anthropic:claude-haiku-4-5-20251001",
"google:gemini-2.5-flash"
]
def generate(messages, opts \\ []) do
chain = Keyword.get(opts, :chain, @default_chain)
do_generate(messages, chain, opts, [])
end
defp do_generate(_messages, [], _opts, errors) do
{:error, {:all_providers_failed, Enum.reverse(errors)}}
end
defp do_generate(messages, [model | rest], opts, errors) do
put_provider_key(model)
case ReqLLM.generate_text(model, messages, Keyword.drop(opts, [:chain])) do
{:ok, response} ->
{:ok, %{model: model, text: ReqLLM.Response.text(response)}}
{:error, reason} ->
Logger.warning("[AI.Fallback] #{model} failed: #{inspect(reason)}")
do_generate(messages, rest, opts, [{model, reason} | errors])
end
end
defp put_provider_key("openai:" <> _), do: ReqLLM.put_key(:openai_api_key, System.get_env("OPENAI_API_KEY"))
defp put_provider_key("anthropic:" <> _), do: ReqLLM.put_key(:anthropic_api_key, System.get_env("ANTHROPIC_API_KEY"))
defp put_provider_key("google:" <> _), do: ReqLLM.put_key(:google_api_key, System.get_env("GOOGLE_GEMINI_API_KEY"))
end
iex> MyApp.AI.Fallback.generate([%{role: "user", content: "hi"}])
{:ok, %{model: "openai:gpt-4o-mini", text: "Hi! How can I help?"}}
# When OpenAI is down, fallback kicks in transparently:
iex> MyApp.AI.Fallback.generate([%{role: "user", content: "hi"}])
# [warning] [AI.Fallback] openai:gpt-4o-mini failed: %ReqLLM.Error.API.Request{status: 503, ...}
{:ok, %{model: "anthropic:claude-haiku-4-5-20251001", text: "Hello! What's up?"}}
defmodule MyApp.AI.CircuitBreaker do
@moduledoc """
Stop hammering a dead provider. Track failures per model in ETS,
skip a model if it's failed N times in the last window.
"""
use GenServer
@table :ai_circuit_breaker
@window_ms 60_000
@threshold 5
def start_link(_), do: GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
def init(:ok) do
:ets.new(@table, [:named_table, :public, :set])
{:ok, :ok}
end
def healthy?(model) do
now = System.monotonic_time(:millisecond)
cutoff = now - @window_ms
case :ets.lookup(@table, model) do
[] -> true
[{^model, failures}] ->
recent = Enum.filter(failures, &(&1 > cutoff))
length(recent) < @threshold
end
end
def record_failure(model) do
now = System.monotonic_time(:millisecond)
:ets.update_counter(@table, model, {2, 0}, {model, []})
[{^model, failures}] = :ets.lookup(@table, model)
:ets.insert(@table, {model, [now | failures]})
end
end
Real-world failure modes this catches
- Provider full outage (rare but devastating, and it happens once or twice a year per provider).
- Regional rate limit ceilings, because most providers throttle per-org, not per-account.
- Specific model deprecation. An old model spec still works on a different provider while you migrate.
- Don't use this for cost optimization, it's overkill. Use the multi-provider recipe for static routing instead.