Avec OpenRouter, un même schéma JSON peut produire une réponse propre et typée avec un modèle, puis revenir avec des clés différentes, une chaîne vide ou une erreur 400 avec le suivant — sans modifier le corps de la requête. L’utilisateur Reddit u/MicBeckie a testé des modèles Qwen via les sorties structurées d’OpenRouter : « 9 fois sur 10, j’obtenais des erreurs ». Dans la même configuration, les modèles OpenAI respectaient pourtant le schéma.
Ce n’est pas vraiment un bug à signaler. Chez OpenRouter, la prise en charge des sorties structurées se joue endpoint par endpoint, et non modèle par modèle. Surtout, le mot « prise en charge » recouvre trois niveaux d’application : de l’imposition native et stricte du schéma aux fournisseurs qui le traitent comme une simple indication. Ce guide détaille le routage de cette fonctionnalité, les six formes d’échec observées en pratique et les protections à mettre en place avant d’envoyer ce type de sortie en production. Le fonctionnement décrit ici s’appuie sur la documentation officielle des sorties structurées ; les retours d’expérience proviennent des fils de discussion liés dans l’article.
Ce que recouvre réellement la prise en charge des sorties structurées
OpenRouter accepte un paramètre response_format contenant type: "json_schema", un name pour le schéma, le drapeau strict et le schéma JSON lui-même. Voici une requête minimale :
{
"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
}
}
}
}
Deux points de la documentation officielle déterminent si cette approche peut fonctionner :
- La prise en charge dépend de l’endpoint, pas du modèle. Un modèle proposé par cinq fournisseurs peut offrir les sorties structurées chez deux d’entre eux seulement. Dans la section Providers de la page du modèle, le paramètre
structured_outputsest indiqué pour chaque fournisseur. La documentation précise également que « la prise en charge des endpoints peut évoluer au fil du temps ». - La couverture est partie d’un périmètre restreint. OpenRouter a annoncé les sorties structurées le 12 décembre 2024, initialement pour OpenAI 4o et les modèles Fireworks uniquement. Les autres compatibilités sont arrivées ensuite, fournisseur par fournisseur : toute liste de modèles établie aujourd’hui vieillira donc rapidement.
La documentation conseille aussi d’ajouter une description à chaque propriété et d’utiliser additionalProperties: false. Dans les niveaux d’application les moins stricts, le schéma sert également de matériau de prompt.
Un même drapeau, trois niveaux de garantie
La signification de strict: true dépend de l’endroit où aboutit la requête. Le guide officiel distingue trois comportements côté fournisseur :
| Niveau | Traitement du schéma par le fournisseur | Peut-on faire confiance à la sortie ? |
|---|---|---|
| Mode strict natif | Le schéma est imposé exactement pendant le décodage | Oui : la sortie respecte le schéma par construction |
| Format traduit | Le schéma est converti dans le format de sortie structurée propre au fournisseur | En grande partie : dans la limite des fonctionnalités prises en charge par ce format |
| Indication forte | Le schéma est injecté comme consigne au modèle | Non : forme conforme les bons jours, clés inventées les mauvais |
OpenRouter n’indique pas, au moment de la requête, le niveau employé par un endpoint donné ; la documentation renvoie aux documents propres à chaque fournisseur. Les modes stricts natifs limitent aussi les fonctionnalités JSON Schema acceptées : des mots-clés peu courants peuvent échouer sur les endpoints les plus stricts tout en passant comme simples indications ailleurs.
Un cas particulier concernant Claude est documenté sur la page consacrée au routage des fournisseurs. Pour response_format.type: "json_schema", OpenRouter ajoute automatiquement l’en-tête bêta Anthropic structured-outputs-2025-11-13, qui active des arguments d’outils stricts et validés par schéma. En revanche, pour les définitions d’outils avec strict: true envoyées dans tools, l’appelant doit ajouter cet en-tête bêta lui-même. Sinon, OpenRouter retire strict et route la requête sans lui. L’échec est silencieux : les appels d’outils cessent d’être validés par schéma, sans qu’aucune erreur ne soit renvoyée.
Les six pannes possibles avec un schéma identique
Deux catégories échouent immédiatement et sont documentées dans le guide officiel. Quatre autres ressortent des discussions de la communauté — et ce sont souvent celles qui font perdre un après-midi entier.
Échec immédiat n°1 : l’endpoint ne gère pas les sorties structurées. La requête retourne une erreur indiquant que cette capacité n’est pas prise en charge. C’est frustrant, mais sans ambiguïté. Échec immédiat n°2 : le schéma JSON est invalide. L’API refuse la requête parce que le schéma ne peut pas être analysé ou enfreint les règles de l’endpoint.
Échec silencieux n°1 : le schéma est ignoré. La réponse est un JSON valide, mais pour un tout autre schéma. Dans le fil consacré au non-respect des schémas sur r/LocalLLaMA, u/DaniyarQQQ résume :
Il renvoie un json qui ne ressemble absolument pas à mon schéma.
Dans le même fil, u/MicBeckie souligne la difficulté du diagnostic :
Soit je vois des réussites où le json correspond exactement au besoin, soit j’obtiens une erreur sans pouvoir consulter le json.
Échec lié au wrapper n°2 : une 400 sur tool_choice que vous n’avez jamais envoyé. Dans le cas LangChainJS cité, withStructuredOutput() mettait en œuvre la « sortie structurée » en forçant tool_choice sur une fonction générée. Sur les modèles qui annoncent les appels d’outils sans prendre en charge le choix forcé d’un outil, la requête échoue avec invalid_request_error. Avec DeepSeek v4, l’erreur nommait explicitement le modèle : deepseek-reasoner does not support this tool_choice. C’est exactement ce qui est arrivé à u/shansoft avec LangChainJS (fil) : l’hypothèse de u/eyueldk, « il indique prendre en charge les appels d’outils, donc il devrait gérer les sorties structurées », s’est révélée fausse. La prise en charge des appels d’outils et celle des schémas stricts sont deux capacités distinctes.
Échec silencieux n°3 : ni erreur, ni contenu. Un retour sur gpt-oss-120b décrit une requête à schéma strict qui déclenche une 400 en routage direct chez le fournisseur, mais retourne via OpenRouter une 200 avec un message.content vide. Un autre fil sur r/openrouter montre un modèle pourtant « compatible » ne renvoyant que [1] ou [1.1]. Un SDK qui analyse sans broncher une chaîne vide ne fait que repousser le problème de trois couches.
Échec silencieux n°4 : l’endpoint ne répond jamais. À propos d’endpoints qui annonçaient les sorties structurées pour DeepSeek v4, u/Beneficial-Loss-1031 écrit dans ce fil :
deepinfra/fp4etakashml/fp8proposent l’option de sortie structurée, mais j’ai attendu 3 min pour chacun sans jamais obtenir de réponse de l’API.
| # | Forme d’échec | Ce que vous observez | Cause typique |
|---|---|---|---|
| 1 | Endpoint non compatible | Erreur : sorties structurées non prises en charge | Routage vers un fournisseur sans cette capacité |
| 2 | Schéma invalide | Erreur API lors de la requête | Le schéma enfreint les règles de l’endpoint |
| 3 | Schéma ignoré | JSON valide, mais mauvaises clés | Application au niveau indication |
| 4 | 400 sur tool_choice | invalid_request_error | SDK qui simule un schéma via un appel d’outil forcé |
| 5 | Contenu vide | 200, message.content vide | Mauvaise gestion du mode strict chez le fournisseur |
| 6 | Blocage | Aucune réponse pendant plusieurs minutes | Cause non confirmée dans le signalement — attente de 3 minutes sur les endpoints fp4/fp8 |
Fiabilisez d’abord la requête, avant d’accuser le modèle
Le réglage le plus déterminant est require_parameters: true dans l’objet provider. Sa valeur par défaut est false : les paramètres inconnus sont alors transmis à des fournisseurs qui peuvent les ignorer silencieusement. Même avec false, response_format et les sorties structurées ne constituent qu’une préférence souple entre les endpoints : souhaitées, mais non garanties. Avec true, le routage est limité aux endpoints qui prennent en charge tous les paramètres envoyés, conformément à la documentation sur le routage des fournisseurs :
{
"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
}
}
Chaque restriction réduit le nombre de fournisseurs éligibles, et allow_fallbacks: false échange de la disponibilité contre du déterminisme. Cette même documentation explique que la stratégie par défaut répartit la charge selon le temps de disponibilité et l’inverse du carré du prix sur les 30 secondes précédentes. Elle privilégie donc le fournisseur sain et économique, pas nécessairement celui qui applique correctement le schéma. En fixant order sur un seul fournisseur et en désactivant les solutions de repli, le routage devient reproductible : une requête ne peut plus dériver vers un autre fournisseur pendant une panne. Il vous reste toutefois à vérifier le niveau d’application de cet endpoint.
Deux habitudes d’audit permettent de détecter ce que le routage ne peut pas résoudre :
- Identifiez le fournisseur qui a réellement servi la requête. Les métadonnées de génération d’OpenRouter exposent le routage fournisseur de chaque génération, ainsi que le modèle, la latence et les volumes de tokens. Si la qualité dérive, cette attribution permet de savoir si le modèle a changé de comportement ou si le routeur a changé de fournisseur.
- Validez toujours côté client. Aucun des trois niveaux ne remplace une validation Pydantic ou Zod dans votre application. La leçon qui revient dans les fils de test r/LLMDevs est simple : « JSON valide », « conforme au schéma » et « sémantiquement juste » sont trois seuils différents. Seuls les deux premiers relèvent, même partiellement, de l’API.
Streaming : pris en charge, parsing à votre charge
Les sorties structurées fonctionnent avec stream: true. Selon la documentation, le modèle diffuse du JSON partiel valide et la réponse assemblée doit respecter le schéma une fois le flux terminé. Cette conformité dépend néanmoins du niveau d’application de l’endpoint : un endpoint au niveau indication peut toujours produire un résultat final non conforme. Validez donc l’objet final vous-même. La documentation ne fournit pas non plus de parseur incrémental ; pour une interface sensible à la latence, c’est le véritable sujet d’ingénierie. Dans le fil de bonnes pratiques sur le streaming de r/LLMDevs :
J’ai fini par écrire moi-même une fonction qui complète le JSON. — u/am174744
« … c’est une vraie machine à états. » — u/ImNotLegitLol, en corrigeant l’idée d’une simple réparation suivie d’un parsing
En pratique, vous pouvez utiliser un parseur tolérant au JSON partiel, n’afficher que les champs terminés, ou renoncer au rendu incrémental et montrer un indicateur de chargement jusqu’à l’assemblage de l’objet final.
Ce que Response Healing corrige — et ce qu’il ne corrigera jamais
Le plugin Response Healing d’OpenRouter cible les requêtes json_schema sans streaming et répare les défauts de formatage : JSON tronqué, balises Markdown parasites et problèmes du même type. Ses limites sont plus importantes que sa liste de corrections :
- Le streaming n’est pas couvert. La documentation limite le plugin aux requêtes non diffusées en flux.
- Les violations de schéma ne sont pas couvertes. Healing rend le JSON analysable ; il ne rend pas conforme à votre schéma une réponse qui l’a ignoré. L’échec n°3 ci-dessus reste donc intact.
Choisir des modèles qui respectent vraiment les schémas
Les listes de modèles deviennent vite obsolètes ; les critères de sélection, beaucoup moins. Ces trois filtres évitent la plupart des échecs décrits plus haut :
- Une application stricte native. Préférez les modèles dont le fournisseur impose les schémas pendant le décodage à ceux qui les traduisent ou les utilisent comme indication. Le tableau Providers de la page du modèle indique quels endpoints annoncent
structured_outputs; c’est le niveau du fournisseur qui détermine la qualité d’application. - Un fournisseur identifiable et auditable. Comparez l’attribution du fournisseur à celle d’un endpoint connu comme fiable sur plusieurs appels. Si le routeur répartit les requêtes entre des fournisseurs de niveaux différents, votre taux d’échec devient une loterie de routage. Épinglez le fournisseur ou choisissez un modèle servi par un seul fournisseur.
- Un test rapide que vous avez exécuté vous-même. Les signaux communautaires vieillissent vite, dans les deux sens : les erreurs rapportées pour Qwen et l’absence de prise en charge de DeepSeek v4 peuvent évoluer à mesure que les fournisseurs mettent à jour leurs endpoints. Le seul chiffre de fiabilité utile est celui produit par votre propre schéma.
FAQ : sorties structurées OpenRouter
Quelle différence entre json_object et json_schema ?
json_object demande seulement un JSON syntaxiquement valide ; json_schema fournit un schéma auquel la réponse doit se conformer. json_object garantit la syntaxe JSON, pas la conformité à vos champs : si votre code en aval dépend de champs nommés, validez la réponse vous-même.
Quels modèles OpenRouter prennent en charge les sorties structurées ?
Il n’existe pas de liste statique fiable : la prise en charge dépend de l’endpoint, évolue au fil du temps et ne concernait initialement, en décembre 2024, que OpenAI 4o et les modèles Fireworks. Consultez la section Providers de la page du modèle et le drapeau structured_outputs pour chaque endpoint.
Pourquoi le modèle ignore-t-il mon schéma ?
Trois causes sont fréquentes : la requête a été routée vers un endpoint au niveau indication ou non compatible — corrigez cela avec require_parameters: true et l’épinglage du fournisseur ; le schéma emploie des mots-clés refusés par le mode strict de l’endpoint ; ou un wrapper SDK émule la sortie structurée par des appels d’outils sur un modèle qui ne prend pas en charge le choix forcé d’un outil.
Puis-je utiliser Pydantic ou LangChain avec les sorties structurées OpenRouter ?
Oui. La documentation officielle présente le format de requête comme compatible avec l’API OpenRouter de type chat completions : les schémas générés par Pydantic et le SDK OpenAI fonctionnent donc directement. withStructuredOutput() de LangChain fonctionne également, mais vérifiez qu’il envoie bien response_format plutôt que d’émuler la fonctionnalité avec tool_choice ; c’est ce dernier mécanisme qui a provoqué des erreurs 400 sur DeepSeek v4.
Les sorties structurées fonctionnent-elles en streaming ?
Oui. Le flux émet du JSON partiel valide, mais la conformité finale au schéma dépend du niveau d’application de l’endpoint : validez donc vous-même l’objet assemblé. Le parsing incrémental des fragments relève de votre application, et Response Healing ne s’applique pas aux flux.
OpenRouter valide-t-il les réponses par rapport à mon schéma ?
Pas avec une garantie valable sur tous les endpoints : l’application dépend du niveau du fournisseur, et Response Healing ne répare que le JSON mal formé, pas les violations de schéma. La validation côté client reste indispensable.
Le test rapide en 10 appels
Avant de mettre un modèle en production derrière des sorties structurées, procédez ainsi :
- Fixez un schéma représentatif : complexité moyenne,
additionalProperties: falseet descriptions sur toutes les propriétés. - Envoyez 10 requêtes identiques avec
strict: trueetrequire_parameters: true, en laissant les solutions de repli actives. Cette passe teste volontairement le comportement des fallback : ne les désactivez pas. - Évaluez chaque réponse selon trois critères : JSON analysable ? Conforme au schéma ? Sémantiquement cohérente ?
- Relevez le fournisseur ayant servi chaque réponse à l’aide des métadonnées de génération. Un taux de 10/10 réparti entre quatre fournisseurs reste une loterie de routage, pas une garantie.
- Décidez ensuite : déployer tel quel, fixer
provider.ordersur l’endpoint qui a réussi puis répéter les 10 appels avec ce fournisseur épinglé, ou changer de modèle et ajouter une couche côté client de validation et de nouvelle tentative.
À vous de fixer le seuil acceptable, mais un score inférieur à 9/10 avec un schéma fixe signifie que les nouvelles tentatives et le code de validation ne sont pas facultatifs. Ils font partie du produit.
À lire également : comment l’auto-router d’OpenRouter choisit les fournisseurs, réduire les coûts avec le prompt caching d’OpenRouter et résoudre les limites de débit OpenRouter 429.