AIREITER

Structured Output в OpenRouter: почему схема JSON может игнорироваться

Последнее обновление: 2026-08-23 01:29:55

Одна и та же JSON Schema через одну модель OpenRouter может вернуть аккуратный типизированный ответ, а через другую — JSON с чужими ключами, пустую строку или ошибку 400. Тело запроса при этом не меняется. Пользователь Reddit u/MicBeckie проверял модели Qwen через Structured Output OpenRouter и получил результат: «9 раз из 10 я стабильно получал ошибки». Модели OpenAI в той же конфигурации, напротив, схему соблюдали.

Это не тот баг, который можно просто завести в трекере. Поддержка структурированных ответов в OpenRouter определяется для каждого endpoint отдельно, а само слово «поддержка» охватывает три уровня: от нативного строгого контроля схемы до провайдеров, для которых ваша схема — лишь рекомендация. Ниже разберём маршрутизацию этой функции, шесть реальных сценариев поломки и меры, после которых схемные ответы можно безопаснее выпускать в прод. Механика контроля описана в официальной документации Structured Outputs; примеры сбоев взяты из обсуждений разработчиков, ссылки на которые приведены по ходу текста.

Страница документации OpenRouter по структурированным ответам

Что в 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 минуты от каждого и ничего не получил.

#Форма сбояЧто вы увидитеТипичная причина
1Endpoint не поддерживаетсяОшибка: structured outputs not supportedМаршрутизация отправила запрос провайдеру без этой возможности
2Некорректная схемаОшибка API на запросеСхема нарушает правила endpoint
3Схема проигнорированаВалидный JSON с неправильными ключамиКонтроль на уровне подсказки
4400 из-за tool_choiceinvalid_request_errorSDK эмулирует схему через принудительный вызов инструмента
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-ограждения и подобные случаи. Важнее всего помнить о двух ограничениях:

  1. Стриминг не поддерживается. В документации область действия плагина ограничена нестриминговыми запросами.
  2. Нарушения схемы не исправляются. Healing делает JSON парсируемым, но не приведёт к схеме ответ, который её проигнорировал. Сценарий сбоя №3 из таблицы останется без изменений.

Как выбирать модели, которые действительно соблюдают схемы

Списки моделей быстро устаревают, а критерии отбора — нет. Три фильтра отсекают большинство описанных выше проблем:

  1. Нативный строгий контроль. Лучше выбирать модели, чьи провайдеры контролируют схему при декодировании, а не переводят её в другой формат или используют как подсказку. В таблице Providers на странице модели видно, какие endpoint заявляют structured_outputs; качество контроля определяется уровнем провайдера.
  2. Один проверяемый провайдер. Сверьте атрибуцию провайдера с заведомо рабочим endpoint на нескольких запросах. Если роутер распределяет запросы между провайдерами с разными уровнями контроля, частота сбоев превращается в лотерею маршрутизации. Закрепите провайдера или выберите модель с единственным провайдером.
  3. Собственный 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, проведите такой тест:

  1. Возьмите одну репрезентативную схему средней сложности, с additionalProperties: false и описаниями всех свойств.
  2. Отправьте 10 одинаковых запросов с strict: true и require_parameters: true, оставив fallback включёнными. Этот прогон намеренно проверяет поведение fallback, поэтому их нужно оставить активными.
  3. Оцените каждый ответ по трём критериям: JSON парсится? Соответствует схеме? Семантически корректен?
  4. Зафиксируйте провайдера, обработавшего каждый ответ, через метаданные generation. Результат 10/10, полученный от четырёх разных провайдеров, — это лотерея маршрутизации, а не гарантия.
  5. Примите решение: выпускать как есть; закрепить через provider.order endpoint, который прошёл тест, и повторить 10 закреплённых запросов; либо сменить модель и добавить на клиенте слой валидации и повторных попыток.

Порог прохождения определяете вы, но при результате ниже 9/10 на фиксированной схеме повторные попытки и код валидации перестают быть опцией. Они становятся частью продукта.

Читайте также: как автороутер OpenRouter выбирает провайдеров, как снизить расходы с помощью кеширования промптов OpenRouter и как исправить ошибки лимита запросов OpenRouter 429.