Ошибки

.md

Фактические формы ошибок API СОЮЗ, HTTP-коды, ошибки джоб, исключения фреймворка и файлового хоста, правила безопасного ретрая.

Формат

Ошибки, которые обрабатывает наше приложение, приходят в одной из двух JSON-форм — обе содержат error.code, и завязываться надо на него, а не на форму тела. Но не любой ответ с ошибкой — это наш JSON: см. «Ответы, которые не в нашем формате» ниже. Пиши клиент так, чтобы он переживал тело, которое не парсится как JSON.

1. Короткий конверт — для ошибок, которые видны сразу на границе: разбор тела, авторизация, лимиты, файловые эндпоинты.

{
  "error": {
    "code": "invalid_request",
    "message": "`model` and `prompt` are required",
    "type": "invalid_request"
  }
}

2. Сериализованная джоба — для ряда отказов асинхронной генерации после разбора запроса (несовместимый параметр, отключённая модель, нехватка кредитов), а также для идемпотентного повтора уже упавшей джобы. API возвращает сам объект failed-джобы с HTTP 400. Ниже — пример capability-отказа: у него цена нулевая, резерв не создаётся, но джоба появляется в истории. У отказа из-за нехватки кредитов price содержит рассчитанную ненулевую цену; полная матрица различий приведена в следующем разделе.

{
  "id": "job_...",
  "object": "image",
  "model": "souz/nano-banana-pro",
  "status": "failed",
  "price": 0,
  "created_at": 1715000000,
  "parameters": {
    "schema": "souz.parameters/v1",
    "requested": { "resolution": "4K" },
    "effective": null,
    "degradations": []
  },
  "error": {
    "code": "capability_mismatch",
    "message": "The request cannot be executed with the available capabilities",
    "type": "capability_mismatch",
    "details": {
      "schema": "souz.capability-mismatch/v1",
      "blocking": [{
        "path": "resolution",
        "reason": "unsupported_value",
        "requested": "4K",
        "allowed": ["1K"]
      }],
      "available_degradations": []
    }
  }
}

3. Ответы, которые не в нашем формате. Их нужно учитывать при интеграции:

  • Неизвестный маршрут или необработанное исключение — теперь тоже наш JSON-конверт: 404 not_found и 500 internal_error соответственно. Текст фиксированный и о внутреннем устройстве ничего не сообщает.
  • Прямая раздача файлов с хранилища/CDN — тело и заголовки формируем не мы: ответ может быть произвольным (HTML, XML, пусто). См. раздел про файлы в Джобах.

Создание асинхронной джобы: что приходит сразу

POST /v1/images|videos/generations отвечает сразу одним из этих способов. Деньги двигаются только там, где это указано:

Случай HTTP Форма ответа Деньги
Джоба принята 202 Объект джобы, status: "queued" Резерв создан
Повтор с тем же Idempotency-Key, существующая джоба не failed 200 Тот же объект джобы (в любом статусе: queued, in_progress, completed) Ничего не двигается
Повтор с тем же Idempotency-Key, существующая джоба failed 400 Тот же объект джобы Ничего не двигается
Тот же ключ, но другой нормализованный запрос 409 idempotency_conflict Ничего не двигается
Тот же ключ и запрос, но первый admission ещё не bound 409 + Retry-After idempotency_in_progress Ничего не двигается; повторить позже
Модель не существует 400 Короткий конверт invalid_request Джоба не создаётся
Модель отключена (или отключилась между запросом и постановкой) 400 Джоба failed, error.code: model_unavailable Резерв не создаётся
Нормализатор отклонил неизвестное поле, type/enum/range/condition, alias-конфликт, >14 референсов или вложенный params 400 Короткий invalid_request Новый claim и джоба не создаются, резерв отсутствует; exact bound v1/legacy cutover-row только replay'ится read-only
Локальный capability-check доказал, что canonical field/value не поддержан ни одним доступным вариантом 400 Джоба failed, price: 0, error.code: capability_mismatch, versioned error.details Резерв не создаётся
Имя ещё не канонизированного advanced-поля вне allow-list 400 Джоба failed, price: 0, error.code: invalid_input Резерв не создаётся
Не хватает кредитов 400 Джоба failed, error.code: insufficient_credits; price содержит рассчитанную maximum charge Резерв не создаётся

