rumblr Work in progressWIP

● The AI Primer · Lesson 17 · Part 1: how the model works inside

Structured output

answers that fit a shape, every time

You'll be able to explain Constrained decoding: grammars and JSON schemas that guarantee valid output

Members · open during launch 45 min17 figures and diagrams
Guide is what to use and when. How it works builds it from scratch. Math & code adds the formulas and the Python.

The lesson in one minute

What you'll be able to explain

  1. The problem: every token is a chance to break the format, so an answer of n tokens is valid about of the time. Prompting raises p but never to 1.
  2. Constrained decoding: before each draw, set the logit of every token that can't lead to a valid answer to minus infinity, then sample as usual. Allow the end token only when the answer is complete. Valid by construction, if the answer is allowed to finish.
  3. Patterns: compile to a finite-state machine; a token is allowed if the machine can read all its characters; precompute the allowed tokens for every state, so each step is a lookup.
  4. JSON Schema: nesting needs a stack (a pushdown automaton); the schema decides which keys, types and closers are legal at each character.
  5. Pitfalls: the mask can force a made-up value or bend the model's preferences; tokens don't align with grammar pieces; nested grammars cost more to check. Retrying is fine when the model is usually right. Always validate business rules afterwards.

Level 1

The practitioner's guide

In one sentence

Structured output means making a model's answer fit an exact shape (a JSON object, a label from a fixed list, a date) every time, so that a program can read it with no person in the loop.

When you need it

The moment something other than a person reads the answer: a tool call's arguments, a row for a database, a classification label, a form to fill in. A person shrugs off a stray word in front of the JSON; a parser rejects the whole answer. You don't need it for prose a person will read, and you don't need it when the reader is another model that copes with loose text. The tell: if you are writing code to strip "Sure, here you go!" from the front of responses, you need it.

How often does asking nicely fail? This lesson's toy model, asked 200 times for {"age": 42} with only the prompt to guide it, gets it exactly right 133 times (66.5%). Real models do far better than a toy, but not perfectly, and the failures grow with length: in an answer of 100 tokens where each token is right 98% of the time, the whole answer comes out valid only about 13% of the time (Level 2 shows why). At a million calls a day, a 1% failure rate is ten thousand broken answers a day.

Your options

Five ways, from the cheapest to the most certain:

Option What it does What it guarantees What it costs Where it lives
Prompting and examples Ask for the format and show an example or two Nothing; it raises the odds A few extra input tokens Your prompt
Post-processing Repair the common slips: strip chatter, close a bracket Nothing, but it catches the frequent cases A small parser you maintain Your code
Validate and retry Parse; on failure, ask again, quoting the error Valid eventually, if the model is usually right A whole extra call per retry, and latency Your code
Constrained decoding Forbid, at every token, anything that cannot lead to a valid answer A valid shape, by construction A grammar compiled once, a small check per token, some drift in what the model says The model server: JSON mode, strict schemas, grammar engines
Fine-tuning on the format Train the model on thousands of examples in the shape Far more reliable; still not certain Data, a training run, a model to host Training

How to choose

Start from what reads the answer and how often it may be wrong.

  • One field, a label from a list, a yes or no: prompt for it and validate. Retries are cheap because the answer is short.
  • A JSON object your code depends on, at volume: use the hosted API's strict schema, or a grammar engine in front of a model you run yourself. It removes the parsing code, the type checks and the retry loop in one move.
  • A format no engine supports (a custom mini-language, a legacy fixed-width record): post-process what you can, validate, retry, and consider fine-tuning once the volume justifies it.
  • Whatever you pick, validate the values afterwards. A schema proves shape, not truth: {"amount": 0, "currency": "USD"} fits a payment schema exactly and is still a bad payment.

What it costs

Prompting costs tokens. Retries cost whole calls and double the slowest requests. Constrained decoding costs a one-time compile of the schema (a noticeable pause on the first request, cached after that) and a small check per token; nested schemas cost more than flat ones. It can also cost quality: forcing a model off the path it wanted can make it invent a value to satisfy a required field, or wander, legally, until the token budget runs out. In this lesson's toy, a strict schema with a required age the model has no answer for produces "age": 9700. Fine-tuning costs the most up front and the least per call.

What breaks

  • A required field the model can't fill becomes a made-up value that parses. Make such fields optional, or allow null.
  • Valid JSON, wrong content. JSON mode alone guarantees something parseable, not your keys or your types. That needs a schema.
  • Truncation. A valid shape cut off by the output limit is invalid. Set the limit with the schema's size in mind.
  • Token boundaries. A token can straddle a boundary in the grammar, so real engines check character by character; a home-made masker that judges whole tokens rejects valid answers.
  • Drift. A constrained model can sound different, because the mask changes which continuations it is allowed.
  • Business rules. Shape is checked; meaning is not. Keep the validator.

In the wild

