Skip to content
INFRO

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

429 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

StatusTypeWhen it occursRetry
400invalid_request_errorMalformed JSON, unknown parameter, invalid message structure, or an oversized imageNo
401authentication_errorMissing, malformed, or revoked API keyNo
402insufficient_creditsAccount balance exhausted, or the key hit its spend limitNo
403permission_deniedThe key lacks access, or a provider's content moderation blocked the requestNo
404model_not_foundThe model ID does not exist in the catalogNo
408request_timeoutNo provider produced a response in timeYes
429rate_limit_exceededPer-key request rate exceeded; the response includes a Retry-After headerYes
502upstream_errorEvery provider for the requested model returned an errorYes
503no_available_providerNo provider is currently able to serve the requested modelYes

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-After header over your computed delay — it says exactly when capacity frees up. The X-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

Python
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:

Mid-stream error event
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.