Fleeexdocs

Sortie structurée

response_format avec un json_schema est honoré nativement, y compris la compilation du premier appel, assez lente pour expirer.

response_format: { type: "json_schema", … } est honoré et transmis au mode contraint par schéma du fournisseur : la réponse est donc contrainte au fil de sa génération, et non suggérée par un prompt.

const completion = await client.chat.completions.create({
  model: "nova-lite",
  messages: [{ role: "user", content: "Extrais la ville et la température." }],
  response_format: {
    type: "json_schema",
    json_schema: {
      name: "weather_reading",
      schema: {
        type: "object",
        properties: {
          city: { type: "string" },
          celsius: { type: "number" },
        },
        required: ["city", "celsius"],
        additionalProperties: false,
      },
    },
  },
});
 
const reading = JSON.parse(completion.choices[0]!.message.content!);

json_schema prend { name, schema, description?, strict? }.

Les trois valeurs de response_format

Valeur
{ type: 'json_schema', … }Honorée. Transmise au champ de sortie structurée du fournisseur.
{ type: 'text' }Acceptée et sans effet. La valeur par défaut d'OpenAI ne demande rien, donc rien n'est transmis.
{ type: 'json_object' }Un 400, dont le message désigne json_schema comme solution de remplacement.

json_object est refusé plutôt qu'approximé, car toute approximation serait malhonnête d'une façon que vous ne pourriez pas détecter. Il n'existe pas de mode « n'importe quel JSON » sans schéma sur lequel le projeter : un schéma { type: 'object' } fabriqué contraindrait la réponse à quelque chose que vous n'avez jamais demandé, et ajouter « réponds en JSON » au prompt changerait le prompt, changerait les tokens d'entrée facturés, et ne garantirait toujours pas du JSON valide, c'est-à-dire précisément la garantie que vend json_object.

strict: false obtient quand même la garantie forte

Le fournisseur contraint la sortie sans condition, puisqu'il n'a pas de mode non strict. strict est donc accepté aux deux valeurs, et strict: false obtient une garantie plus forte que celle qu'OpenAI promet pour cette valeur, jamais une plus faible. Le refuser casserait tout client qui envoie la valeur par défaut d'OpenAI.

(À ne pas confondre avec tools[].function.strict, qui est un autre champ et donne un 400.)

Le premier appel avec un nouveau schéma peut être lent

⚠ Le fournisseur compile la grammaire du schéma la première fois qu'il le voit, ce qui est documenté comme pouvant prendre quelques minutes, puis la met en cache 24 heures. Cette compilation a lieu à l'intérieur de votre appel synchrone, pendant qu'une réservation est tenue sur le portefeuille, et elle peut dépasser le budget de la requête. Quand c'est le cas :

  • l'appel se manifeste par un 502 UPSTREAM_PROVIDER_ERROR ;
  • la réservation est libérée et rien n'est facturé ;
  • en régime établi, les requêtes réutilisent la grammaire en cache et sont rapides.

Faites donc chauffer un nouveau schéma une fois, hors du chemin critique d'un utilisateur, avant de le mettre en production, et ne prenez pas le premier 502 pour un schéma cassé.

Ce qui n'est pas validé localement

Le fournisseur accepte un sous-ensemble documenté de JSON Schema Draft 2020-12 : pas de schéma récursif, pas de $ref externe, aucune contrainte numérique ou de chaîne, et pas d'additionalProperties autre que false. Fleeex ne réimplémente délibérément pas cette vérification, car elle bouge du côté du fournisseur, et un faux 400 sur un schéma que le fournisseur aurait accepté est pire qu'un refus exact en amont.

Conséquences :

  • Un schéma hors de ce sous-ensemble donne un 502 UPSTREAM_PROVIDER_ERROR, avec la réservation libérée et rien de facturé.
  • La prise en charge dépend du modèle. Un modèle qui ne fait pas de sortie structurée du tout refuse l'appel de la même façon. Testez le modèle sur lequel vous comptez livrer.

Validez quand même le corps

Le fournisseur peut s'arrêter sur une raison « sortie de modèle malformée » qui n'a pas d'équivalent OpenAI ; elle est traduite en finish_reason: "stop", comme toute autre raison d'arrêt non reconnue. Une valeur dédiée serait une extension Fleeex à un champ sur lequel les clients OpenAI font des branchements, ce que le contrat du protocole n'autorise pas.

C'est détectable, puisque le corps ne se parsera pas contre votre schéma : ce n'est donc pas une mauvaise réponse silencieuse. Parsez défensivement :

const raw = completion.choices[0]?.message.content;
const parsed = raw ? safeParse(schema, raw) : undefined;
if (!parsed) {
  // Réessayer, ou se rabattre : le modèle s'est arrêté sans corps conforme.
}

Le schéma coûte des tokens d'entrée

Il est sérialisé une fois et transmis sous forme de chaîne, et ces octets sont du contenu de prompt : ils comptent dans l'estimation qui dimensionne la réservation sur le portefeuille, et dans les tokens facturés. Un gros schéma est payé à chaque appel qui le porte.

Limites

Limite
name64 caractères au maximum, sur la grammaire [A-Za-z0-9_-] d'OpenAI
schemaun objet JSON, 16 384 caractères sérialisés au maximum, 10 niveaux de profondeur au maximum
description4 096 caractères au maximum

Un sous-champ non déclaré à l'intérieur de json_schema donne un 400 qui le nomme.