Одна и та же JSON Schema через одну модель OpenRouter может вернуть аккуратный типизированный ответ, а через другую — JSON с чужими ключами, пустую строку или ошибку 400. Тело запроса при этом не меняется. Пользователь Reddit u/MicBeckie проверял модели Qwen через Structured Output OpenRouter и получил результат: «9 раз из 10 я стабильно получал ошибки». Модели OpenAI в той же конфигурации, напротив, схему соблюдали.
Это не тот баг, который можно просто завести в трекере. Поддержка структурированных ответов в OpenRouter определяется для каждого endpoint отдельно, а само слово «поддержка» охватывает три уровня: от нативного строгого контроля схемы до провайдеров, для которых ваша схема — лишь рекомендация. Ниже разберём маршрутизацию этой функции, шесть реальных сценариев поломки и меры, после которых схемные ответы можно безопаснее выпускать в прод. Механика контроля описана в официальной документации Structured Outputs; примеры сбоев взяты из обсуждений разработчиков, ссылки на которые приведены по ходу текста.
Что в OpenRouter считается поддержкой Structured Output
OpenRouter принимает параметр response_format с type: "json_schema", именем схемы name, флагом strict и самой JSON Schema. Минимальный запрос выглядит так:
{
"model": "openai/gpt-4o",
"messages": [{ "role": "user", "content": "Extract the shipping info" }],
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "shipping_info",
"strict": true,
"schema": {
"type": "object",
"properties": {
"tracking_number": { "type": "string", "description": "Carrier tracking ID" },
"carrier": { "type": "string" },
"eta_days": { "type": "number", "description": "Days until delivery" }
},
"required": ["tracking_number", "carrier", "eta_days"],
"additionalProperties": false
}
}
}
}
В официальной документации есть два нюанса, от которых зависит, заработает ли это вообще:
- Поддержка привязана к endpoint, а не к модели. Одна модель может обслуживаться пятью провайдерами, но Structured Outputs будут работать только у двух. В разделе Providers на странице модели для каждого провайдера указан параметр
structured_outputs; документация отдельно предупреждает: «поддержка endpoint также может меняться со временем». - Функция начиналась с ограниченного набора моделей. OpenRouter анонсировал Structured Outputs 12 декабря 2024 года, и тогда поддерживались только OpenAI 4o и модели Fireworks. Остальные подключались позднее, провайдер за провайдером, поэтому любой зафиксированный сегодня список моделей быстро устаревает.
Документация также советует добавить описание каждому свойству и указать additionalProperties: false. На нижних уровнях контроля схема одновременно служит инструкцией для модели.
Почему один strict-флаг даёт три разных уровня контроля
Значение strict: true зависит от того, куда именно попадёт запрос. В официальном гайде поведение провайдеров разделено на три уровня:
| Уровень | Как провайдер обрабатывает схему | Можно ли доверять ответу? |
|---|---|---|
| Нативный строгий режим | Точно контролирует схему на этапе декодирования | Да: ответ по построению соответствует схеме |
| Преобразование формата | Переводит вашу схему во внутренний формат structured output провайдера | В основном да, но лишь в рамках возможностей этого формата |
| Сильная подсказка | Передаёт схему модели как инструкцию | Нет: в удачный день будет JSON по схеме, в неудачный — выдуманные ключи |
OpenRouter не указывает в момент запроса, какой уровень используется конкретным endpoint: документация предлагает сверяться с материалами самого провайдера. У нативных строгих режимов есть и ограничения по поддерживаемым возможностям JSON Schema. Поэтому экзотические ключевые слова могут не пройти на наиболее строгом endpoint, но быть принятыми как подсказка в другом месте.
Для Claude на странице маршрутизации провайдеров описан отдельный случай. При response_format.type: "json_schema" OpenRouter автоматически добавляет beta-заголовок Anthropic structured-outputs-2025-11-13, который включает строгую валидацию аргументов инструментов по схеме. Но если определения инструментов с strict: true передаются через tools, этот beta-заголовок должен добавить сам вызывающий код. Иначе OpenRouter уберёт strict и отправит запрос без него. Сбой происходит молча: вызовы инструментов перестают валидироваться по схеме, но ошибки не будет.
Шесть типичных поломок одной и той же схемы
Два типа ошибок завершают запрос сразу и описаны в официальном гайде. Ещё четыре встречаются в обсуждениях сообщества — и именно на них обычно уходит полдня отладки.
Быстрый сбой №1: endpoint не поддерживает Structured Outputs. Запрос завершается ошибкой о неподдерживаемой возможности. Это неприятно, зато однозначно. Быстрый сбой №2: некорректная JSON Schema. API отклоняет запрос, потому что схема не парсится или нарушает правила конкретного endpoint.
Тихий сбой №1: схема игнорируется. В ответ приходит валидный JSON, но по совершенно другой структуре. В обсуждении несоблюдения схемы на r/LocalLLaMA пользователь u/DaniyarQQQ описал это так:
Он возвращает JSON, который совсем не похож на мою схему.
u/MicBeckie в той же ветке сформулировал проблему диагностики:
Я либо вижу успешный ответ, где JSON в точности соответствует требованиям, либо получаю ошибку без возможности посмотреть на JSON.
Сбой обёртки №2: ошибка 400 о tool_choice, которого вы не отправляли. В упомянутом случае с LangChainJS метод withStructuredOutput() реализовывал «структурированный вывод», принудительно задавая tool_choice для сгенерированной функции. Если модель заявляет поддержку tool calls, но не умеет принудительный выбор инструмента, запрос падает с invalid_request_error. В случае DeepSeek v4 ошибка прямо называла модель: deepseek-reasoner does not support this tool_choice. Именно с этим столкнулся u/shansoft через LangChainJS (обсуждение), а предположение u/eyueldk «раз поддерживаются tool calls, должен работать и structured output» не подтвердилось. Поддержка вызова инструментов и строгих схем — разные возможности.
Тихий сбой №3: нет ни ошибки, ни содержимого. В отчёте по gpt-oss-120b строгий запрос со схемой возвращал 400 при прямом обращении к провайдеру, но через OpenRouter отвечал кодом 200 и пустым message.content. В другой ветке r/openrouter «поддерживаемая» модель возвращала лишь [1] или [1.1]. Если SDK спокойно парсит пустую строку, сбой проявится на три уровня ниже по стеку.
Тихий сбой №4: endpoint зависает. u/Beneficial-Loss-1031 рассказал об endpoint, которые заявляли поддержку Structured Output для DeepSeek v4 (обсуждение):
deepinfra/fp4иakashml/fp8имеют опцию structured output, но я ждал ответ API по 3 минуты от каждого и ничего не получил.
| # | Форма сбоя | Что вы увидите | Типичная причина |
|---|---|---|---|
| 1 | Endpoint не поддерживается | Ошибка: structured outputs not supported | Маршрутизация отправила запрос провайдеру без этой возможности |
| 2 | Некорректная схема | Ошибка API на запросе | Схема нарушает правила endpoint |
| 3 | Схема проигнорирована | Валидный JSON с неправильными ключами | Контроль на уровне подсказки |
| 4 | 400 из-за tool_choice | invalid_request_error | SDK эмулирует схему через принудительный вызов инструмента |
| 5 | Пустой контент | 200 и пустой message.content | Провайдер некорректно обрабатывает строгий режим |
| 6 | Зависание | Нет ответа несколько минут | В отчёте причина не подтверждена — на endpoint fp4/fp8 ожидание составило 3 минуты |
Сначала укрепите запрос, потом вините модель
Самая полезная настройка — require_parameters: true в объекте provider. По умолчанию она равна false, поэтому неизвестные параметры передаются провайдерам, которые могут молча их проигнорировать. Даже при false параметр response_format и Structured Outputs действуют для endpoint как мягкое предпочтение: желательно, но не гарантированно. При true маршрутизация ограничивается endpoint, которые поддерживают все переданные параметры, согласно документации по маршрутизации провайдеров:
{
"model": "deepseek/deepseek-chat",
"messages": [{ "role": "user", "content": "Extract the shipping info" }],
"response_format": { "type": "json_schema", "json_schema": { "name": "shipping_info", "strict": true, "schema": { "...": "..." } } },
"provider": {
"require_parameters": true,
"order": ["fireworks"],
"allow_fallbacks": false
}
}
Каждое ограничение сокращает пул подходящих провайдеров, а allow_fallbacks: false меняет доступность на детерминированность. В той же документации стандартная стратегия описана как балансировка по uptime и обратному квадрату цены за предыдущие 30 секунд. Она оптимизирует выбор под дешёвые и доступные endpoint, а не под соблюдение схемы. Если закрепить одного провайдера через order и отключить fallback, маршрутизация станет воспроизводимой: во время сбоя запрос не переключится на другого провайдера. Но уровень контроля схемы у выбранного endpoint всё равно нужно проверить самостоятельно.
Есть ещё две полезные привычки, которые ловят проблемы, недоступные контролю маршрутизации:
- Проверяйте, какой провайдер обработал запрос. В метаданных generation OpenRouter показывает маршрут провайдера для каждого поколения вместе с моделью, задержкой и числом токенов. Если качество ответов меняется, эта информация поможет понять: изменилась модель или роутер переключил провайдера.
- Всегда валидируйте результат на своей стороне. Ни один из уровней не заменяет разбор через Pydantic или Zod в вашем коде. Главный вывод из обсуждений тестирования на r/LLMDevs: «валидный JSON», «JSON, соответствующий схеме» и «семантически корректный ответ» — это три разные планки. API хотя бы частично отвечает только за первые две.
Стриминг поддерживается, но разбирать JSON придётся вам
Structured Outputs можно использовать с stream: true. Документация описывает контракт так: модель передаёт валидный частичный JSON, а собранный ответ должен соответствовать схеме после завершения потока. Это соответствие наследует уровень контроля endpoint: endpoint с режимом подсказки вполне может собрать ответ, не соответствующий схеме. Поэтому финальный объект нужно валидировать самостоятельно. Инкрементальный парсер документация тоже не предоставляет, и для интерфейсов, чувствительных к задержке, именно здесь начинается настоящая инженерная задача. Из ветки о лучших практиках стриминга на r/LLMDevs:
В итоге я просто написал функцию, которая сама достраивает JSON. — u/am174744
«…это полноценный автомат состояний». — u/ImNotLegitLol, о том, почему модель «восстановить, затем распарсить» недостаточна
Практические варианты: разбирать частичный JSON парсером, устойчивым к незавершённым данным; показывать только полностью полученные поля; либо отказаться от пошаговой отрисовки и выводить индикатор загрузки до сборки финального объекта.
Что исправляет Response Healing, а что нет
Плагин Response Healing в OpenRouter предназначен для нестриминговых запросов json_schema и исправляет проблемы форматирования: обрезанный JSON, лишние markdown-ограждения и подобные случаи. Важнее всего помнить о двух ограничениях:
- Стриминг не поддерживается. В документации область действия плагина ограничена нестриминговыми запросами.
- Нарушения схемы не исправляются. Healing делает JSON парсируемым, но не приведёт к схеме ответ, который её проигнорировал. Сценарий сбоя №3 из таблицы останется без изменений.
Как выбирать модели, которые действительно соблюдают схемы
Списки моделей быстро устаревают, а критерии отбора — нет. Три фильтра отсекают большинство описанных выше проблем:
- Нативный строгий контроль. Лучше выбирать модели, чьи провайдеры контролируют схему при декодировании, а не переводят её в другой формат или используют как подсказку. В таблице Providers на странице модели видно, какие endpoint заявляют
structured_outputs; качество контроля определяется уровнем провайдера. - Один проверяемый провайдер. Сверьте атрибуцию провайдера с заведомо рабочим endpoint на нескольких запросах. Если роутер распределяет запросы между провайдерами с разными уровнями контроля, частота сбоев превращается в лотерею маршрутизации. Закрепите провайдера или выберите модель с единственным провайдером.
- Собственный smoke-тест, а не чужой отзыв. Сигналы сообщества быстро меняются в обе стороны: отчёты об ошибках Qwen выше и отсутствие поддержки у DeepSeek v4 могут исчезнуть после обновления endpoint провайдерами. Единственный значимый показатель надёжности — тот, который ваша собственная схема выдаёт в ваших тестах.
FAQ по Structured Outputs в OpenRouter
Чем json_object отличается от json_schema?
json_object просит только синтаксически валидный JSON, тогда как json_schema передаёт схему, которой должен соответствовать ответ. json_object гарантирует синтаксис JSON, но не соблюдение структуры с конкретными полями. Если последующий код зависит от именованных полей, валидируйте результат самостоятельно.
Какие модели OpenRouter поддерживают Structured Outputs?
Статичного списка, которому можно доверять, нет: поддержка определяется endpoint, меняется со временем и в декабре 2024 года начиналась только с OpenAI 4o и моделей Fireworks. Проверяйте флаг structured_outputs для каждого endpoint в разделе Providers на странице модели.
Почему модель игнорирует мою схему?
Чаще всего причин три: запрос ушёл на endpoint с уровнем подсказки или без поддержки функции — исправляется через require_parameters: true и закрепление провайдера; схема использует ключевые слова, которые строгий режим endpoint не принимает; либо SDK-обёртка эмулирует структурированный вывод через tool calling на модели без поддержки принудительного выбора инструмента.
Можно ли использовать Pydantic или LangChain со Structured Outputs в OpenRouter?
Да. Официальная документация описывает формат запроса как совместимый с API OpenRouter в стиле chat completions, поэтому схемы, сгенерированные Pydantic, и OpenAI SDK работают напрямую. Метод LangChain withStructuredOutput() тоже подходит, но убедитесь, что он отправляет response_format, а не эмулирует результат через tool_choice: именно это вызывало ошибки 400 у DeepSeek v4.
Работает ли Structured Output со стримингом?
Да. Поток передаёт валидный частичный JSON, но итоговое соответствие схеме зависит от уровня контроля конкретного endpoint — собранный объект нужно валидировать самостоятельно. Инкрементальный разбор фрагментов остаётся задачей вашего приложения, а Response Healing к потокам не применяется.
Валидирует ли OpenRouter ответы по моей схеме?
Не с гарантией для всех endpoint: контроль зависит от уровня провайдера, а Response Healing исправляет только некорректный JSON, но не нарушения схемы. Валидация на стороне клиента остаётся обязательной.
Smoke-тест из 10 запросов
Прежде чем ставить любую модель в прод за Structured Outputs, проведите такой тест:
- Возьмите одну репрезентативную схему средней сложности, с
additionalProperties: falseи описаниями всех свойств. - Отправьте 10 одинаковых запросов с
strict: trueиrequire_parameters: true, оставив fallback включёнными. Этот прогон намеренно проверяет поведение fallback, поэтому их нужно оставить активными. - Оцените каждый ответ по трём критериям: JSON парсится? Соответствует схеме? Семантически корректен?
- Зафиксируйте провайдера, обработавшего каждый ответ, через метаданные generation. Результат 10/10, полученный от четырёх разных провайдеров, — это лотерея маршрутизации, а не гарантия.
- Примите решение: выпускать как есть; закрепить через
provider.orderendpoint, который прошёл тест, и повторить 10 закреплённых запросов; либо сменить модель и добавить на клиенте слой валидации и повторных попыток.
Порог прохождения определяете вы, но при результате ниже 9/10 на фиксированной схеме повторные попытки и код валидации перестают быть опцией. Они становятся частью продукта.
Читайте также: как автороутер OpenRouter выбирает провайдеров, как снизить расходы с помощью кеширования промптов OpenRouter и как исправить ошибки лимита запросов OpenRouter 429.