Capabilities

Structured outputs

When you need a machine-readable answer rather than prose — extracting fields, classifying, building an API response — structured outputs make the model return valid JSON, so there's no fragile string-parsing on your end. Pendra enforces this while the reply is being generated rather than by asking the model nicely in the prompt, so the shape you ask for is the shape you get.

The one thing that can still leave you with JSON that doesn't parse is running out of room: if the reply hits max_tokens (or the end of the model's context window) before the closing brace, you get the fragment that was produced so far. Pendra tells you when that happens — see Truncated replies below — so you never have to infer it from a parse error.

JSON mode

Set response_format to { "type": "json_object" } and the reply is syntactically valid JSON. Use this when you want JSON but don't need a specific shape — describe the fields you want in your prompt.

JSON Schema

For a guaranteed shape, pass { "type": "json_schema", "json_schema": { ... } }. The model is constrained to produce output matching your schema — the right keys, the right types — so you can deserialise straight into your own types.

Exact conformance with strict

Add "strict": true next to your schema (as in the example below) and the reply matches it exactly: every field you list in required is present, each key appears once, and nothing outside the schema can appear. Nested objects, arrays and enum values are held to the same standard, so the reply deserialises into a typed struct or model without a validation step of your own.

Without strict, the shape is best-effort: keys and value types come from your schema, but an optional field the model skips — or a required one it forgets — won't be corrected for you. Reach for strict whenever you're parsing the result into a fixed type; leave it off when you'd rather the model answer with whichever fields it actually knows.

Enforced exactly: the object's shape and key set, which fields are required, value types, enum and const values, array item types and lengths (minItems / maxItems), and schemas that reference their own definitions — so a schema generated from a Pydantic model or a Zod object works as you'd expect.

Some things can't be enforced: a union (anyOf, oneOf, allOf — including an optional or nullable field), a $ref to another document, and value constraints like pattern, format, minLength or minimum. Setting additionalProperties to a schema (rather than false) is also not honoured — the reply is held to exactly the keys you listed, so it still matches your schema, but the extra keys you allowed won't appear. In particular, nothing in the schema bounds how long a value can run — a number with no ceiling is the usual way a reply ends up truncated, so give it a sensible max_tokens and check for the notice below. The rest of the schema is enforced in every case, and because you asked for exact conformance and didn't get all of it, the reply tells you so:

Response
{
  "choices": [ ... ],
  "pendra": {
    "notice": {
      "code": "strict_schema_not_enforced",
      "message": "Part of this schema could not be enforced, so the reply may not match it exactly ..."
    }
  }
}

The pendra field is an addition of ours, so OpenAI-compatible clients ignore it — check for it when you want to know whether the guarantee held. When streaming, it arrives as its own event near the end of the stream.

If response_format is malformed — a type other than text, json_object, or json_schema, or { "type": "json_schema" } without its json_schema object — the request is rejected with a 422 before it runs, so a bad constraint fails fast instead of quietly returning unconstrained text.

Truncated replies

A schema constrains the shape of the reply, not how long the model takes to fill it in. If generation stops because it reached max_tokens — or ran to the end of the context window — before the JSON was finished, you get finish_reason: "length" and a fragment that won't parse. Rather than leave a parse error as your only clue, the reply says so directly:

Response
{
  "choices": [{ "index": 0, "message": { ... }, "finish_reason": "length" }],
  "pendra": {
    "notice": {
      "code": "truncated_during_structured_output",
      "message": "Response truncated before the JSON was complete, so it will not parse ..."
    }
  }
}

Check for it and retry with a larger max_tokens, or a shorter prompt if the context window is what ran out. A schema whose fields have no natural end — an integer with no ceiling, a free-text field you asked the model to be thorough in — is the common cause, so it's worth budgeting generously for those.

from pendra import Pendra

client = Pendra()

response = client.chat.completions.create(
    model="qwen3.6:27b",
    messages=[{"role": "user", "content": "Extract the name and age from: Sara is 34."}],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "person",
            "strict": True,
            "schema": {
                "type": "object",
                "properties": {
                    "name": {"type": "string"},
                    "age": {"type": "integer"},
                },
                "required": ["name", "age"],
                "additionalProperties": False,
            },
        },
    },
)
print(response.choices[0].message.content)  # {"name": "Sara", "age": 34}
Response
{ "name": "Sara", "age": 34 }
Structured outputs are a parameter of the chat endpoint. See the Chat completions API reference for the full field list.