Errors & retries

Every endpoint except chat answers with an envelope carrying error.kind; chat speaks OpenAI-shaped errors. Below is what each kind means and what to do about it.

The envelope

Success and failure share one shape: success, code, data, error. An error always carries kind and message, and sometimes retry_after_sec — a recommended pause in seconds.

{
  "success": false,
  "code": 429,
  "data": null,
  "error": {
    "kind": "rate_limited",
    "message": "rate limit exceeded (rpm)",
    "retry_after_sec": 12
  }
}
{
  "error": {
    "message": "no healthy text provider",
    "type": "invalid_request_error"
  }
}

Error kinds

kindHTTPWhat happenedRetry?
invalid_request400The body failed validation: no prompt or messages, malformed aspect_ratio, unknown provider.No — fix the request
unauthorized401The key is missing, revoked or invalid.No — check the key
forbidden403Access refused, e.g. an expired signature on a file URL.No
not_found404The object does not exist — a foreign or purged job, for instance.No
rate_limited429You hit the plan rpm or concurrency ceiling.Yes — after a pause
quota_exceeded429The daily or lifetime quota of the plan is exhausted.No — until reset or upgrade
no_healthy_provider503No eligible provider is currently available.Yes — with backoff
provider_error502Every provider the gateway tried returned an error.Yes — with backoff
timeout504The upstream did not answer in time.Yes — with backoff
internal_error500An internal gateway failure.Yes — with backoff

What the gateway already does

One /v1 call is one attempt on your side, but not necessarily one attempt inside. The gateway walks its provider pool, honouring health and circuit-breaker state; a finished job reports how many it needed in attempts. Failed internal attempts cost you nothing.

What to do on your side

  • Honour retry_after_sec when it is present — it is the gateway’s own calculation, not a guess.
  • For a 429 caused by rpm, use exponential backoff with jitter; for quota_exceeded, retrying is pointless.
  • Never retry 400, 401, 403 or 404 — the same body yields the same answer.
  • For image generation, retry the job poll rather than the POST, or you create a duplicate job and pay the quota twice.

Handling it in code

import time, httpx

def call_with_retry(request, attempts=4):
    delay = 1.0
    for attempt in range(attempts):
        response = request()
        if response.status_code < 400:
            return response

        body = response.json()
        kind = (body.get("error") or {}).get("kind")

        # Client mistakes and auth failures never get better on retry.
        if response.status_code in (400, 401, 403, 404):
            response.raise_for_status()

        # The gateway tells you how long to wait when it knows.
        wait = (body.get("error") or {}).get("retry_after_sec") or delay
        if kind == "quota_exceeded":
            raise RuntimeError("quota exhausted — upgrade the plan or wait for the reset")

        time.sleep(wait)
        delay *= 2

    response.raise_for_status()