Capabilities
Structured output
When the answer feeds code rather than a person, ask for JSON and validate it.
JSON mode#
Set response_format and the model is constrained to emit syntactically valid JSON.
{
"model": "openai/gpt-5.6-sol",
"response_format": { "type": "json_object" },
"messages": [
{
"role": "system",
"content": "Extract the invoice fields. Reply with JSON matching: { \"vendor\": string, \"total_usd\": number, \"due_date\": string }"
},
{ "role": "user", "content": "Invoice from Acme Corp, $1,240.50, due 2026-09-15" }
]
}JSON mode guarantees syntax, not shape
You get parseable JSON. You do not get your JSON — field names, types and required keys are still up to the model. Describe the schema in the prompt, and validate what comes back.
Describe the shape in the prompt#
The constraint enforces syntax; the prompt is what communicates structure. Be explicit about the keys, the types, and what to do when a value is missing — otherwise a model faced with an absent field will invent something plausible.
Extract these fields from the invoice and reply with JSON only.
{
"vendor": string, // company name exactly as written
"total_usd": number, // no currency symbol, no thousands separator
"due_date": string, // ISO 8601, YYYY-MM-DD
"line_items": [{ "description": string, "amount_usd": number }]
}
If a field is not present in the document, use null. Do not guess.Always validate#
Treat model output as untrusted input, because that is what it is. Parsing straight into your domain objects means a hallucinated field type surfaces three layers away from its cause.
import { z } from "zod";
const Invoice = z.object({
vendor: z.string(),
total_usd: z.number(),
due_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
line_items: z.array(
z.object({ description: z.string(), amount_usd: z.number() }),
),
});
const res = await client.chat.completions.create({
model: "openai/gpt-5.6-sol",
response_format: { type: "json_object" },
messages,
});
// Two separate failure modes, and they need different handling: invalid JSON
// usually means the answer was truncated, while valid JSON of the wrong shape
// means the prompt was ambiguous.
let raw: unknown;
try {
raw = JSON.parse(res.choices[0].message.content ?? "");
} catch {
throw new Error("Model did not return valid JSON — check finish_reason for 'length'");
}
const invoice = Invoice.parse(raw);from pydantic import BaseModel, ValidationError
class LineItem(BaseModel):
description: str
amount_usd: float
class Invoice(BaseModel):
vendor: str
total_usd: float
due_date: str
line_items: list[LineItem]
res = client.chat.completions.create(
model="openai/gpt-5.6-sol",
response_format={"type": "json_object"},
messages=messages,
)
try:
invoice = Invoice.model_validate_json(res.choices[0].message.content)
except ValidationError as e:
# Feed the error back to the model for one retry — it corrects itself
# far more often than a human would guess.
print(e)Tool calling as an alternative#
A single tool with your schema as its parameters is often a better fit than JSON mode: the schema travels with the request instead of living in prose, and arguments come back already separated from any commentary.
{
"tools": [
{
"type": "function",
"function": {
"name": "record_invoice",
"description": "Record the fields extracted from an invoice.",
"parameters": {
"type": "object",
"properties": {
"vendor": { "type": "string" },
"total_usd": { "type": "number" },
"due_date": { "type": "string", "description": "ISO 8601 date" }
},
"required": ["vendor", "total_usd"]
}
}
}
],
"tool_choice": { "type": "function", "function": { "name": "record_invoice" } }
}Forcing the tool with tool_choice guarantees the model answers in that shape. See Tool calling.
Practical notes#
- Lower the temperature. Extraction wants consistency, not creativity. 0 to 0.2 is usually right.
- Give enough max_tokens. Truncated JSON is invalid JSON, and the failure looks like a model problem when it is a budget problem.
- Ask for null, not omission. An explicit
nullis easy to handle; a missing key is indistinguishable from a bug in your parser. - Retry once with the validation error. Models correct their own output remarkably well when told exactly what was wrong.
Watch the finish reason
If parsing fails, check the finish reason before blaming the model. length means the answer was cut off — the JSON was fine, there just was not room for the closing brace.