Hosted APIs expose the mechanism at three strengths: JSON mode (any valid JSON), strict structured outputs (your schema, guaranteed) and strict tool use (a tool's arguments must fit its input schema). Claude's structured outputs and strict tool use are one example, linked in Further reading. For models you run yourself, Outlines and XGrammar turn a JSON Schema or a regular expression into token masks, and llama.cpp accepts a grammar file (GBNF). Libraries such as instructor (validate and retry against a Python type) and guidance (constrain a generation to a pattern or a set of options) wrap these steps. Every agent framework leans on all of this: a tool call is a structured output.

Go deeper

Level 2 builds constrained decoding from nothing: why failures compound with length (a one-line formula), how a mask is applied before each token is drawn, how a pattern becomes a finite-state machine and a JSON Schema becomes a stack, and what each pitfall above looks like in numbers you can rerun. If you only needed to choose, you are done.

Level 2

How it works, from scratch

A language model writes one token at a time, and each token is a draw from a probability distribution (see primer.ml.inference). Most of the time you want free text. Sometimes a program is going to read the answer: a tool call's arguments, a row for a database, a label from a fixed list. Then the answer must have an exact shape, such as a JSON object with an integer field called age, and one stray character breaks it.

Structured output is the family of techniques that make the shape certain. The main one, constrained decoding, is surprisingly small: before each token is drawn, find every token that could not possibly continue a valid answer, and forbid it. This lesson builds that from scratch: first for simple patterns, with a finite-state machine, then for nested JSON, with a stack.

Chapter 1

Asking nicely is not enough

Everyday picture You read a form aloud over the phone to a friend and ask them to type it in exactly. They are careful and mostly right. But now and then they add "Sure, here you go!" before the form, or they forget the last bracket, or a finger slips. A person reading the result shrugs it off. A program reading it stops dead: one wrong character and the whole thing is rejected.

Tiny worked example This lesson's toy model, ToyModel, has mostly learned to answer {"age": 42}. It is a pretend model, a few lines of NumPy, that copies that answer with some noise on every choice and a habit of opening with "Sure". Asked 200 times, with nothing but the request to guide it, it writes the exact answer 133 times (66.5%). The other 67 look like this:

What it wrote What went wrong
Sure{"age": 42} a friendly word before the JSON (22 times)
{"age": 42w a slip where the closing brace should be
{"age": 428 an extra digit, then it stopped
{"age"6 42} a digit where the colon belongs
Sure{"age": 42}EUR chatter at both ends

Nothing here is a big mistake. Each one is a single bad token.

Figure 2 · Diagram

Reading it: follow the loop on the left: the model adds one token at a time and nothing checks the answer until it is finished. Only then does the parser look at it, and it has two exits. One bad token anywhere in the loop sends the whole answer to the error exit, however good the rest was.

That is why failures pile up with length. If each token is right with chance , and the chances are roughly independent, the whole answer is right only when every token is:

Level 3: the formula and its symbols

Symbols

Symbol Meaning here In the example
the chance that one token is right, between 0 and 1 0.98
how many tokens the answer has 10
multiplied by itself times: the chance that all are right
the chance that the whole answer parses 0.817

In words: "the chance that a whole answer is valid is the chance that one token is right, multiplied together once for every token."

With the numbers: a model that gets 98% of tokens right, writing a 10-token answer, produces valid output of the time: nearly one answer in five is broken. Make the answer 100 tokens long and it drops to .

Level 3: in Python
# p: chance one token is right; n: tokens in the answer
p, n = 0.98, 10
# p^n: all n tokens right
round(p ** n, 3)  # → 0.817
# a ten times longer answer
round(p ** 100, 3)  # → 0.133

Figure 1 · Drawn from the lesson's code

0 25 50 75 100 125 150 175 200 answer length n (tokens) 0.0 0.2 0.4 0.6 0.8 1.0 share of answers valid, pⁿ Every token is a chance to break the format 10 tokens at 98%: 0.82 99.9% per token 99.0% per token 98.0% per token 95.0% per token

Valid-output rate falls as the answer grows: at 98% per token, 10 tokens are valid 82% of the time and 100 tokens only 13%

Reading it: the x-axis is the length of the answer in tokens, the y-axis the share of answers that come out valid. Each curve is one per-token accuracy. Even the top curve, 99.9% per token, sags over a long answer, and the 95% curve is near zero by 100 tokens. The dot marks the worked example. Better prompting moves you to a higher curve, but no curve stays at 100%.

In code: ToyModel is the pretend model, sample_answer draws one answer token by token, and chance_all_valid is the formula.

Why it matters in practice. A program that calls a model a million times a day and fails 1% of the time fails ten thousand times a day. Long answers, nested objects and small models make it worse. Prompting and examples help, but they only move you to a better curve. To get to 100% you have to change how tokens are chosen.

Chapter 2

Constrained decoding: mask, then sample

Everyday picture Picture a keyboard whose keys lock and unlock as you type. You still decide what to write. But at every keystroke, any key that would make the text break the form is locked for that one keystroke. You cannot make the mistake, because the key isn't there.

Tiny worked example At the very first step the model scores four tokens (these scores are logits: raw preferences, before softmax turns them into probabilities; see primer.ml.attention for softmax from zero). Only { and [ can start a JSON value, so the other two are locked:

Token Logit Allowed? Share before the mask Share after the mask
Sure 2.0 0 0.579 0
Here 1.0 0 0.213 0
{ 0.5 1 0.129 0.622
[ 0.0 1 0.078 0.378

Before the mask the model put 79% of its belief on chatter. After the mask, the chatter has a chance of exactly zero, and the two allowed tokens share everything. They keep their odds against each other: { was 1.65 times as likely as [ before, and it still is.

Figure 5 · Diagram

Reading it: two arrows leave the answer so far. The top path is the ordinary model, which scores every token as it always does. The bottom path is the new part: a checker that knows the shape and says which tokens are still possible. They meet in the mask box, and from there it is ordinary sampling again. The end token is itself just a token, so it is allowed only when the answer is complete.

The mask as a formula is softmax with a 0-or-1 switch on every term:

Level 3: the formula and its symbols

Symbols

Symbol Meaning here In the example
the vocabulary: every token the model can write the 4 tokens in the table
how many tokens that is 4
the token whose probability we are computing 3, the {
token 's logit
the mask: 1 if token can continue a valid answer, 0 if not
raised to the logit: always positive
add up the following for every token
the probability that token is drawn 0.622

In words: "a token's probability is its usual softmax share, except that forbidden tokens count as zero on top and bottom, so the allowed tokens share all the probability between them."

With the numbers: .

Level 3: in Python
import math
# Sure, " Here", "{", "["
z = [2.0, 1.0, 0.5, 0.0]
# m_i: 1 if the token may come next
m = [0, 0, 1, 1]
# m_i e^(z_i): forbidden tokens contribute nothing
kept = [m_i * math.exp(z_i) for m_i, z_i in zip(m, z)]
[round(k, 3) for k in kept]  # → [0.0, 0.0, 1.649, 1.0]
# Σ_j m_j e^(z_j)
total = sum(kept)
round(total, 3)  # → 2.649
[round(k / total, 3) for k in kept]  # → [0.0, 0.0, 0.622, 0.378]
# without the mask, most of the belief went to chatter
e = [math.exp(z_i) for z_i in z]
[round(x / sum(e), 3) for x in e]  # → [0.579, 0.213, 0.129, 0.078]

Setting a logit to minus infinity does the same thing, because : it is the trick the causal mask uses in primer.ml.attention. After masking, temperature, top-k and top-p from primer.ml.inference work exactly as before, on the tokens that are left.

Figure 3 · Drawn from the lesson's code

'{' 'Sure' 'USD' 'y' 0.0 0.2 0.4 0.6 0.8 1.0 probability First token of {"age": 42}: the mask removes the chatter 0.78 1.00 0.19 0.00 0.00 0.00 0.00 0.00 what the model wanted after the mask

At the toy model's first step, 19% of its belief sits on Sure; after the mask, the brace gets all of it

Reading it: these are the toy model's real first-step probabilities for the {"age": 42} task, top four tokens only. Grey bars are what the model wanted; blue bars are what it may choose from after the mask. "Sure" had nearly a fifth of the belief and drops to zero. The brace, the only legal way to begin, takes everything.

Why 100% by construction. The checker keeps one promise: every answer so far can still be finished validly. It holds at the start (the empty answer can be finished). Each step only allows a token that keeps it. The end token is allowed only when the answer is already complete. So every answer that ends is valid. No luck is involved.

Figure 4 · Drawn from the lesson's code

age pattern unconstrained age pattern constrained person schema unconstrained person schema constrained 0.0 0.2 0.4 0.6 0.8 1.0 share of 200 answers Constrained decoding: every finished answer is valid 66.5% 100.0% 18.0% 97.5% valid chatter before the JSON broken inside cut off by the token budget

Unconstrained, the age answer is valid 66.5% of the time and the person object 18%; constrained, every finished answer is valid

Reading it: each bar is 200 answers from the toy model, split by what happened. Green is valid. The unconstrained bars show all three failures: chatter before the JSON, a broken character inside, and (rarely) running out of token budget. The person object is longer, so it breaks far more often, just as predicts. The constrained bars have no chatter and nothing broken. The one sliver left, 5 of the 200 person answers, is cut off: the 40-token budget ran out mid-object. The guarantee covers every prefix, but only finishing makes a whole answer, so leave room in the budget.

In code: masked_softmax applies the mask, sample_answer takes an optional constraint and masks every step, and validity_experiment produces the bars above.

Why it matters in practice. Constrained decoding changes nothing about the model: no retraining, same weights. It only changes which tokens may be drawn, which is why it can be added to any open model at serving time. The hard part is the checker: answering "which of 100,000 tokens could still lead to a valid answer?" fast, at every step. The next two sections build it.

Chapter 3

From a pattern to a state machine

Everyday picture A subway map. You stand at a station. Each line leaving it is labelled with one character. To write a character, you ride the line with that label; if no line from your station has it, that character is impossible here. Some stations are marked "you may stop here". That map is a finite-state machine: a fixed set of states (stations) and one move per character. A regular expression, the pattern language behind [0-9]+ and cat|car|dog, can always be drawn as one.

Tiny worked example The pattern cat|car|dog (one of three words) becomes this machine:

Figure 7 · Diagram

Reading it: start at S0 and read a word one character at a time. "cat" goes S0, S1, S3, S6, and S6 has an exit arrow, so "cat" is accepted. "cow" gets stuck at S1, which has no "o" line. After "ca" (state S3) only "r" and "t" are possible: the machine has turned "what may come next?" into "which lines leave this station?".

Tokens are not characters. A model writes tokens, and one token can hold several characters. The rule: a token is allowed if the machine can swallow all of its characters, one after another, without getting stuck. With a vocabulary of 13 tokens:

Token From S0 From S3 (after "ca")
c, d allowed stuck
ca, cat, do, dog allowed: every character has a line stuck
a, at, o, og, g stuck at the first character stuck
t, r stuck allowed

cat is allowed at the start even though no single line reads "cat": the machine rides c, then a, then t. at is part of a real word and still never allowed at the start, because S0 has no "a" line.

Level 3: the formula and its symbols

Symbols

Symbol Meaning here In the example
a state of the machine S0
the transition function: the state you reach from by reading character , or undefined if there is no such line
a token cat
the characters of , in order; is how many c, a, t;
read every character of in turn, starting from
the vocabulary the 13 tokens above
"the set of every token in for which ... holds"
the allowed tokens at state c, d, ca, cat, do, dog

The end token joins only when is an accepting state (S5, S6 or S7 here), because only there is the text complete.

In words: "to see whether a token fits, walk its characters through the machine one at a time; the allowed tokens are the ones that never get stuck."

With the numbers: , so cat is in . is undefined, so at is not.

In Python:

# the cat|car|dog machine: state -> {character: next state}
delta = {0: {"c": 1, "d": 2}, 1: {"a": 3}, 2: {"o": 4}, 3: {"r": 5, "t": 6}, 4: {"g": 7}, 5: {}, 6: {}, 7: {}}
accepting = {5, 6, 7}
def walk(s, token):
    # δ*: one δ per character; None means stuck
    for ch in token:
        s = delta[s].get(ch) if s is not None else None
    return s
walk(0, "cat")  # → 6
walk(0, "at")  # → None
V = ["c", "a", "t", "r", "d", "o", "g", "ca", "cat", "do", "dog", "at", "og"]
# A(0): every token the machine can swallow whole from the start
[t for t in V if walk(0, t) is not None]  # → ['c', 'd', 'ca', 'cat', 'do', 'dog']
# A(3), after "ca"
[t for t in V if walk(3, t) is not None]  # → ['t', 'r']
# the end token only where the text is complete
walk(0, "cat") in accepting  # → True

How a pattern becomes a machine. Two classic steps, both in the code:

Figure 8 · Diagram

Reading it: left to right, the pattern gets more mechanical. Thompson's construction reads the pattern like a sentence and builds a tiny machine for each piece: one line for a character, a fork for |, a loop for +. Glued together they make an NFA (a non-deterministic machine), which can be in several states at once: after "c" it is both "inside cat" and "inside car". The subset construction turns each set of NFA states into one state of a DFA (a deterministic machine), which is always in exactly one state, so following it is a dictionary lookup. The last box is the payoff: since the DFA has a fixed number of states, the allowed tokens can be worked out for every state before generation starts.

The pattern -?[0-9]+ (an optional minus sign, then digits) compiles to just three states: the start, "saw a minus", and "saw at least one digit", the only accepting one. The lesson's running example, \{"age": [0-9]+\}, compiles to 11.

Figure 6 · Drawn from the lesson's code

'{' '}' '"' ':' ' ' '0' '1' '2' '3' '4' '5' '6' '7' '8' '9' 'a' 'e' 'g' '"age"' 'age' '": ' ': ' '42' '17' '20' end 'Sure' 'x' token (every token allowed somewhere, plus two that never are) (start) { {" {"a {"ag {"age {"age" {"age": {"age": {"age": 4 {"age": 42} state, named by the text that reaches it The age pattern's mask table, computed once before generation

Each row is one state of the age machine, each column a token; only a handful of cells are lit, and the digit states allow many tokens at once

Reading it: rows are the 11 states, labelled by the text that reaches them; columns are tokens; a dark cell means "allowed here". Most rows have one or two dark cells: the structure is fixed, so only one next character is legal, written alone or as the start of a longer token such as "age" or ": . The two digit rows are where the model has real choice: any digit, a two-digit token such as 42, or, once one digit is down, the closing brace. The last row allows only the end token. This whole table is computed once, before the first token.

In code: compile_pattern runs both constructions and returns a DFA; DFA.walk follows text through it; allowed_tokens is , and mask_table precomputes it for every state. RegexConstraint plugs the table into sample_answer.

Why it matters in practice. Integers, dates, enums, phone numbers and fixed-key objects are all patterns. Reframing generation as moving between the states of a machine, with the allowed tokens indexed per state, is the idea of Willard and Louf (2023) behind the open-source Outlines library, and it makes each step's mask a single lookup.

Chapter 4

JSON Schema: nesting needs a stack

Everyday picture A stack of plates. Each time you open something, a bracket, a brace or a quote, you put a plate on the stack with a note: "an array is open", "an object is open". To close something, you may only take the top plate: close the most recent thing first. When the stack is empty, you're done.

A subway map can't do this job. JSON can nest as deep as you like: [[[[...]]]]. A machine with, say, 50 states can't tell 50 open brackets from 51, so it can't know how many closers it still owes. Counting without limit needs memory without limit. A finite-state machine plus a stack is called a pushdown automaton, and it is exactly enough for nested formats such as JSON, SQL and most programming languages.

Tiny worked example Read {"a": [1, {"b": one character at a time.

Figure 9 · Diagram

Reading it: each box is a moment in the reading, with the stack written bottom first. Every opener adds a frame on the right; every closer removes the rightmost one. At the third box the innermost open thing is an object, so } is the only closer allowed, and ] would be refused even though an array is open further down. The last box is empty: the value is complete, and only now is the end token allowed.

The checker answers one question about any prefix: can it still be finished?

Prefix Status Why
{"a": [1, {"b": open a value for "b" may come next
{"a": [1} dead the array is on top, so } can't close it
{"a": [1, {"b": 2}]} complete the stack is empty

The schema steers every character. A JSON Schema (see primer.agents.tools) names the fields and their types. This lesson's person schema allows name (a string), age (an integer) and pets (an array of "cat" or "dog"); name and age are required and nothing else is allowed. The checker enforces each rule at the first character that breaks it:

Prefix Status The rule that decides
{"na open "na" can still become "name"
{"nx dead no field starts with "nx"
{"name": "Ada"} dead age is required, so the object may not close yet
{"age": 3. dead an integer has no decimal point
{"age": 01 dead JSON numbers never start with a zero followed by digits
{"pets": ["x dead only "cat" and "dog" are listed
{"name": "Ada", "age": 36} complete every required field is present

Inside one frame, a small state machine does the work. Here is an object's:

Figure 10 · Diagram

Reading it: these are the phases one object frame moves through, from opening brace to closing brace. The labels carry the schema's rules: a key letter is allowed only if it still spells a field not yet used, and the closing brace only if every required field is in. The value arrow is where nesting happens: the value gets its own frame pushed on top of the stack, and this frame waits in "value" until that frame is popped.

The leaves of JSON (numbers, true, false, null) are regular patterns, so the checker reads numbers with the machines from section 3. Real engines split the work the same way: patterns for the small pieces, a stack for the nesting.

One rule here is deliberate: at most one space after : and ,. JSON allows any amount of whitespace, and a constrained model that has lost its way can pad with spaces until the budget runs out. Real engines limit whitespace for the same reason.

In code: SchemaChecker.step reads one character and returns the new stack (a tuple of Frame entries), or nothing when the prefix is dead; SchemaChecker.status answers open, dead or complete for a prefix; SchemaChecker.open_containers shows the stack; SchemaConstraint tries every token against the stack at each step.

Why it matters in practice. This is how a schema becomes a guarantee: compile the schema into grammar rules, run them as a pushdown automaton, and mask every token that would kill it. Geng et al. (2023) showed the approach works for structured tasks without any fine-tuning, and PICARD (Scholak et al., 2021) did the same for SQL by parsing incrementally. The stack has a cost: it can grow without limit, so the masks can't all be tabled in advance as they were for a pattern. Section 5 comes back to that.

Chapter 5

Costs and pitfalls

Constrained decoding guarantees the shape. It does not guarantee a good answer, and it isn't free.

The mask changes what the model says

Everyday picture A satellite navigation system that only forbids illegal turns, one junction at a time, and never looks ahead. It happily takes the motorway because the motorway looks fastest right now, and only at the end discovers that the one legal exit is a long detour. A driver who could see the whole map would have taken the side road from the start.

Tiny worked example 1: forced to invent. The model is asked for a person's age, but the text never says it. The model's honest belief for the next token:

Token Model's belief Allowed by "type": "integer"? After the mask
null 0.80 no 0
3 0.12 yes 0.12 / 0.20 = 0.60
5 0.08 yes 0.08 / 0.20 = 0.40

The mask throws away 80% of the model's belief, and the answer is a made-up age, delivered as confidently as a real one. The toy model does this too: told to write the person schema while it "wants" to write only a name, it is forced to add an age, and invents numbers such as 9700. The fix is in the schema, not the decoder: allow null ("type": ["integer", "null"]), or add a field for "not stated". A related finding (Tam et al., 2024): forcing a strict format from the first token can hurt a model's reasoning, so let the model reason in free text first, or put a reasoning field before the answer field.

Tiny worked example 2: locally tempting, globally wrong. A two-token model. Valid answers must end in "y".

Figure 14 · Diagram

Reading it: each arrow is one token with the model's probability for it, and each leaf is a whole answer with the product of the probabilities along its path. Of the two valid answers, the model much prefers By (0.09 against 0.009). But masking decides one token at a time. At the first step both A and B can still end in "y", so nothing is masked, and A wins 90% of the time. At the second step the mask forces "y". The result: Ay, the answer the model itself thought ten times less likely, comes out 90% of the time.
Level 3: the formula and its symbols

Symbols

Symbol Meaning here In the example
one whole answer Ay
its -th token; is every token before it y, A
the number of tokens in the answer 2
the model's own chance of writing : its token chances multiplied together
the set of valid answers (the "language" the grammar allows) {Ay, By}
add up over every valid answer
"the chance of given that the answer is valid": the model's own odds, among valid answers only 0.091
the masked, renormalised chance of token at that step
multiply together over every step
the chance that token-by-token masking produces 0.9

In words: "what the model believes, restricted to valid answers, is each valid answer's probability divided by the total of all valid ones; what masking actually produces is the product of the step-by-step masked probabilities, and the two need not agree."

With the numbers: , but : ten times the model's own preference.

Level 3: in Python
first = {"A": 0.9, "B": 0.1}
second = {"A": {"x": 0.99, "y": 0.01}, "B": {"x": 0.1, "y": 0.9}}
# P(y) for each valid answer: the model's token chances multiplied
P = {a + "y": first[a] * second[a]["y"] for a in first}
{k: round(v, 3) for k, v in P.items()}  # → {'Ay': 0.009, 'By': 0.09}
# P(y | valid): divide by the total over valid answers
total = sum(P.values())
{k: round(v / total, 3) for k, v in P.items()}  # → {'Ay': 0.091, 'By': 0.909}
# P_mask: step 1 keeps both, step 2 renormalises "y" to 1
{a + "y": first[a] * (second[a]["y"] / second[a]["y"]) for a in first}  # → {'Ay': 0.9, 'By': 0.1}

Figure 11 · Drawn from the lesson's code

Ay By 0.0 0.2 0.4 0.6 0.8 1.0 1.2 probability Valid, but not what the model preferred 0.09 0.90 0.91 0.10 model's own odds among valid answers what token-by-token masking produces

The model's own odds among valid answers favour By 91 to 9; token-by-token masking produces Ay 90% of the time

Reading it: two answers, two bars each. Grey is the model's own preference among valid answers; blue is what masked decoding actually produces. The bars point in opposite directions. Nothing invalid comes out, yet the distribution is badly bent. Park et al. (2024) name this problem and propose a correction; in practice it is milder when the model already writes the format well, because then the mask rarely has to overrule it.

The toy model shows the everyday version: once noise knocks it off its answer, the mask keeps it legal but not sensible, and it writes digits until the closing brace happens to win, as in {"age": 4174209}.

In code: forced_choice renormalises a belief over the allowed tokens and reports the share thrown away; NULLABLE_AGE_SCHEMA is the fix; distortion_example computes both distributions above.

Token boundaries

Everyday picture You can say "forty-two", or spell it "four, two". Both arrive at the same text, but only one is how you would naturally say it.

Tiny worked example The toy vocabulary has a 42 token and single digits, so "42" can be written two ways: 42, or 4 then 2. The whole answer {"age": 42} can be spelled 16 ways, and the person answer 1,536 ways. The mask allows every one of them, but a trained model has almost only ever seen the first, its tokenizer's usual spelling (see primer.ml.tokenization). When the mask forces it onto an unusual spelling, it is in unfamiliar territory and its next choices get worse.

Figure 15 · Diagram

Reading it: two paths lead from the same place to the same place and write the same text. The top path is the one the model saw thousands of times in training; the bottom one it rarely saw. The checker walks characters, so it cannot tell them apart; only the model can.

Two more boundary effects follow from the same fact. A single token can cross several structural boundaries at once, such as "} closing a string and an object together; walking the token character by character, as allowed_tokens does, handles that. And if the prompt ends in the middle of what would normally be one token (a prompt ending in {"age": when the model would usually write ": as one token), the natural token is no longer available. Some engines back up one token and let the model rewrite it, which is called token healing.

In code: tokenizations lists every way a vocabulary can spell a text.

The speed of computing masks

Everyday picture Before every keystroke, a proofreader checks every word in the dictionary against the rules: slow. Or: a card for every station, printed once, listing which words fit there: fast, once the cards exist.

Tiny worked example A real vocabulary has around 128,000 tokens. Say they average 4 characters, the answer is 200 tokens long, and the pattern's machine has 50 states. Checking every token at every step walks 200 × 128,000 × 4 = 102.4 million characters for one answer. Building the table walks 50 × 128,000 × 4 = 25.6 million characters once; after that, each step only reads one row of 128,000 yes-or-no entries.

Level 3: the formula and its symbols

Symbols

Symbol Meaning here In the example
work: characters walked or table entries read
tokens generated 200
vocabulary size 128,000
the average token length in characters (the bar means "average") 4
states in the machine 50
multiply

In words: "the naive way walks every token at every step; the table walks every token once per state, up front, then reads one row per step."

With the numbers: for every answer. for the first answer, and only the second term, 25.6 million cheap reads, for each answer after that.

Level 3: in Python
n, V, L_bar, S = 200, 128_000, 4, 50
# W_naive = n · |V| · L̄
n * V * L_bar  # → 102400000
# W_table = S · |V| · L̄ + n · |V|
S * V * L_bar + n * V  # → 51200000
# ten answers: the table's build cost is paid once
10 * n * V * L_bar, S * V * L_bar + 10 * n * V  # → (1024000000, 281600000)

Figure 12 · Drawn from the lesson's code

0 2 4 6 8 10 answers generated with one schema 0 200 400 600 800 1000 work (millions of steps) |V| = 128,000, 200 tokens per answer, 50 states check every token at every step build a table once, read a row per step

Checking every token at every step costs the same for every answer; the table pays once, then grows slowly

Reading it: the x-axis counts answers generated with one schema, the y-axis the total work so far. The naive line climbs steeply and steadily. The table line starts above zero (the build) and then climbs gently (one row read per step). They cross partway through the very first answer. That is why hosted APIs compile a schema once and cache it: the first request with a new schema is slower, and the ones after are fast.

For a schema with nesting, the stack can grow without limit, so no table can cover every situation. XGrammar (Dong et al., 2024) splits the vocabulary: most tokens are context-independent (whether they fit depends only on the current grammar position, not on what is deeper in the stack), and those are prechecked into tables; only the few context-dependent ones are checked against the stack at run time.

In code: mask_cost is the formula; mask_table builds the table once and RegexConstraint reads a row per step, while SchemaConstraint walks every token against the stack at every step, the naive way.

When validating and retrying is enough

Everyday picture Instead of a form that can't be filled in wrong, you let people fill in a blank sheet, check it, and hand it back with a note when it is wrong. Fine if most people get it right first time; miserable if most don't.

Tiny worked example The toy model writes a valid age answer 66.5% of the time. Retrying until it succeeds takes 1 / 0.665 = 1.50 attempts on average, and three tries in a row all fail only 0.335³ = 3.8% of the time. For the longer person object, valid 18% of the time, it takes 5.56 attempts on average: slow and expensive.

Figure 16 · Diagram

Reading it: the happy path goes straight through. Every failure costs a full extra model call, round the loop. Sending back the validator's exact message (as primer.agents.tools.validate produces) makes the second try much more likely to succeed. The dotted exit is the budget: a loop needs a limit.
Level 3: the formula and its symbols

Symbols

Symbol Meaning here In the example
the chance one try is valid 0.665
the expected value: the long-run average over many repeats
the average number of tries to the first success 1 / 0.665 = 1.50
a number of tries 3
the chance one try fails 0.335
the chance that independent tries all fail

In words: "if each try succeeds with chance p, you need one over p tries on average, and the chance that k tries all fail is the chance of one failure, multiplied together k times."

With the numbers: ; ; for the person object, .

Level 3: in Python
# the toy model's unconstrained rate on the age answer
p = 0.665
# E[attempts] = 1 / p
round(1 / p, 2)  # → 1.5
# (1 - p)^k: three tries, all invalid
round((1 - p) ** 3, 4)  # → 0.0376
# the longer person answer, valid 18% of the time
round(1 / 0.18, 2)  # → 5.56

Figure 13 · Drawn from the lesson's code

0.2 0.4 0.6 0.8 1.0 chance a single try is valid, p 0 2 4 6 8 10 12 average tries until valid, 1/p Retrying is cheap only when the model is usually right 95% valid: 1.05 toy age answer: 1.50 toy person answer: 5.56

Expected attempts are close to 1 for a model that is usually right, and shoot up as the valid rate falls

Reading it: the x-axis is the chance a single try is valid; the y-axis is the average number of tries until one is. The curve is flat on the right and steep on the left. The dots mark three cases: a model that is 95% right barely notices retries (1.05 tries); the toy age answer needs half a try extra; the toy person answer needs more than five calls.

Validate and retry when: you can't change the decoder (a hosted model without a structured-output option), the model is already right nearly every time, or the rule can't be expressed as a grammar. Constrain when answers are long or nested, the model is small, or latency matters. Either way, keep the validator: a grammar enforces shape, not rules such as minimum, and never truth.

In code: expected_attempts and chance_still_failing are the two formulas.

Chapter 6

JSON mode, strict schemas and tool calls

Everyday picture Two paper forms. One only insists that you write in block capitals: whatever you write is readable, but nothing says what goes where. The other has a labelled box for each field, and tick boxes where only certain answers are allowed. The first is JSON mode; the second is a strict schema.

Tiny worked example Give the toy model a reference answer that leaves out the age, {"name": "Ada"}, and ask 50 times. With JSON mode (the empty schema {}, which allows any JSON value) every finished answer parses: 44 copies of {"name": "Ada"} with the required age missing, one {"name": 42628} whose name is a number, and one bare false. All valid JSON; not one valid person. (The other 3 ran out of budget.) With the person schema, every one of the 37 answers that finish has an age, because the closing brace stays locked until one is written. Since the model had no age to give, it invents one, such as {"name": "Ada","age": 9700,"pets": []}: section 5's warning in action. The other 13 show a second cost of forcing a model off its path: it loses its way and wanders, legally, until the token budget runs out.

A tool call is the same thing with a name attached: the model's arguments are a JSON object that must fit the tool's input_schema (see primer.agents.tools and primer.agents.llm).

Figure 17 · Diagram

Reading it: the grammar engine lives inside the model server, next to sampling. It compiles the schema once (the slow first request from section 5) and then hands a mask to every step of the loop. What reaches your code is guaranteed to have the right shape. The last arrow is the part no grammar covers: whether the values are true and allowed. The payment tool in primer.agents.tools makes this concrete: {"amount": 0, "currency": "USD"} fits the schema's shape exactly, and primer.agents.tools.validate still rejects it, because "minimum": 0.01 is a rule about the value, not the shape.

In code: SchemaConstraint with the schema {} is JSON mode, and with PERSON_SCHEMA it is a strict schema; primer.agents.tools.ToolRegistry.definitions shows how a strict tool definition is sent.

Why it matters in practice. You now have three tools and know what each buys. JSON mode guarantees something parseable. A strict schema guarantees the shape your code expects, so the parsing and type-checking code disappears. Validation after the fact still catches what only your program knows. Hosted APIs (for example Claude's structured outputs and strict tool use) and open engines (Outlines, llama.cpp grammars, XGrammar) all run the mechanism built in this lesson: a checker that masks the logits before every draw.

Test yourself

9 questions

Answer each one out loud or on paper before you open it. If you can explain it, you know it.

Question 1Why does asking a model for JSON in the prompt fail some of the time, and why do longer outputs fail more?Think it through, then reveal

Each token is a separate draw with some small chance of being wrong, and a single wrong token breaks the parse. The chance that all n tokens are right is about , which shrinks as n grows: 98% per token gives 82% at 10 tokens and 13% at 100.

Question 2What exactly does constrained decoding change in the model?Think it through, then reveal

Nothing in the weights. At each step it sets the logits of forbidden tokens to minus infinity (probability zero) before sampling. The allowed tokens keep their relative odds, and temperature and top-p still apply to them.

Question 3Why is the output valid "by construction", and what can still go wrong with the shape?Think it through, then reveal

Every prefix is kept completable, and the end token is allowed only when the answer is complete, so any answer that ends is valid. It can still be cut off by the token budget, leaving a valid but unfinished prefix.

Question 4How do you decide whether a multi-character token is allowed in a given state?Think it through, then reveal

Walk its characters through the state machine one at a time from the current state. It is allowed if the walk never gets stuck. cat is allowed at the start of cat|car|dog; at is not, because the start has no "a" line.

Question 5Why can't a finite-state machine check arbitrary JSON?Think it through, then reveal

JSON nests without limit, and closing correctly requires remembering every open bracket in order. A machine with a fixed number of states can't count without limit. A stack, which a pushdown automaton adds, can.

Question 6Why can masks be precomputed for a pattern but not fully for a JSON Schema?Think it through, then reveal

A pattern's machine has a fixed number of states, so the allowed tokens for each can be tabled once. With nesting the stack can take unboundedly many forms. Engines precompute the tokens whose fate depends only on the current position and check the rest against the stack at run time.

Question 7How can constrained decoding make answers worse?Think it through, then reveal

It forces the model's choices into the allowed set even when the model believed something else: an integer field makes it invent a number when the honest answer was null, and token-by-token masking can commit early to a path the model thought unlikely overall. Allowing null, or letting the model reason before the structured part, helps.

Question 8When is validating and retrying good enough?Think it through, then reveal

When the model is valid almost every time (95% needs about 1.05 calls on average), when you can't change the decoder, or when the rule can't be written as a grammar. It gets expensive fast as the valid rate falls: 1/p calls on average.

Question 9What does a strict schema guarantee about tool arguments, and what doesn't it?Think it through, then reveal

It guarantees the shape: field names, types, required fields, enum values. It does not guarantee the values are true or allowed: a customer ID can be well formed and not exist, and an amount can fit the type while breaking a minimum. Your code still validates business rules.

Primary sources

The papers behind this lesson

Willard and Louf, Efficient Guided Generation for Large Language Models (2023)

Recast constrained generation as moving between the states of a finite-state machine, with the allowed tokens indexed per state in advance, the design behind Outlines.

The paper ↗
Geng, Josifoski, Peyrard and West, Grammar-Constrained Decoding for Structured NLP Tasks without Finetuning (2023)

Showed that constraining decoding with a formal grammar lets an off-the-shelf model produce complex structured outputs reliably, with no task-specific training.

The paper ↗
Scholak, Schucher and Bahdanau, PICARD: Parsing Incrementally for Constrained Auto-Regressive Decoding from Language Models (2021)

Rejected tokens that an incremental parser could not accept, making generated SQL valid as it was written.

The paper ↗
Dong et al., XGrammar: Flexible and Efficient Structured Generation Engine for Large Language Models (2024)

Made grammar-constrained decoding fast by prechecking context-independent tokens and checking only the context-dependent ones against the stack.

The paper ↗
Park et al., Grammar-Aligned Decoding (2024)

Showed that token-by-token masking distorts the model's distribution over valid outputs, and proposed a way to sample closer to the model's own conditional odds.

The paper ↗
Tam et al., Let Me Speak Freely? A Study on the Impact of Format Restrictions on Performance of Large Language Models (2024)

Measured how strict output formats can lower a model's reasoning performance.

The paper ↗

Researcher's shelf

Further reading

  • Russ Cox, Regular Expression Matching Can Be Simple And Fast (Thompson's construction, explained): https://swtch.com/~rsc/regexp/regexp1.html
  • Understanding JSON Schema: https://json-schema.org/understanding-json-schema
  • RFC 8259, The JavaScript Object Notation (JSON) Data Interchange Format: https://www.rfc-editor.org/rfc/rfc8259
  • Claude structured outputs (JSON outputs and strict tool use): https://platform.claude.com/docs/en/build-with-claude/structured-outputs
  • llama.cpp, GBNF Guide (grammars for local models): https://github.com/ggml-org/llama.cpp/blob/master/grammars/README.md
  • Outlines, structured generation library: https://github.com/dottxt-ai/outlines
  • Willard and Louf (2023): https://arxiv.org/abs/2307.09702
  • Dong et al., XGrammar (2024): https://arxiv.org/abs/2411.15100
  • Park et al., Grammar-Aligned Decoding (2024): https://arxiv.org/abs/2405.21047

About this lesson. This is the illustrated edition of a lesson from the open-source AI Primer. Its text, figures and numbers are generated from the Primer's source at commit 048aeaa, so the two always agree: the explanation, the code that builds it and the tests that prove it.