Structured output
Make a model return JSON that actually parses, optionally against your schema.
Pass a response_format and the model server constrains generation so the answer is valid JSON. This is real decoding-time constraint, not a prompt trick: tokens that would break the format are never sampled.
How it works
ThaiRouter forwards response_format to the vLLM server unchanged, where it drives guided decoding. The model's content comes back as a JSON string that you parse yourself; the field is not pre-parsed for you. Two modes are supported, both under the OpenAI-compatible shape.
| Field | Type | Description |
|---|---|---|
| response_format.typerequired | "json_object" | "json_schema" | json_object for any valid JSON; json_schema to conform to a schema. |
| response_format.json_schema | object | Required when type is json_schema. Holds name, the JSON schema, and optional strict. |
JSON object mode
The loosest option: the answer is guaranteed to be a parseable JSON value, but its shape is up to the model. Tell the model what keys you want in the prompt.
{
"model": "thairouter/glm-5.3-flash",
"messages": [
{ "role": "system", "content": "ตอบเป็น JSON object ที่มี key: summary (string), tags (array of string)" },
{ "role": "user", "content": "ข่าว: ..." }
],
"response_format": { "type": "json_object" }
}JSON schema mode
The strict option: the output is forced to match a JSON Schema you supply. Use strict: true and set additionalProperties: false so the model can't add stray keys. This is the mode to use for data extraction and anything you feed to typed code.
curl https://api.thairouter.ai/v1/chat/completions \
-H "Authorization: Bearer $THAIROUTER_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "thairouter/glm-5.3-flash",
"messages": [
{"role": "system", "content": "แยกข้อมูลจากข้อความเป็น JSON"},
{"role": "user", "content": "สมชาย อายุ 32 อยู่กรุงเทพ"}
],
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "person",
"strict": true,
"schema": {
"type": "object",
"properties": {
"name": { "type": "string" },
"age": { "type": "integer" },
"province": { "type": "string" }
},
"required": ["name", "age", "province"],
"additionalProperties": false
}
}
}
}'json_schema block for you: zodResponseFormat in TypeScript, and client.chat.completions.parse(...) with a Pydantic model in Python. Both work through ThaiRouter because the wire format is identical.vLLM structured_outputs
Because unknown fields are forwarded as-is (see Parameters), vLLM's own constrained-decoding object works too. Send a top-level structured_outputs with exactly one of the keys below when a JSON Schema isn't the right tool. All four are verified against production.
| structured_outputs key | Constrains output to |
|---|---|
| choice | One of a fixed list of strings. Good for classification; output is the bare string, no JSON. |
| regex | A regular expression. |
| grammar | A Lark grammar (must define a start rule). |
| json | A JSON Schema (same effect as response_format json_schema, without the name / strict wrapper). |
{
"model": "thairouter/glm-5.3-flash",
"messages": [{ "role": "user", "content": "รีวิวนี้บวกหรือลบ: ของดีมาก ส่งไว" }],
"structured_outputs": { "choice": ["positive", "negative", "neutral"] },
"reasoning_effort": "low",
"max_tokens": 8
}
// → "content": "positive"{
"model": "thairouter/glm-5.3-flash",
"messages": [{ "role": "user", "content": "is 7 prime?" }],
"structured_outputs": { "grammar": "start: \"yes\" | \"no\"" },
"max_tokens": 8
}
// → "content": "yes"guided_choice / guided_regex / guided_grammar / guided_json fields are silently ignored by the current model server: you get a normal, unconstrained answer (typically cut off with finish_reason: "length"). Use structured_outputs or response_format. For portability across providers, prefer response_format with an enum in the schema; it gives the same classification result as choice, wrapped in a JSON object.Prompting
- Still describe the JSON you want in the prompt. Constrained decoding fixes the shape, not the content; a model told nothing will fill a schema with plausible nonsense.
- In schema mode, put field meanings in the schema's
descriptionstrings; the model sees them. - Keep schemas shallow where you can. Deeply nested or huge schemas raise latency and the chance of hitting
max_tokensmid-object.
Streaming & billing
Streaming works with response_format: delta.content chunks arrive as usual and concatenate into the full JSON string, which is only complete at the end. Don't parse partial chunks. Billing is unchanged; constrained output is billed as ordinary completion tokens. See Streaming and Billing.
max_tokens the JSON will be truncated and won't parse. Give schema-mode requests enough max_tokens for the whole object, and check finish_reason is stop, not length, before parsing.Notes & limits
- Support depends on the model server. Every live model runs on vLLM with guided decoding available; if a request is rejected the upstream 400 is passed through and the reservation refunded (see Errors).
- On reasoning models, only the final
contentis constrained;reasoningis free-form. Parsecontent. - Not every JSON Schema keyword is honoured. Stick to
type,properties,required,enum,items,additionalPropertiesand basic string/number bounds; exotic keywords may be ignored. - The response
contentis a JSON string. AlwaysJSON.parse/json.loadsit yourself.