Обрати внимание: для асинхронной генерации нехватка кредитов — это HTTP 400 с объектом джобы, а не 402. 402 insufficient_credits бывает только у синхронного чата.

Текущий опубликованный v0-срез типизируется единым нормализатором до side effects. Доказанная несовместимость canonical field/value с доступными вариантами возвращается структурированным capability_mismatch; per-variant resolver отсекает несовместимые маршруты. Opt-in fallback исполняет только опубликованные choices; сейчас это Nano Banana Pro references 9–14 → первые 8.

capability_mismatch использует provider-opaque details.schema = "souz.capability-mismatch/v1": blocking[] содержит отсортированные canonical path, reason, requested и allowed, а available_degradations[] — только заранее опубликованные opt-in варианты. Имена провайдера/его модели, себестоимость, попытки, секреты и source locator наружу не попадают. Тот же details возвращается polling для terminal failure после исчерпания совместимого rich-route; envelope в create/polling/webhook одинаков и содержит code, message, type: "capability_mismatch", details; transient/ambiguous сбой этим кодом не маскируется.

Всё, что происходит после успешной постановки (провайдер не смог, дедлайн, модерация), приходит не HTTP-ответом, а через опрос GET /v1/jobs/{id} или вебхук.

Коды HTTP-ответов

HTTP code Когда Что делать
400 invalid_request Границевые ошибки: тело не JSON, нет обязательных полей, неизвестное/нетипизированное поле, alias-конфликт, вложенный params, плохой callback_url, непринятый референс Исправить запрос. Не ретраить как есть
400 invalid_input / content_blocked (короткий конверт) Синхронный чат: апстрим терминально отклонил вход или контент. message — наш текст по коду Исправить/переформулировать запрос; логику строить только по code, не по message
400 capability_mismatch (внутри объекта джобы) Создание async-job: локальный check доказал, что канонически допустимая capability недоступна всем lossless-вариантам. Тело — сериализованная failed-джоба с versioned details, деньги не двигались Исправить поля из details.blocking; /v1/models до сертификации выключен
401 invalid_api_key Нет заголовка / неверный или отключённый ключ Проверить ключ
402 insufficient_credits Только синхронный чат: не хватает кредитов. У асинхронной генерации это 400 + джоба failed Пополнить баланс на dash.souz.ai
403 forbidden Подпись ссылки на файл невалидна или истекла. Такой же 403 может прийти и от файлового хоста при прямой раздаче — это могут быть права доступа, конфигурация или временный сбой, а не удаление файла Взять свежую ссылку через GET /v1/jobs/{id}; если не помогло — повторить с нарастающей паузой; при устойчивом повторении — написать в поддержку. Перегенерировать «на всякий случай» не нужно — см. Джобы
404 not_found Для джобы: не найдена / чужая. Для локального файла текущий handler ошибочно сводит к 404 и отсутствие, и любую ошибку чтения (права/I/O/конфигурация) Для джобы проверить ID. Для файла: свежая ссылка → backoff → поддержка; до RSA-92 этот 404 не доказывает удаления
404 (без нашего тела) Непрозрачный ответ файлового хоста при прямой раздаче. Может означать и отсутствие объекта, и проблему конфигурации/прав/временный сбой — мы этот ответ не формируем Свежая ссылка → повтор с паузой → поддержка. Сам по себе такой 404 не доказывает, что файл удалён
404 model_not_found Чат: неизвестная модель Список — GET /v1/models, каталог
408 invalid_request Image/video: JSON-тело не удалось дочитать за 30 секунд Повторить с быстрым соединением; большой референс передать по HTTPS URL
410 file_expired Наш структурированный ответ: мы отдаём файл через свой прокси и объекта в хранилище нет Ответ подтверждает отсутствие: если результат ещё нужен — генерировать заново
413 invalid_request Image/video: фактическое JSON-тело больше 25 MiB; streaming limit действует и для chunked/ложного Content-Length. Чат пока проверяет только объявленный Content-Length (RSA-27) Для image/video сжать data URL или передать картинку по HTTPS URL; для чата не считать header-only check полноценной защитой
429 rate_limited Превышен лимит запросов Подождать Retry-After секунд, повторить
500 internal_error Неожиданная ошибка на нашей стороне; в chat это может быть сбой фиксации уже выполненного ответа Следовать правилам ниже: keyed async create можно безопасно повторить с тем же ключом, chat автоматически не повторять
502 model_unavailable Только синхронный чат: провайдеры доказанно исчерпаны либо исход уже отправленной попытки не удалось подтвердить; публичный code одинаков Не повторять автоматически: проверить историю списаний и решить вручную либо через собственную дедупликацию
502 storage_unavailable Хранилище файлов временно недоступно Повторить через минуту — это не удаление
503 internal_error Временная перегрузка bounded-очереди чтения image/video body; есть Retry-After: 1 Повторить keyed-запрос с тем же Idempotency-Key после указанной паузы

