Ошибки
Фактические формы ошибок 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.