thairouter
API

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.

FieldTypeDescription
response_format.typerequired"json_object" | "json_schema"json_object for any valid JSON; json_schema to conform to a schema.
response_format.json_schemaobjectRequired 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
        }
      }
    }
  }'
The OpenAI SDK helpers build the 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 keyConstrains output to
choiceOne of a fixed list of strings. Good for classification; output is the bare string, no JSON.
regexA regular expression.
grammarA Lark grammar (must define a start rule).
jsonA JSON Schema (same effect as response_format json_schema, without the name / strict wrapper).
Classification with structured_outputs.choice
{
  "model": "thairouter/glm-5.3-flash",
  "messages": [{ "role": "user", "content": "รีวิวนี้บวกหรือลบ: ของดีมาก ส่งไว" }],
  "structured_outputs": { "choice": ["positive", "negative", "neutral"] },
  "reasoning_effort": "low",
  "max_tokens": 8
}
// → "content": "positive"
Yes / no with a Lark grammar
{
  "model": "thairouter/glm-5.3-flash",
  "messages": [{ "role": "user", "content": "is 7 prime?" }],
  "structured_outputs": { "grammar": "start: \"yes\" | \"no\"" },
  "max_tokens": 8
}
// → "content": "yes"
The older top-level 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 description strings; the model sees them.
  • Keep schemas shallow where you can. Deeply nested or huge schemas raise latency and the chance of hitting max_tokens mid-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.

If the answer is cut off by 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 content is constrained; reasoning is free-form. Parse content.
  • Not every JSON Schema keyword is honoured. Stick to type, properties, required, enum, items, additionalProperties and basic string/number bounds; exotic keywords may be ignored.
  • The response content is a JSON string. Always JSON.parse / json.loads it yourself.