- Retry with exponential backoff — repeats a failed request against the same provider with growing delays.
- Circuit breaker — short-circuits calls to a provider that has been failing repeatedly, then probes once the timeout elapses.
Defaults
The defaults are tuned to be safe for most deployments. Override only what you need.| Setting | Default | Notes |
|---|---|---|
max_retries | 3 | Maximum retry attempts per request |
initial_backoff | 1s | First retry wait |
max_backoff | 30s | Upper cap on retry wait |
backoff_factor | 2.0 | Exponential multiplier between retries |
jitter_factor | 0.1 | Random jitter as a fraction of the backoff |
failure_threshold | 5 | Consecutive failures before the circuit opens |
success_threshold | 2 | Consecutive successes to close it again |
timeout | 30s | How long the circuit stays open before probing |
max_retries: 0 gives every
request exactly one attempt, and failure_threshold: 0 disables the
circuit breaker entirely. Both work globally or per provider.
What counts as a failure
Retries fire on transport errors (connection refused, resets, DNS failures) and on429, 502, 503, and 504 responses. Other statuses —
including 500 — are returned to the caller without retrying. Two paths
retry less than the table suggests:
- Streaming requests are never retried once dispatched, because partial data may already have been sent.
- Passthrough requests are retried only when they are replay-safe:
GET,HEAD,OPTIONS,PUT, or any request carrying anIdempotency-Keyheader.
5xx responses
(including non-retried 500s) as failures. It deliberately ignores:
429rate limits — a rate-limited provider is up, so the backpressure signal is passed through instead of opening the circuit. The one exception is a429on a half-open probe, which reopens the circuit.4xxresponses — the provider answered; the request was at fault.- Requests canceled by the client — a disconnect says nothing about provider health. Client-side timeouts do count as failures.
503 and the message
circuit breaker is open - provider temporarily unavailable. After
timeout elapses, a single probe request is let through while concurrent
requests keep failing fast; success_threshold consecutive successful
probes close the circuit, and any probe failure reopens it.
The circuit breaker is in-memory and per gateway process: each provider
gets its own breaker, state resets on restart, and nothing is shared
between replicas.
Environment Variables
These set the global defaults that apply to every provider unless overridden in YAML.Retry
| Variable | Type | Default | Description |
|---|---|---|---|
RETRY_MAX_RETRIES | int | 3 | Maximum retry attempts per request |
RETRY_INITIAL_BACKOFF | duration | 1s | First retry wait (e.g. 500ms, 2s) |
RETRY_MAX_BACKOFF | duration | 30s | Upper cap on retry wait |
RETRY_BACKOFF_FACTOR | float | 2.0 | Exponential multiplier between retries |
RETRY_JITTER_FACTOR | float | 0.1 | Random jitter as a fraction of the backoff |
Circuit Breaker
| Variable | Type | Default | Description |
|---|---|---|---|
CIRCUIT_BREAKER_FAILURE_THRESHOLD | int | 5 | Consecutive failures before opening |
CIRCUIT_BREAKER_SUCCESS_THRESHOLD | int | 2 | Consecutive successes to close again |
CIRCUIT_BREAKER_TIMEOUT | duration | 30s | How long the circuit stays open before probing |
YAML
The same fields are available under the globalresilience: block, and can
be overridden per provider:
resilience: block are
overridden. Everything else inherits from the global section, which in turn
inherits from the built-in defaults.
Per-provider tuning must come from YAML. Environment variables set
global defaults only —
RETRY_MAX_RETRIES cannot target a single provider.
See config.yaml gotchas.Worked example
Given the YAML above, the effective per-provider settings are:| Provider | max_retries | failure_threshold | cb timeout |
|---|---|---|---|
| openai | 2 (global) | 3 (global) | 15s (global) |
| anthropic | 5 (override) | 3 (global) | 15s (global) |
| ollama | 2 (global) | 10 (override) | 5s (override) |
anthropic and ollama inherit every field they did not explicitly override.
Circuit breaker vs. dashboard provider health
The dashboard’s provider status (Healthy / Degraded / Unhealthy and the “Last checked” timestamp) is not the circuit breaker. Circuit breaker state is not shown in the dashboard and recovers on its own withintimeout (default 30s) once the provider is back; when
metrics are enabled it is exported as
the gomodel_circuit_breaker_state gauge (0 = closed, 1 = half-open,
2 = open).
Dashboard health reflects model discovery: whether the provider’s model
inventory could last be fetched. Providers are re-checked:
- at startup,
- on every model registry refresh, controlled by
CACHE_REFRESH_INTERVAL(seconds, default3600— hourly), - on the fast recheck loop, which re-probes only providers whose latest
refresh failed, controlled by
PROVIDER_RECHECK_INTERVAL(cache.model.recheck_intervalinconfig.yaml; seconds, default60;0disables), and - on demand, when a request asks for a provider-qualified model
(
provider/model) that is missing from the registry.
What happens while a provider is down
When a provider’s refresh fails, its previously discovered models are kept and marked stale, and the dashboard shows the provider as Degraded (“previous inventory is still available”):- Direct requests to its models still resolve and are sent to the
provider, so callers get an honest
502/503(and manual failover rules can fire) instead of a misleading “model not found”. - Virtual-model redirects skip the provider’s targets, so a load-balanced redirect keeps working through its healthy targets.
PROVIDER_RECHECK_INTERVAL seconds, updating “Last checked” and restoring
normal routing typically within a minute of the provider coming back. A
provider that was already down at startup (nothing discovered yet) shows as
Unhealthy until its first successful fetch.