Ошибки и повторы

Все эндпоинты, кроме чата, отвечают конвертом с полем error.kind. Чат говорит на языке ошибок OpenAI. Ниже — что означает каждый kind и что с ним делать.

Конверт

Успех и ошибка приходят в одинаковой оболочке: success, code, data, error. У ошибки всегда есть kind и message, а иногда — retry_after_sec с рекомендованной паузой в секундах.

{
  "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"
  }
}

Виды ошибок

kindHTTPЧто произошлоПовторять?
invalid_request400Тело запроса не прошло валидацию: нет prompt или messages, кривой aspect_ratio, неизвестный provider.Нет — исправь запрос
unauthorized401Ключ отсутствует, отозван или неверен.Нет — проверь ключ
forbidden403Доступ к ресурсу запрещён, например протухшая подпись у ссылки на файл.Нет
not_found404Объект не найден — например, чужой или удалённый job.Нет
rate_limited429Упёрся в rpm или в конкурентность тарифа.Да — после паузы
quota_exceeded429Исчерпана суточная или пожизненная квота тарифа.Нет — до сброса квоты или смены тарифа
no_healthy_provider503Ни один подходящий провайдер сейчас не доступен.Да — с бэкоффом
provider_error502Все перебранные провайдеры вернули ошибку.Да — с бэкоффом
timeout504Апстрим не ответил в отведённое время.Да — с бэкоффом
internal_error500Внутренняя ошибка шлюза.Да — с бэкоффом

Что уже делает шлюз

Один вызов /v1 — это одна попытка с твоей стороны, но не обязательно одна попытка внутри. Шлюз сам перебирает провайдеров по пулу с учётом здоровья и брейкера, и в результате job поле attempts показывает, сколько их понадобилось. За неуспешные внутренние попытки ты не платишь.

Что стоит делать у себя

  • Уважай retry_after_sec, когда он пришёл: это не оценка, а расчёт шлюза.
  • На 429 из-за rpm — экспоненциальный бэкофф с джиттером; на quota_exceeded повтор бесполезен.
  • Не ретрай 400, 401, 403 и 404: повтор с тем же телом даст тот же ответ.
  • Для генерации изображений повторяй опрос job, а не сам POST — иначе создашь дубль задачи и потратишь квоту дважды.

Пример обработки

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()