Con OpenRouter, lo stesso schema JSON può produrre un output pulito e tipizzato con un modello, poi restituire chiavi diverse, una stringa vuota o un errore 400 con il successivo, pur inviando lo stesso identico body. L'utente Reddit u/MicBeckie ha provato modelli Qwen tramite gli structured output di OpenRouter e ha riferito che "9 times out of 10 I always got errors"; nella medesima configurazione, i modelli OpenAI rispettavano invece lo schema.
Non è il genere di problema che si risolve aprendo un bug report. In OpenRouter il supporto agli structured output dipende dal singolo endpoint, non dal modello in astratto. E il termine "supporto" copre tre livelli di enforcement: si va dalla convalida nativa e rigorosa dello schema fino ai provider che lo trattano come un semplice suggerimento. Vediamo come funziona il routing, le sei modalità con cui le richieste falliscono nella pratica e le misure necessarie per usare output vincolati da schema in produzione. I meccanismi di enforcement seguono la documentazione ufficiale sugli structured output; gli esempi di problemi arrivano da discussioni tra sviluppatori linkate nel testo.
Cosa significa davvero supportare gli structured output in OpenRouter
OpenRouter accetta il parametro response_format con type: "json_schema", un name per lo schema, il flag strict e lo JSON Schema vero e proprio. Una richiesta minima può essere questa:
{
"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
}
}
}
}
Due dettagli della documentazione ufficiale determinano se tutto questo funzionerà oppure no:
- Il supporto è legato all'endpoint, non al modello. Un modello servito da cinque provider può avere gli structured output funzionanti soltanto su due. Nella sezione Providers della pagina del modello compare il parametro
structured_outputsper ciascun provider; la documentazione avverte inoltre che "endpoint support can also change over time." - La copertura è partita da una base ristretta. OpenRouter ha annunciato gli structured output il 12 dicembre 2024, inizialmente solo per OpenAI 4o e i modelli Fireworks. Tutto il resto è arrivato in seguito, provider per provider: qualunque elenco di modelli scritto oggi invecchia in fretta.
La documentazione raccomanda anche di aggiungere una descrizione a ogni proprietà e di impostare additionalProperties: false. Nei livelli di enforcement più deboli, infatti, lo schema finisce per svolgere anche il ruolo di istruzione nel prompt.
Un solo flag, tre livelli di enforcement
Il significato di strict: true cambia in base all'endpoint su cui atterra la richiesta. La guida ufficiale suddivide il comportamento dei provider in tre fasce:
| Livello | Cosa fa il provider con lo schema | Puoi fidarti dell'output? |
|---|---|---|
| Modalità strict nativa | Applica lo schema in modo esatto durante la decodifica | Sì: l'output corrisponde allo schema per costruzione |
| Formato tradotto | Converte lo schema in un formato di structured output specifico del provider | In gran parte sì: entro i limiti delle funzionalità supportate da quel formato |
| Indicazione forte | Inietta lo schema come guida per il modello | No: con buone condizioni somiglia allo schema, con cattive condizioni inventa chiavi |
OpenRouter non indica, al momento della richiesta, quale livello utilizzi un dato endpoint: la documentazione rimanda alle fonti dei singoli provider. Le modalità strict native limitano inoltre le funzionalità JSON Schema accettate. Keyword più insolite possono quindi fallire sugli endpoint più rigidi, pur passando come semplici suggerimenti altrove.
La pagina sul routing dei provider documenta un caso particolare per Claude: per response_format.type: "json_schema", OpenRouter applica automaticamente l'header beta di Anthropic structured-outputs-2025-11-13, che abilita argomenti degli strumenti rigorosi e validati rispetto allo schema. Per definizioni di tool con strict: true inviate tramite tools, invece, chi chiama l'API deve inviare esplicitamente quell'header beta. In caso contrario OpenRouter rimuove strict e instrada la richiesta senza di esso. È un errore silenzioso: le tool call smettono di essere validate rispetto allo schema, senza che venga generato alcun errore.
Sei modi in cui può fallire lo stesso schema
Due classi di errore si manifestano subito e sono documentate nella guida ufficiale. Le altre quattro emergono dalle discussioni della community, e sono quelle che fanno perdere interi pomeriggi.
Errore immediato 1: l'endpoint non supporta gli structured output. La richiesta fallisce dichiarando che la capability non è supportata. È fastidioso, ma almeno inequivocabile. Errore immediato 2: lo JSON Schema non è valido. L'API rifiuta la richiesta perché lo schema non viene parsato oppure viola le regole dell'endpoint.
Errore silenzioso 1: lo schema viene ignorato. La risposta è JSON valido, ma per uno schema completamente diverso. Nella discussione sugli schemi non rispettati su r/LocalLLaMA, u/DaniyarQQQ scrive:
It returns json that does not look like my schema at all.
Sullo stesso thread, u/MicBeckie descrive così il problema di diagnosi:
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.
Errore del wrapper 2: un 400 su tool_choice che non hai mai inviato. Nel caso LangChainJS linkato, withStructuredOutput() implementava gli "structured output" forzando tool_choice su una funzione generata. Con modelli che dichiarano di supportare le tool call ma non la scelta forzata di un tool, la richiesta termina con invalid_request_error; nel caso DeepSeek v4, l'errore citava direttamente il modello: deepseek-reasoner does not support this tool_choice. u/shansoft ha incontrato esattamente questo problema con LangChainJS (thread), dove l'assunto di u/eyueldk — "it says it supports tool calls, thus should support structured output" — si è rivelato falso. Supporto alle tool call e supporto rigoroso dello schema sono capability separate.
Errore silenzioso 3: nessun errore, nessun contenuto. Un report su gpt-oss-120b descrive una richiesta con schema strict che restituisce 400 su una rotta diretta al provider, ma che attraverso OpenRouter riceve un 200 con message.content vuoto. Un'altra discussione su r/openrouter mostra un modello dichiarato "supportato" che restituisce soltanto [1] oppure [1.1]. Un SDK che prova serenamente a parsare una stringa vuota sposta il problema tre livelli più a valle.
Errore silenzioso 4: l'endpoint resta bloccato. u/Beneficial-Loss-1031, parlando di endpoint che dichiaravano structured output per DeepSeek v4 (thread), racconta:
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 del problema | Cosa osservi | Causa tipica |
|---|---|---|---|
| 1 | Endpoint non supportato | Errore: structured output non supportati | Routing verso un provider privo della capability |
| 2 | Schema non valido | Errore API sulla richiesta | Lo schema viola le regole dell'endpoint |
| 3 | Schema ignorato | JSON valido, chiavi errate | Enforcement a livello di indicazione |
| 4 | 400 su tool_choice | invalid_request_error | SDK che emula lo schema con una tool call forzata |
| 5 | Contenuto vuoto | 200, message.content vuoto | Il provider gestisce male la modalità strict |
| 6 | Blocco | Nessuna risposta per minuti | Non confermato nel report: attesa di 3 minuti sugli endpoint fp4/fp8 |
Prima di accusare il modello, rendi solida la richiesta
L'impostazione con il maggiore impatto è require_parameters: true nell'oggetto provider. Per impostazione predefinita vale false, e i parametri sconosciuti vengono inoltrati a provider che potrebbero ignorarli senza segnalarlo. Anche con false, response_format e gli structured output agiscono soltanto come preferenza morbida tra gli endpoint: sono preferiti, non garantiti. Impostando il flag su true, il routing viene limitato agli endpoint che supportano ogni parametro inviato, come spiegato nella documentazione sul routing dei provider:
{
"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
}
}
Ogni restrizione riduce il numero di provider selezionabili, mentre allow_fallbacks: false scambia disponibilità con determinismo. La stessa documentazione descrive la strategia predefinita come un bilanciamento basato sull'uptime e sull'inverso del quadrato del prezzo nei 30 secondi precedenti: ottimizza quindi per provider economici e in salute, non necessariamente capaci di rispettare lo schema. Fissare order a un solo provider e disabilitare i fallback rende il routing riproducibile: durante un disservizio la richiesta non può migrare verso un provider differente. Resta comunque da verificare il livello di enforcement di quell'unico endpoint.
Due abitudini di audit intercettano ciò che il routing non può risolvere:
- Controlla quale provider ha servito la richiesta. I metadati della generazione di OpenRouter espongono il routing del provider per ogni generazione, insieme a modello, latenza e conteggio dei token. Se la qualità dell'output cambia, questa attribuzione consente di capire se è mutato il comportamento del modello o se il router ha cambiato provider.
- Valida sempre lato client. Nessuno dei tre livelli sostituisce un parsing Pydantic o Zod dalla tua parte. La lezione ricorrente nelle discussioni di test su r/LLMDevs è che "JSON valido", "valido rispetto allo schema" e "semanticamente corretto" sono tre soglie distinte; soltanto le prime due rientrano, e solo in parte, nel lavoro dell'API.
Lo streaming funziona, ma il parsing tocca a te
Gli structured output sono compatibili con stream: true. La documentazione descrive un contratto in cui il modello trasmette JSON parziale valido e la risposta assemblata rispetta lo schema una volta completato lo stream. Questa conformità eredita il livello di enforcement dell'endpoint: un endpoint che opera per suggerimento può comunque assemblare un output non conforme. Valida quindi sempre l'oggetto finale. La documentazione non fornisce neppure un parser incrementale; per le UI sensibili alla latenza, è qui che si trova il vero problema ingegneristico. Dalla discussione sulle buone pratiche per lo streaming su r/LLMDevs:
I just ended up writing a function that completes the JSON myself. — u/am174744
"…it's an actual state machine." — u/ImNotLegitLol, correggendo l'idea di riparare e poi parsare
Le opzioni pratiche sono tre: usare un parser tollerante al JSON parziale, mostrare soltanto i campi completi oppure rinunciare al rendering incrementale e visualizzare un indicatore di caricamento finché l'oggetto finale non è pronto.
Cosa risolve Response Healing e cosa non può risolvere
Il plugin Response Healing di OpenRouter interviene sulle richieste json_schema non in streaming e ripara formattazioni imperfette: JSON troncato, fence Markdown residue e problemi di questo tipo. Due limiti contano più dei casi che riesce a correggere:
- Lo streaming è escluso. La documentazione limita il plugin alle richieste non in streaming.
- Le violazioni dello schema restano escluse. Healing rende il JSON parsabile, ma non rende conforme allo schema una risposta che lo ha ignorato. L'errore numero 3 elencato sopra non viene toccato.
Come scegliere modelli che rispettano davvero gli schemi
Le liste di modelli invecchiano; i criteri di selezione no. Tre filtri intercettano la maggior parte dei problemi visti sopra:
- Enforcement strict nativo. Privilegia modelli il cui provider di serving applica gli schemi durante la decodifica, anziché provider che li traducono o li usano come suggerimento. La tabella Providers della pagina del modello mostra gli endpoint che dichiarano
structured_outputs; la qualità dell'enforcement dipende dal livello del provider. - Un solo provider verificabile. Confronta l'attribuzione del provider con un endpoint noto e affidabile su più chiamate. Se il router distribuisce le richieste tra provider appartenenti a livelli diversi, il tasso di fallimento diventa una lotteria di routing. Fissa il provider oppure scegli un modello disponibile presso un solo provider.
- Uno smoke test eseguito da te, non letto online. I segnali della community invecchiano rapidamente in entrambe le direzioni: sia i report sugli errori Qwen riportati sopra sia il supporto mancante di DeepSeek v4 possono cambiare quando i provider aggiornano gli endpoint. L'unico dato di affidabilità che conta è quello prodotto dal tuo schema.
FAQ sugli structured output di OpenRouter
Che differenza c'è tra json_object e json_schema?
json_object chiede soltanto JSON sintatticamente valido; json_schema fornisce uno schema a cui la risposta deve conformarsi. json_object garantisce la sintassi JSON, non la conformità al tuo schema di campi: se il codice a valle richiede campi nominati, devi validare autonomamente.
Quali modelli OpenRouter supportano gli structured output?
Non esiste un elenco statico di cui fidarsi: il supporto è per endpoint, cambia nel tempo ed è partito nel dicembre 2024 soltanto con OpenAI 4o e i modelli Fireworks. Controlla il flag structured_outputs per ciascun endpoint nella sezione Providers della pagina del modello.
Perché il modello ignora il mio schema?
Le cause più comuni sono tre: la richiesta è stata instradata verso un endpoint senza supporto o con enforcement a livello di suggerimento, da correggere con require_parameters: true e il pinning del provider; lo schema usa keyword che la modalità strict dell'endpoint rifiuta; oppure un wrapper SDK emula lo structured output tramite tool calling su un modello che non supporta la scelta forzata del tool.
Posso usare Pydantic o LangChain con gli structured output di OpenRouter?
Sì. La documentazione ufficiale descrive un formato di richiesta compatibile con l'API in stile chat completions di OpenRouter, quindi gli schemi generati da Pydantic e l'SDK OpenAI funzionano direttamente. Anche withStructuredOutput() di LangChain funziona, ma verifica che invii response_format invece di emulare tutto tramite tool_choice: è proprio questo che ha prodotto errori 400 con DeepSeek v4.
Gli structured output funzionano in streaming?
Sì. Lo stream emette JSON parziale valido, ma la conformità finale allo schema dipende dal livello di enforcement dell'endpoint: valida sempre l'oggetto assemblato. Il parsing incrementale dei frammenti è responsabilità dell'applicazione e Response Healing non si applica agli stream.
OpenRouter valida le risposte rispetto al mio schema?
Non con una garanzia valida per tutti gli endpoint: l'enforcement dipende dal livello del provider e Response Healing ripara soltanto JSON malformato, non le violazioni dello schema. La validazione lato client rimane obbligatoria.
Lo smoke test da 10 chiamate
Prima di mettere in produzione un modello dietro agli structured output, esegui questo test:
- Fissa uno schema rappresentativo: complessità media,
additionalProperties: falsee descrizioni per tutte le proprietà. - Invia 10 richieste identiche con
strict: trueerequire_parameters: true, con fallback abilitati. Questo passaggio serve volutamente a testare il comportamento dei fallback, quindi lasciali attivi. - Valuta ogni risposta su tre livelli: JSON parsabile? Valido rispetto allo schema? Semanticamente sensato?
- Registra il provider che ha servito ogni risposta tramite i metadati della generazione. Un 10/10 ottenuto da quattro provider diversi è una lotteria di routing, non una garanzia.
- Decidi: pubblicare così com'è, fissare
provider.ordersull'endpoint che ha superato il test e ripetere le 10 chiamate con provider bloccato, oppure cambiare modello e aggiungere un livello lato client di validazione e retry.
La soglia di superamento la decidi tu, ma con meno di 9/10 su uno schema fisso, retry e codice di validazione non sono opzionali. Sono parte del prodotto.
Approfondimenti correlati: come l'auto router di OpenRouter sceglie i provider, come ridurre i costi con il prompt caching di OpenRouter e come risolvere i rate limit 429 di OpenRouter.