Error Handling¶
Continuum Router returns OpenAI-compatible JSON errors at the HTTP boundary and uses retries, active health filtering, and model fallback to contain transient backend failures.
Error response format¶
Most router-generated failures use this shape:
{
"error": {
"message": "The service is temporarily unavailable. Please try again later.",
"type": "service_unavailable",
"param": null,
"code": "service_unavailable"
}
}
All four members are always present, matching the OpenAI Error schema: message and type are strings, and param and code are a string or null rather than being omitted.
code is a stable machine-readable identifier, never the HTTP status. The status stays on the HTTP status line, where it always was. param names the offending request field when the upstream provider supplies one. A rate-limited response may also include Retry-After.
When an upstream provider supplies its own string code, that code passes through unchanged, so a client can still branch on values such as insufficient_quota or context_length_exceeded. The table below lists the codes the router itself authors.
Router-authored error codes¶
| Code | Meaning | Usual status |
|---|---|---|
invalid_request | The request is malformed or otherwise unacceptable | 400 |
invalid_api_key | Credentials are missing or invalid | 401 |
insufficient_permissions | The caller is authenticated but not allowed to use the resource | 403 |
model_not_found | The requested model is unknown to the router | 404 |
timeout | A router or backend deadline expired | 408, 504 |
request_too_large | The request body exceeded the configured limit | 413 |
rate_limit_exceeded | A router or provider rate limit refused the request | 429 |
internal_error | Router configuration or internal failure | 500 |
upstream_error | A backend answered with an error, or its stream or connection failed | 502 |
service_unavailable | No healthy backend could serve the request | 503 |
server_overloaded | The router is at its in-flight request ceiling | 503 |
Streaming failures that happen before any bytes are sent use an HTTP error response. Once an SSE response has started, the stream reports an error event and terminates; the status code can no longer be changed. On the Chat Completions wire the error event carries the same error object as the HTTP body. On the Responses wire it is the flat ResponseErrorEvent, with code, message, param, and sequence_number beside type.
Status codes¶
| Status | Typical cause | Retry guidance |
|---|---|---|
400 | Invalid request or provider rejected the parameters | Correct the request |
401 | Missing or invalid credentials | Correct authentication |
403 | Authenticated caller is not allowed to use the requested resource | Correct permissions or key policy |
404 | Requested model or stored response was not found | Check the identifier |
408 | Router request deadline expired | Retry only when safe for the operation |
413 | Request body exceeded the configured limit | Reduce the request size |
429 | Router/provider rate limit or exhausted quota | Honor Retry-After; quota failures are not retried automatically |
500 | Router configuration or internal failure | Inspect logs and configuration |
502 | Backend connection, stream, or gateway failure | Usually transient |
503 | Backend unavailable or all eligible backends unhealthy | Usually transient |
504 | Backend timeout | Usually transient |
Provider error bodies are normalized when possible. For unrecognized provider 4xx responses, the router keeps only a bounded message excerpt and does not expose arbitrary internal data.
Retries¶
The top-level retry section controls retries to the selected backend:
retry:
max_attempts: 3
initial_delay: 100ms
max_delay: 10s
backoff_multiplier: 2.0
jitter: true
retryable_status_codes: [429, 502, 503, 504]
retryable_errors: [ConnectionError, TimeoutError]
timeout: 30s
These are also the default values. The router applies exponential backoff with optional jitter. A provider 429 is retried only when it is classified as a transient rate limit; quota, credit, and billing exhaustion fail fast. Upstream Retry-After or Google retry-delay hints are honored after validation and are capped at 24 hours.
Retries repeat an attempt against a backend. They are distinct from fallback, which can select another model or provider after an eligible failure.
Circuit breaker¶
Continuum Router includes a configurable three-state circuit-breaker implementation and Admin controls:
circuit_breaker:
enabled: true
failure_threshold: 5
failure_rate_threshold: 0.5
minimum_requests: 10
timeout: 60s
half_open_max_requests: 3
half_open_success_threshold: 2
failure_status_codes: [500, 502, 503, 504]
timeout_as_failure: true
sliding_window_size: 60s
sliding_window_type: count_based
backends:
openai:
failure_threshold: 10
timeout: 120s
sliding_window_type accepts count_based or time_based. Entries below circuit_breaker.backends override only the supported per-backend fields.
The regular LLM proxy path records request outcomes in this state machine and consults it during backend selection: an open backend circuit is excluded from selection, and when every compatible circuit is open the router returns a 503 service-unavailable error and participates in configured fallback. It complements active health checks, retries, and fallback rather than replacing them. See Circuit Breaker for the outcome-recording and admission details.
When Admin API authentication permits access, operators can inspect or control breaker state through:
| Method | Path | Purpose |
|---|---|---|
GET | /admin/circuit/all | List all breaker states |
GET | /admin/circuit/{backend}/status | Get one backend's state |
POST | /admin/circuit/{backend}/open | Force the circuit open |
POST | /admin/circuit/{backend}/close | Force the circuit closed |
POST | /admin/circuit/{backend}/reset | Reset tracked breaker state |
See Admin API for authentication and complete endpoint details.
Model fallback¶
Fallback chains choose alternative models after configured failure conditions:
fallback:
enabled: true
mid_stream_enabled: true
fallback_chains:
gpt-5.4:
- gpt-5.4-mini
- claude-sonnet-4-6
fallback_policy:
trigger_conditions:
error_codes: [429, 500, 502, 503, 504]
timeout: true
connection_error: true
model_not_found: true
backend_unhealthy: true
max_fallback_attempts: 3
fallback_timeout_multiplier: 1.5
preserve_parameters: true
model_settings:
gpt-5.4:
fallback_enabled: true
notify_on_fallback: true
With notify_on_fallback: true (the default), successful fallback responses can contain:
X-Fallback-UsedX-Original-ModelX-Fallback-ModelX-Fallback-ReasonX-Fallback-Attempts— total model attempts including the primary; always at least 2 when a fallback served the response
Set notify_on_fallback: false for a model to suppress these headers while still serving the fallback.
The chain runs on every ingress that resolves a backend from the requested model: the OpenAI-shaped routes through the shared dispatch funnel, and /anthropic/v1/messages and /v1/responses through their own per-attempt selection and native dispatch, on both the non-streaming arm and the streaming arm before the first SSE byte. A streaming attempt whose handshake succeeded is committed and never hops. When the chain is exhausted or the failure is not a trigger, the error is the last attempt's own, in that ingress's dialect: an Anthropic {"type":"error",...} body on /anthropic/v1/messages, an OpenAI {"error":{...}} body on /v1/responses. /v1/responses/compact and count_tokens do not run a chain. See Model fallback for the per-ingress rules.
Cross-provider fallback keeps the request payload canonical and only swaps the model name; wire-format conversion for the newly selected backend happens at dispatch, not at the hop.
fallback_timeout_multiplier (1.0–5.0, default 1.5) scales each hop's own request timeout: the primary attempt keeps the timeout the configuration resolved for it, and hop n runs with base * multiplier^(n-1), clamped to the timeouts.limits ceiling for that timeout class. The base is whatever that attempt would have used without a chain, so a longer per-model budget stays a longer budget on the hop. It does not scale timeouts.connection, the wait for a fallback dial permit, a within-hop retry, or a mid-stream hop after the client already holds the stream. It is a different knob from timeouts.streaming_fallback_budget_multiplier, which sizes the single wall-clock budget shared by every attempt of a streaming request; the two compose, because a streaming hop is scaled first and then capped at what is left of that budget.
mid_stream_enabled: true permits mid-stream recovery where the streaming path supports it. It may buffer output, so disable it when immediate token delivery is more important than recovery after a stream has begun.
Timeouts and request size¶
Configure router and model deadlines in the timeouts section and backend request limits in the documented server/backend sections. Avoid making retry timeouts longer than the caller's total deadline; the caller can otherwise disconnect while retry work continues.
Use the generated configuration as the canonical field reference:
Observability and diagnosis¶
Use implemented interfaces rather than undocumented debug endpoints:
# Public process/backend health
curl http://localhost:8080/health
# Admin health and circuit state (add configured admin credentials)
curl http://localhost:8080/admin/health
curl http://localhost:8080/admin/circuit/all
# Validate configuration without starting the server
continuum-router config validate /etc/continuum-router/config.yaml
Prometheus metrics are exposed at the configured metrics path when the metrics feature and runtime metrics configuration are enabled. Logs use tracing; select verbosity with logging.level, CONTINUUM_LOG_LEVEL, or RUST_LOG as appropriate.
The router does not provide /admin/errors/* or /admin/debug/error endpoints. Inspect structured logs, metrics, /admin/health, and /admin/circuit/* instead.
Client recommendations¶
- Set a caller-side deadline longer than the expected router/backend timeout.
- Retry only idempotent operations unless your application has deduplication.
- Use exponential backoff with jitter and honor
Retry-After. - Do not retry authentication, permission, validation, or quota-exhaustion errors without changing the request or credentials.
- Record a correlation/request identifier from your own client logs and avoid logging API keys or resolved configuration output.