Um mesmo schema JSON pode retornar dados limpos e tipados em um modelo do OpenRouter, mas vir com chaves diferentes, uma string vazia ou um erro 400 no seguinte — usando exatamente o mesmo corpo de requisição. O usuário u/MicBeckie, do Reddit, testou modelos Qwen com structured outputs via OpenRouter e resumiu o resultado: "9 times out of 10 I always got errors". No mesmo cenário, os modelos da OpenAI respeitavam o schema.
Não é exatamente um bug que você possa reportar e esperar uma correção única. No OpenRouter, o suporte a structured output é definido por endpoint, não por modelo. E esse "suporte" pode ir de uma aplicação nativa e estrita do schema a provedores que tratam sua definição como mera sugestão. Este guia mostra como o recurso é roteado, as seis falhas mais comuns na prática e o que fazer para tornar respostas baseadas em schema viáveis em produção. Os mecanismos de aplicação seguem a documentação oficial de structured outputs; os relatos de falhas vêm de discussões de desenvolvedores linkadas ao longo do texto.
O que o OpenRouter chama de suporte a structured output
O OpenRouter aceita o parâmetro response_format com type: "json_schema", um name para o schema, a flag strict e o próprio JSON Schema. Uma requisição mínima é assim:
{
"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
}
}
}
}
Dois pontos da documentação oficial definem se isso vai funcionar:
- O suporte é por endpoint, não por modelo. Um modelo atendido por cinco provedores pode ter structured outputs funcionando em apenas dois deles. A seção Providers da página do modelo exibe o parâmetro
structured_outputspor provedor, e a documentação alerta que "endpoint support can also change over time." - A cobertura começou pequena. O OpenRouter anunciou structured outputs em 12 de dezembro de 2024, inicialmente só para modelos OpenAI 4o e Fireworks. O restante foi chegando depois, provedor por provedor. Por isso, qualquer lista de modelos disponível hoje envelhece rápido.
A documentação também recomenda incluir descrições em todas as propriedades e definir additionalProperties: false. Nos níveis inferiores de aplicação, afinal, o schema também serve como instrução para o modelo.
Um único strict: true, três níveis de aplicação
O significado de strict: true muda conforme o endpoint que atende a requisição. O guia oficial divide o comportamento dos provedores em três níveis:
| Nível | O que o provedor faz com seu schema | Dá para confiar na saída? |
|---|---|---|
| Modo estrito nativo | Aplica o schema exatamente durante a decodificação | Sim: a saída corresponde ao schema por construção |
| Formato traduzido | Converte seu schema para um formato de structured output específico do provedor | Em grande parte: dentro dos recursos de schema aceitos por esse formato |
| Instrução forte | Insere o schema como orientação para o modelo | Não: pode seguir o formato em um dia e inventar chaves no outro |
O OpenRouter não informa, no momento da requisição, qual nível é usado por cada endpoint; a documentação remete à documentação de cada provedor. Os modos estritos nativos também limitam os recursos de JSON Schema que aceitam. Assim, keywords mais incomuns podem falhar nos endpoints mais rígidos, embora passem como simples instruções em outros.
Há um caso especial documentado para Claude na página de roteamento de provedores: ao usar response_format.type: "json_schema", o OpenRouter aplica automaticamente o beta header structured-outputs-2025-11-13 da Anthropic, que habilita argumentos de ferramentas estritos e validados pelo schema. Já para definições de ferramentas com strict: true enviadas em tools, quem chama a API precisa enviar esse beta header explicitamente. Caso contrário, o OpenRouter remove strict e roteia a requisição sem ele. A falha é silenciosa: suas tool calls deixam de ser validadas pelo schema, sem nenhum erro.
Seis formas de um mesmo schema dar errado
Duas classes de erro falham imediatamente e estão documentadas no guia oficial. As outras quatro aparecem em discussões da comunidade — e são as que consomem uma tarde inteira de debugging.
Falha imediata 1: o endpoint não suporta structured outputs. A requisição retorna um erro informando que esse recurso não é suportado. É incômodo, mas não deixa dúvidas. Falha imediata 2: seu JSON Schema é inválido. A API rejeita a requisição porque o schema não é interpretado corretamente ou viola as regras daquele endpoint.
Falha silenciosa 1: o schema é ignorado. A resposta é um JSON válido, mas para outro schema. Na discussão sobre schemas não seguidos no r/LocalLLaMA, u/DaniyarQQQ relatou:
It returns json that does not look like my schema at all.
Na mesma discussão, u/MicBeckie descreveu a dificuldade de diagnosticar o problema:
I either see successes when the json corresponds exactly to the requirement, or I get an error without being able to look at the json.
Falha do wrapper 2: um 400 sobre tool_choice que você nem enviou. No caso linkado do LangChainJS, withStructuredOutput() implementava "structured output" forçando tool_choice para uma função gerada. Em modelos que anunciam suporte a tool calls, mas não aceitam escolha forçada de ferramenta, a requisição morre com invalid_request_error. No caso do DeepSeek v4, o erro citava diretamente o modelo: deepseek-reasoner does not support this tool_choice. Foi exatamente o que aconteceu com u/shansoft via LangChainJS (discussão). A suposição de u/eyueldk — "it says it supports tool calls, thus should support structured output" — não se confirmou. Suporte a tool calls e suporte estrito a schemas são capacidades distintas.
Falha silenciosa 3: sem erro, sem conteúdo. Um relato sobre gpt-oss-120b descreve uma requisição com schema estrito que retorna 400 ao ir diretamente ao provedor, mas volta pelo OpenRouter com status 200 e message.content vazio. Outra discussão no r/openrouter mostra um modelo "suportado" respondendo apenas [1] ou [1.1]. Um SDK que faz parse de uma string vazia sem reclamar empurra o problema três camadas adiante.
Falha silenciosa 4: o endpoint trava. Sobre endpoints que anunciavam structured output para DeepSeek v4, u/Beneficial-Loss-1031 escreveu nesta discussão:
deepinfra/fp4andakashml/fp8have structured output option, but I waited for 3 min on each for the API to return, but didn't get anything.
| # | Forma da falha | O que aparece | Causa típica |
|---|---|---|---|
| 1 | Endpoint sem suporte | Erro: structured outputs não suportados | Roteamento para um provedor sem essa capacidade |
| 2 | Schema inválido | Erro da API na requisição | O schema viola as regras do endpoint |
| 3 | Schema ignorado | JSON válido, chaves erradas | Aplicação no nível de instrução |
| 4 | 400 de tool_choice | invalid_request_error | SDK emulando schema com tool call forçada |
| 5 | Conteúdo vazio | 200, message.content vazio | Provedor tratando incorretamente o modo estrito |
| 6 | Travamento | Sem resposta por minutos | Não confirmado no relato — espera de 3 minutos nos endpoints fp4/fp8 |
Fortaleça a requisição antes de culpar o modelo
A configuração de maior impacto é require_parameters: true no objeto provider. Por padrão, ela vale false, e parâmetros desconhecidos são encaminhados a provedores que podem ignorá-los silenciosamente. Mesmo com false, response_format e structured outputs funcionam como uma preferência branda entre endpoints: são desejados, não garantidos. Ao definir a flag como true, o roteamento fica limitado a endpoints que suportam todos os parâmetros enviados, como explica a documentação de roteamento de provedores:
{
"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
}
}
Cada restrição reduz o conjunto de provedores elegíveis, e allow_fallbacks: false troca disponibilidade por determinismo. A mesma documentação informa que a estratégia padrão faz balanceamento conforme o uptime e o inverso do quadrado do preço nos 30 segundos anteriores. Isso privilegia opções baratas e saudáveis, não necessariamente capazes de aplicar seu schema. Fixar order em um provedor e desabilitar fallbacks torna o roteamento reproduzível: a requisição deixa de migrar para outro provedor no meio de uma indisponibilidade. Ainda assim, cabe a você verificar o nível de aplicação daquele endpoint.
Dois hábitos de auditoria identificam problemas que o roteamento não resolve:
- Confira qual provedor atendeu a requisição. Os metadados de geração do OpenRouter mostram o roteamento por provedor em cada geração, além de modelo, latência e contagem de tokens. Se a qualidade da saída variar, essa atribuição indica se o comportamento mudou no modelo ou se o roteador trocou de provedor.
- Valide no cliente de qualquer forma. Nenhum dos níveis substitui um parse com Pydantic ou Zod no seu lado. A lição recorrente nas discussões de testes do r/LLMDevs é que "JSON válido", "válido contra o schema" e "semanticamente correto" são três critérios diferentes. E apenas os dois primeiros são, ainda que parcialmente, responsabilidade da API.
Streaming funciona, mas o parsing fica por sua conta
Structured outputs podem ser combinados com stream: true. Segundo a documentação, o modelo transmite JSON parcial válido, e a resposta montada corresponde ao schema quando o stream termina. Essa conformidade herda o nível de aplicação do endpoint: um endpoint que trata o schema como instrução ainda pode montar uma saída fora do padrão. Portanto, valide o objeto final por conta própria. A documentação também não oferece um parser incremental; em interfaces sensíveis à latência, esse é o verdadeiro problema de engenharia. Na discussão sobre boas práticas de streaming no r/LLMDevs:
I just ended up writing a function that completes the JSON myself. — u/am174744
"…it's an actual state machine." — u/ImNotLegitLol, corrigindo a ideia de reparar e depois fazer parse
Na prática, você pode fazer parse parcial com um parser tolerante a streaming, renderizar somente campos já concluídos ou abandonar a renderização incremental e mostrar um indicador de carregamento até que o objeto final esteja montado.
O que o Response Healing corrige — e o que não corrige
O plugin Response Healing do OpenRouter é voltado a requisições json_schema sem streaming e corrige problemas de formatação, como JSON truncado e cercas de Markdown indevidas. Mas dois limites importam mais do que sua lista de correções:
- Streaming fica de fora. A documentação limita o plugin a requisições sem streaming.
- Violações de schema ficam de fora. O Healing torna o JSON interpretável; ele não faz uma resposta que ignorou seu schema passar a obedecê-lo. A falha 3 acima continua intacta.
Como escolher modelos que realmente respeitam schemas
Listas de modelos ficam desatualizadas; critérios de seleção, não. Estes três filtros capturam a maior parte das falhas acima:
- Aplicação estrita nativa. Prefira modelos cujo provedor de serving aplique schemas durante a decodificação, em vez de provedores que traduzem o formato ou usam apenas instruções. A tabela Providers na página do modelo informa quais endpoints anunciam
structured_outputs; o nível do provedor define a qualidade da aplicação. - Um provedor auditável. Compare a atribuição do provedor com um endpoint comprovadamente confiável em várias chamadas. Se o roteador distribui requisições entre provedores de níveis diferentes, sua taxa de falhas vira uma loteria de roteamento. Fixe o provedor ou escolha um modelo de provedor único.
- Um smoke test executado por você, não apenas lido em algum lugar. Os sinais da comunidade envelhecem rápido nos dois sentidos: tanto os relatos de erros com Qwen quanto a ausência de suporte no DeepSeek v4 podem mudar quando provedores atualizam endpoints. O único número de confiabilidade relevante é o que seu próprio schema produz.
Perguntas frequentes sobre structured outputs no OpenRouter
Qual é a diferença entre json_object e json_schema?
json_object apenas pede um JSON sintaticamente válido; json_schema fornece um schema ao qual a resposta deve obedecer. json_object garante a sintaxe JSON, não a conformidade com seu schema em nível de campo. Se seu código downstream depende de campos nomeados, valide por conta própria.
Quais modelos do OpenRouter suportam structured outputs?
Não há uma lista estática em que valha a pena confiar: o suporte é por endpoint, muda ao longo do tempo e começou apenas com modelos OpenAI 4o e Fireworks em dezembro de 2024. Verifique a seção Providers na página do modelo e procure a flag structured_outputs em cada endpoint.
Por que o modelo ignora meu schema?
Há três causas comuns: a requisição foi roteada para um endpoint sem suporte ou que trata o schema como instrução — resolva com require_parameters: true e fixação de provedor; o schema usa keywords que o modo estrito do endpoint rejeita; ou um wrapper de SDK está emulando structured output com tool calling em um modelo que não aceita escolha forçada de ferramenta.
Posso usar Pydantic ou LangChain com structured outputs do OpenRouter?
Sim. A documentação oficial descreve um formato de requisição compatível com a API do OpenRouter no estilo chat completions, de modo que schemas gerados pelo Pydantic e o SDK da OpenAI funcionam diretamente. O withStructuredOutput() do LangChain também funciona, mas verifique se ele envia response_format em vez de emular o recurso por tool_choice. Foi essa emulação que produziu erros 400 no DeepSeek v4.
Structured output funciona com streaming?
Sim. O stream emite JSON parcial válido, mas a conformidade final com o schema depende do nível de aplicação do endpoint — valide você mesmo o objeto montado. O parsing incremental dos fragmentos é responsabilidade da sua aplicação, e o Response Healing não se aplica a streams.
O OpenRouter valida as respostas contra meu schema?
Não como garantia em todos os endpoints: a aplicação depende do nível do provedor, e o Response Healing apenas corrige JSON malformado, não violações de schema. A validação no cliente continua obrigatória.
O smoke test de 10 chamadas
Antes de colocar qualquer modelo em produção por trás de structured outputs, faça o seguinte:
- Defina um schema representativo: complexidade média,
additionalProperties: falsee descrições em todas as propriedades. - Envie 10 requisições idênticas com
strict: trueerequire_parameters: true, mantendo os fallbacks ativos — esta etapa testa deliberadamente o comportamento dos fallbacks, então não os desabilite. - Avalie cada resposta por três critérios: o JSON é interpretável? É válido contra o schema? Faz sentido semanticamente?
- Registre qual provedor atendeu cada resposta pelos metadados de geração. Uma taxa de 10/10 distribuída entre quatro provedores é uma loteria de roteamento, não uma garantia.
- Decida: publique como está, fixe
provider.orderno endpoint que passou e repita as 10 chamadas com o provedor fixado, ou troque de modelo e adicione uma camada de validação e retry no cliente.
O limite de aprovação é você quem define. Mas, com um schema fixo, qualquer resultado abaixo de 9/10 significa que código de retry e validação não é opcional. É parte do produto.
Leitura relacionada: como o auto router do OpenRouter escolhe provedores, como reduzir custos com prompt caching no OpenRouter e como corrigir limites de taxa 429 no OpenRouter.