Ошибки и деградация
Три формы тела ошибки, полный каталог кодов /v1/process, extraction_status и warnings[] — как API сообщает о частичном или ненадежном результате.
API различает два принципиально разных сигнала: «запрос не выполнен»
(HTTP-ошибка, тела результата нет) и «запрос выполнен, но результату стоит
не полностью доверять» (200 OK, но extraction_status и warnings[]
говорят, что часть данных — не более чем лучшая попытка). Второе — не баг,
а осознанная деградация: сервис отдает то, что смог, вместо молчаливого
падения.
Три формы тела ошибки
Это первое, обо что спотыкается парсер ошибок на клиенте: форм три, и они отличаются по уровню, на котором ошибка поймана.
1. Ошибки формы запроса (валидация /v1/process) — HTTP 422, поле
detail — объект:
{
"detail": {
"code": "PROMPT_REQUIRES_SCHEMA",
"message": "prompt без output_schema не поддерживается…",
"details": {}
}
}2. Ошибки обработки (модель, парсинг, NDA-граница, размер файла) — привычный конверт:
{
"success": false,
"error": {
"code": "FILE_TOO_LARGE",
"message": "File size 62914560 exceeds maximum 52428800 bytes",
"details": { "size": 62914560, "max_size": 52428800 }
}
}3. Ошибка ключа — HTTP 401, detail — обычная строка:
{ "detail": "Invalid API key." }Ключ проверяется раньше эндпоинта, поэтому формат отличается от обеих предыдущих форм. Подробнее — в разделе «Аутентификация».
Практическое правило разбора: сначала detail?.code, затем error?.code,
затем detail как строка.
Каталог кодов
Форма запроса — 422, тело {"detail": {code, …}}
| Код | Когда |
|---|---|
SOURCE_REQUIRED | Ни file, ни text не переданы |
SOURCE_CONFLICT | Переданы и file, и text — нужен ровно один источник |
EMPTY_TASK | Запрос ничего не просит: нет непустой output_schema, split, match или include |
UNKNOWN_FIELD | Поле вне контракта (опечатка вроде promt); details.unknown — список, details.allowed — принимаемые поля |
UNSUPPORTED_FILE_TYPE | Расширение вне списка форматов; details.supported — актуальный список |
PROMPT_REQUIRES_SCHEMA | prompt без непустой output_schema |
PROMPT_TOO_LONG | prompt длиннее ~10 000 знаков |
INCLUDE_UNKNOWN | В include секция вне markdown/fragments/tables/fields |
SPLIT_REQUIRES_PDF | split указан не для PDF-файла |
COMBINATION_NOT_SUPPORTED | Сочетание параметров пока не поддерживается (сейчас: split + match) |
MATCH_REGISTRY_REQUIRED | match без файла registry — в том числе любой чисто-JSON запрос |
MATCH_REGISTRY_INVALID | Файл реестра не читается: битый XLSX, CSV не в UTF-8 |
MATCH_REQUIRES_SCHEMA | match без непустой output_schema — сверять нечего |
MATCH_KEY_COLUMN_NOT_FOUND | key_column не найдена в самом реестре |
MATCH_FIELD_NOT_FOUND | Колонку не сопоставить с полем result; details.similar — подсказка, а не выбор за вас |
MATCH_FIELD_AMBIGUOUS | Несколько полей result подходят — укажите values_from |
MATCH_NO_DATA | result пуст — сравнивать с реестром нечего |
LANGUAGE_UNKNOWN | language вне ru/en/zh — допустимые значения в details.allowed |
TRANSLATE_REQUIRES_SCHEMA | translate без непустой output_schema — переводить нечего |
FORMAT_UNKNOWN | format вне json/xlsx |
XML_MALFORMED | Файл не является корректным XML (в том числе отклоненные XXE-конструкции) |
XML_TOO_LARGE | XML больше 10 МБ |
XML_TOO_COMPLEX | В XML больше 200 000 элементов |
FIELD_NOT_JSON | Form-поле (output_schema/include/split/match) — невалидный JSON |
FIELD_INVALID | Поле валидно синтаксически, но не той формы (объект вместо массива и т.п.) |
BODY_NOT_JSON / BODY_NOT_OBJECT | Тело JSON-запроса не парсится или не является объектом |
Обработка — тело {"success": false, "error": {…}}
| HTTP | Код | Когда |
|---|---|---|
| 413 | FILE_TOO_LARGE | file больше 50 МБ или registry больше 20 МБ (details.field укажет, что именно) |
| 422 | ANONYMIZATION_INCOMPLETE | PII-граница fail_closed: скраб не дал гарантии — вызов к внешней модели не выполнен. В details.residual только категории и счетчики, никогда сами значения |
| 502 | LLM_RESULT_NOT_OBJECT | Модель вернула текст вместо объекта по вашей схеме — испорченный ответ апстрима, не ошибка запроса |
| 503 | PARSING_ERROR | Сбой разбора документа (Docling/OCR), детали — в details |
| 503 | EXTRACTION_ERROR | Сбой на этапе извлечения |
| 503 | PROVIDER_UNAVAILABLE | Провайдер модели недоступен после исчерпания fallback-цепочки |
Предохранители тарифа
Срабатывают до тяжелой обработки — за отклоненный запрос страницы не списываются. Механика и пороги — в «Аутентификации».
| HTTP | Код | Когда |
|---|---|---|
| 429 | RATE_LIMITED | Превышена скорость плана (страниц в минуту, скользящее окно 60 с). Заголовок Retry-After — реальные секунды до следующей допустимой отправки, интервал не надо угадывать |
| 413 | PAYLOAD_TOO_LARGE | Один документ длиннее потолка страниц плана; details.max_pages_per_document. Оценка делается до парсинга |
| 402 | QUOTA_EXCEEDED | Месячная квота страниц исчерпана; details.pages_used_this_month, details.pages_limit. Сбрасывается в начале календарного месяца |
| 422 | KEY_LIMIT_REACHED | У аккаунта уже пять активных ключей — отзовите ненужный в кабинете |
| 403 | FREE_PLAN_NO_CHAT | /v1/chat/completions недоступен на плане free (форма ошибки — detail.code, не error.code) |
Остальное
| HTTP | Код | Когда |
|---|---|---|
| 401 | (строковый detail) | Ключ отсутствует или неверен |
| 401 | CABINET_HEADER_REQUIRED | Доступ по сессии кабинета (кука session, без X-API-Key) без заголовка X-Requested-With: docai-cabinet — вторая линия защиты после SameSite=Lax |
| 404 | TASK_NOT_FOUND | GET /v1/process/{task_id}: задача неизвестна, истекла (результат живет 1 час) или принадлежит другому ключу |
| 503 | QUEUE_FULL | Очередь async-обработки заполнена; тело — форма {"detail": {code…}}, в ответе есть заголовок Retry-After — повторите через указанное число секунд |
| — | PROCESSING_ERROR | Только в async: непредвиденный сбой фоновой обработки. В HTTP-ответе не встречается — приходит в теле поллинга как {"status": "error", "error": {…}} |
| — | INTERRUPTED_BY_RESTART | Только в async: перезапуск сервиса застал задачу посреди обработки. Приходит в теле поллинга как {"status": "error", …} — отправьте документ повторно |
Коды стабильны — это часть контракта; список аддитивный: новые коды могут появляться, существующие не переименовываются.
extraction_status
Приходит в metadata при 200 OK и отвечает на вопрос «что вообще
получилось», независимо от HTTP-кода:
| Значение | Значит |
|---|---|
full | Данные получены — моделью или детерминированно |
partial | Часть секций или полей пуста (характерно для split) |
fallback | Данных нет: модель их не вернула либо документ не удалось разобрать и модель не вызывалась вовсе |
parse_only | Модель не вызывалась, потому что не просили: нет output_schema |
Это 200 OK в каждом случае. Проверяйте extraction_status так же
внимательно, как HTTP-код: 200 означает «запрос обработан», а не «данные
надежны».
Уверенности по полям в ответе нет
Ни общего confidence, ни уверенности по каждому полю в конверте нет:
калибровка в единый эндпоинт не перенесена, metadata.input_quality_score
всегда null. Сигналы качества здесь — extraction_status и
warnings[]. Единственный confidence в ответе — в documents[], и он
про другое: насколько сплиттер уверен в границе секции.
warnings[] — структурные предупреждения
Каждый warning — {code, field, severity, message}. field — путь до поля
(supplier.inn, items[0].amount, documents[1]) или $document для
документа целиком. severity: info (к сведению) → low →
medium (стоит перепроверить) → high (скорее всего неверно).
{
"warnings": [
{
"code": "invalid_inn_length",
"field": "supplier.inn",
"severity": "high",
"message": "ИНН «77» имеет некорректную длину…"
}
]
}Источников два, и они дополняют друг друга.
1. Проверки извлеченных данных
Работают всегда, независимо от вида задачи. Валидатор не только
предупреждает, но и обнуляет заведомо неверное значение — например
party2, дословно повторяющую party1.
| Код | Severity | Когда |
|---|---|---|
invalid_inn_length | high | ИНН не 10 и не 12 цифр |
invalid_kpp_length | medium | КПП не 9 цифр |
suspicious_amount | high | Сумма больше 100 млн ₽ без суммы прописью рядом |
ocr_noise | medium | В тексте символы замены (�) — OCR не справился с частью листа |
duplicate_party | high | ИНН обеих сторон договора совпали — вторая сторона обнулена |
missing_contractor_inn | medium/low | У стороны нет валидного ИНН |
ambiguous_document_number | high | В акте номер документа совпал с номером договора |
totals_mismatch | medium | Итог не сходится с суммой строк (с учетом НДС) |
weight_invariant_violation | high | Нетто больше брутто — физически невозможно |
sender_equals_carrier | high | В CMR отправитель и перевозчик — одно лицо |
date_out_of_range | medium | Дата вне [1990, 2100] — OCR мог спутать «24» с «1924» |
2. Предупреждения самого эндпоинта
| Код | Severity | Когда |
|---|---|---|
parse_empty | high | Разбор файла (или секции при split) дал пустой текст — модель не вызывается: выдумывать по пустому документу нечего. Со схемой это extraction_status: "fallback" и result: null |
parse_thin | info | Текст есть, но его подозрительно мало для такого числа листов — типичная картина нераспознанного скана. Обработка продолжается |
llm_no_result | high | Модель вернула пустой результат (отказала вся fallback-цепочка) |
section_unavailable | info | Секция fields/tables недоступна для этого входа (не-PDF) |
tables_unavailable | info | Разбор таблиц упал — tables: null это сбой, а не «таблиц нет» |
mode_ignored | info | mode ≠ balanced: параметр принят, но на обработку не влияет |
match_values_incomplete | info | В поле сверки есть пропуски — coverage посчитан по остальным значениям |
split | info | Предупреждение сплиттера, проброшено как есть |
split_truncated | info | Секций больше, чем split.max_documents — хвост отброшен |
split_result_truncated | high | Несогласованный результат сплиттера — часть секций отброшена |
translation_failed | medium | translate задан, но провайдер перевода недоступен — result отдан без перевода (translated: null) |
translation_partial | low | Часть строк не переведена — оставлены как в оригинале, остальное translated заполнено |
format_ignored_for_async | info | format=xlsx вместе с async: true — файл забирается отдельно через GET /v1/process/{task_id}?format=xlsx |
xlsx_export_failed | medium | Сборка Excel упала — результат отдан в JSON вместо файла (уже оплаченный результат не теряется) |
Проверки «пусто/слишком мало текста» применяются только к файловому входу:
короткий text вы прислали сами и осознанно.
Практика: как обрабатывать деградацию на своей стороне
- Проверяйте HTTP-статус и
extraction_status/warnings[].200 OKозначает «запрос обработан», а не «данным можно верить». - На
high-warning по конкретному полю не показывайте значение пользователю как окончательное без пометки. Наfallback— рассмотрите повторный вызов с более узкой схемой или ручную проверку. parse_empty— сигнал о документе, а не о запросе: повтор с тем же файлом даст тот же результат. Нужен другой файл или лучшее качество скана.- На
422 ANONYMIZATION_INCOMPLETEповтор скорее всего повторит ошибку — это осознанный отказ границы, а не временный сбой; нужен другой документ или self-hosted-контур (см. цены → «Бизнес»). - На
502/503(LLM_RESULT_NOT_OBJECT,PARSING_ERROR,EXTRACTION_ERROR,PROVIDER_UNAVAILABLE) — это временная деградация после исчерпания fallback-цепочки, повторите запрос с задержкой. - На
503 QUEUE_FULLповторите через число секунд из заголовкаRetry-After— это штатный ответ заполненной очереди, а не сбой.INTERRUPTED_BY_RESTARTв теле поллинга — сигнал отправить документ заново: страницы за прерванную обработку не списываются повторно.