Skip to contentKumoDocs
Sections
On this page
The gateway

Structured output

An answer to a named JSON Schema — what format the API takes, what strict means, how it is spelled on the three dialects, and why support is proven separately for every model.

View as Markdown

Quick

Add response_format and the answer comes back to your schema instead of as free text.

{
  "model": "<model>",
  "max_tokens": 256,
  "messages": [{ "role": "user", "content": "Parse this address: 15 Baumana St, Kazan." }],
  "response_format": {
    "type": "json_schema",
    "json_schema": {
      "name": "postal_address",
      "strict": true,
      "schema": {
        "type": "object",
        "properties": [
          { "name": "city", "type": "string", "description": "The city." },
          { "name": "street", "type": "string", "description": "The street." },
          { "name": "house", "type": "string", "description": "The house number." }
        ]
      }
    }
  }
}
import json
import os

from openai import OpenAI

client = OpenAI(base_url="https://api.kumorouter.com/v1", api_key=os.environ["KUMO_API_KEY"])

answer = client.chat.completions.create(
    model="<model>",
    max_tokens=256,
    messages=[{"role": "user", "content": "Parse this address: 15 Baumana St, Kazan."}],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "postal_address",
            "strict": True,
            "schema": {
                "type": "object",
                "properties": [
                    {"name": "city", "type": "string", "description": "The city."},
                    {"name": "street", "type": "string", "description": "The street."},
                    {"name": "house", "type": "string", "description": "The house number."},
                ],
            },
        },
    },
)
address = json.loads(answer.choices[0].message.content)

What a schema looks like

The schema travels under a name — carried to the supplier as this protocol carries it — and follows the published profile.

nameThe schema's name: letters, digits, hyphen and underscore, up to 64 characters.
strictRequired, and it is `true`.
schemaThe schema itself: the root is an object, and its fields are listed.

properties is a list of named fields, not a map. A field carries name, type and an optional description, and there are five types: boolean, integer, null, number, string. A list rather than a map, because every object on this contract closes itself and a map of arbitrary names cannot.

The member has no other kind of format. A bare "just JSON" mode is not offered here: it promises valid JSON and nothing about its shape, and that is a downgrade this platform will not make silently.

What strict means

strict: true means the supplier must enforce the schema rather than be encouraged toward it. The member takes no other value: strict: false is refused by name, because a schema that is not enforced is a suggestion rather than a contract, and serving two different guarantees under one name would be worse than refusing.

Strictness is proven separately from structured output itself. A model-and-channel pair that has not proven strict semantics refuses such a request before anything upstream is contacted, rather than trying and downgrading.

On the other dialects

On Responses the same requirement lives in text.format and carries four members at once: type, name, strict and schema.

{
  "text": {
    "format": {
      "type": "json_schema",
      "name": "postal_address",
      "strict": true,
      "schema": {
        "type": "object",
        "properties": [{ "name": "city", "type": "string" }]
      }
    }
  }
}

The Messages dialect has no structured-output member at all. When you want a parsed object there, declare a tool: its input_schema is your own schema, and the call's arguments arrive as an object.

How to declare a tool →

In a stream

A structured answer streams in the same deltas as ordinary text: the JSON arrives in fragments and is parseable only whole. Parse after the terminal frame, not on the way.

Streaming a structured answer is a proven capability of its own, separate from streaming and from structured output alike: the catalog keeps a structured_output_streaming_status cell for the pair.

The terminal-frame rule →

When the model refuses

A model may refuse to answer. Then content arrives as null and refusal sits beside it, in the model's own words. That is an answer, not an error: the request was served, the supplier accounted for it, and it is settled like any other. No error envelope arrives, the status stays successful, and the client has to read refusal for itself.

On Responses a refusal arrives as an output part of kind refusal, and in a stream as the response.refusal.delta event.

A schema is proven per model

Note

Structured output is a proven capability of an exact model and channel. The catalog keeps four cells for it: structured_output_status, structured_output_mode, strict_semantics_status and structured_output_streaming_status. Each of the three status cells takes proven, unproven or unsupported, and "unproven" never means "probably supported": a request to an unproven pair is refused before anything upstream is contacted. What a given model has proven is on its card in the catalog.

Next