Documentation
Errors
Every INFRO error uses one JSON shape. Status codes and type strings, which failures to retry with backoff, and how errors surface mid-stream.
Every API error is JSON with a single top-level error object and a meaningful HTTP status code. The status tells you whether a retry can help; the type string is stable and safe to branch on. The shape is identical across every endpoint, including chat completions.
Because INFRO fails over between providers automatically, most upstream errors never reach you. What does surface falls into the small set of statuses below.
Error response body
{
"error": {
"message": "Rate limit exceeded: this key allows 200 requests per minute.",
"type": "rate_limit_exceeded",
"code": null
}
}error.messagestringrequired- Human-readable description of the failure. Useful in logs; do not branch on it — the wording can change.
error.typestringrequired- Stable, machine-readable category. One type per status code — see the table below. Branch on this or on the HTTP status.
error.codestring | nullrequired- An upstream provider code when one exists, such as a moderation code on a 403. Often null.
Status codes
| Status | Type | When it occurs | Retry |
|---|---|---|---|
| 400 | invalid_request_error | Malformed JSON, unknown parameter, invalid message structure, or an oversized image | No |
| 401 | authentication_error | Missing, malformed, or revoked API key | No |
| 402 | insufficient_credits | Account balance exhausted, or the key hit its spend limit | No |
| 403 | permission_denied | The key lacks access, or a provider's content moderation blocked the request | No |
| 404 | model_not_found | The model ID does not exist in the catalog | No |
| 408 | request_timeout | No provider produced a response in time | Yes |
| 429 | rate_limit_exceeded | Per-key request rate exceeded; the response includes a Retry-After header | Yes |
| 502 | upstream_error | Every provider for the requested model returned an error | Yes |
| 503 | no_available_provider | No provider is currently able to serve the requested model | Yes |
For 404, confirm the model ID against GET /v1/models or the model catalog — IDs use the vendor/model-name form, like anthropic/claude-sonnet-5.
Retry rules
- Retry 408, 429, 502, and 503 with exponential backoff and jitter. These are transient: a later attempt can land on a healthy provider or a fresh rate-limit window.
- On 429, honor the
Retry-Afterheader over your computed delay — it says exactly when capacity frees up. TheX-RateLimit-*headers on every response are covered in Rate limits. - Never retry 400, 401, 402, or 404. They are deterministic — the same request fails the same way. Fix the request, the key, or the model ID; for 402, add credits or raise the key's spend limit in the console.
403 sits in between: retrying the identical request will not help, but if provider content moderation was the cause, rewording the content might. Treat it as non-retryable in automated code.
Automatic provider failover
When a provider returns 429 or 5xx or times out before the first token, INFRO silently reroutes the request to the next provider for the same model. The response's top-level provider field tells you who actually served it. You only see 502 when every provider for the model errored, and 503 when none is available at all.
Failover is provider-level and automatic. For model-level resilience, pass an ordered fallbacks list so the request can complete on a different model when the primary is fully down — see Fallbacks. You can also constrain which providers are eligible with routing.
A 502 means INFRO already tried every provider for that model, so an immediate retry rarely helps. Back off first, or configure fallbacks so the request lands on another model instead of failing.
Retrying with backoff
import os
import random
import time
import requests
RETRYABLE = {408, 429, 502, 503}
def chat(payload: dict, max_retries: int = 5) -> dict:
headers = {"Authorization": f"Bearer {os.environ['INFRO_API_KEY']}"}
for attempt in range(max_retries + 1):
resp = requests.post(
"https://api.infro.io/v1/chat/completions",
headers=headers,
json=payload,
timeout=120,
)
if resp.ok:
return resp.json()
if resp.status_code not in RETRYABLE or attempt == max_retries:
resp.raise_for_status()
# Honor Retry-After on 429; otherwise exponential backoff with jitter
retry_after = resp.headers.get("Retry-After")
if retry_after:
delay = float(retry_after)
else:
delay = min(2 ** attempt, 30) + random.uniform(0, 1)
time.sleep(delay)
completion = chat({
"model": "openai/gpt-5-mini",
"messages": [{"role": "user", "content": "Ping"}],
})
print(completion["choices"][0]["message"]["content"])Errors mid-stream
Failover and fallbacks only apply before the first token — once a provider begins streaming, it cannot be swapped out. If it drops mid-stream, the SSE stream delivers an event whose payload is the standard error object instead of a chat.completion.chunk:
data: {"error": {"message": "Provider disconnected before the stream completed", "type": "upstream_error", "code": null}}The stream then closes without a terminating data: [DONE], and if you requested usage via stream_options, the final usage chunk never arrives. Treat mid-stream errors as retryable: keep or discard the partial output as your application requires, then reissue the request with your normal backoff. The full event format is covered in Streaming.