⚠️ В тексте message у 410 сейчас есть упоминание срока «~24 часа». Это устаревшая строка в коде, оставшаяся от прежней формулировки: она не часть контракта, не подтверждает никакой срок хранения и не должна использоваться как доказательство. Ориентируйся на error.code; строку уберут вместе с наведением порядка в выдаче файлов.

Ошибки джоб (асинхронные)

Асинхронные сбои после постановки задачи не приходят отдельным HTTP-ответом — их видно при опросе GET /v1/jobs/{id} (или в вебхуке job.failed). Джоба завершается status: "failed" с кодом в error.code; для таких наблюдаемых сбоев резерв возвращается перед тем, как джоба сохраняется в статусе failed (границы этой гарантии — в Кредитах):

error.code Значение
invalid_input Провайдер отклонил вход (промпт/референс/параметры) — исправь запрос
capability_mismatch Доказанная provider-opaque несовместимость canonical field/value: исправь details.blocking; после rich-route exhaustion резерв уже возвращён атомарно с terminal job
content_blocked Контент заблокирован политиками модели — переформулируй промпт
model_unavailable Водопад провайдеров доказанно исчерпан. Неоднозначная async-попытка остаётся публично in_progress, пока не появится доказательство исхода

Правила ретраев

  • Создание async image/video: 429 повторяй после Retry-After; 500, сетевой таймаут или обрыв можно повторять с нарастающей паузой только с тем же Idempotency-Key и тем же запросом. Сервер вернёт исходную job либо временный idempotency_in_progress, а не создаст вторую. После получения job.id опрашивай эту job, не отправляй новое создание.
  • Синхронный chat: после 500, 502, сетевого таймаута или обрыва не делай слепой автоматический retry. Вызов мог дойти до апстрима и стать платным, а Idempotency-Key на chat не действует; повтор создаст отдельную генерацию. Сначала проверь историю списаний, затем решай вручную или через собственную дедупликацию. 429 можно повторить после Retry-After.
  • Безопасные read/status/storage операции: временные 500, storage_unavailable и сетевые ошибки можно повторять с backoff — они не создают новую генерацию.
  • Не ретраить без изменений: 400, 401, 402, not_found при поиске джобы, model_not_found, content_blocked, invalid_input. Для файла только структурированный 410 file_expired сейчас однозначно терминален. И локальный структурированный 404, и непрозрачные 403/404 файлового хоста требуют: свежая ссылка → backoff → поддержка; их классификацию исправляет RSA-92.