đ Constrained Decoding â Guaranteeing Valid JSON Output¶
What this doc covers: The token-by-token generation loop, how to track JSON state at each step, how to use the vocabulary file to identify valid tokens, and how to enforce schema types (number, string, boolean). This is the core algorithm of the project.
Table of Contents¶
- Why Constrained Decoding Exists
- The Generation Loop
- The Vocabulary File
- Tracking JSON State
- Valid Tokens at Each JSON Position
- Enforcing Schema Types
- The -inf Trick
- Complete Walkthrough
- Putting It All Together
- Common Mistakes
Why Constrained Decoding Exists¶
At each generation step, the model outputs a probability score (logit) for every token in its vocabulary â roughly 151,643 tokens for Qwen3. Normally, you just pick the highest one.
The problem: nothing stops the model from picking a token that breaks your JSON.
Partial output so far: '{"name": "'
Model's top candidates:
"fn_add_numbers" logit: 3.2 â
valid function name
"fn_greet" logit: 2.9 â
valid function name
"The" logit: 1.4 â breaks JSON â not a function name
"42" logit: 0.8 â breaks schema â name must be a string
" " logit: 0.3 â breaks JSON â unexpected whitespace here
Without intervention, the model might pick "The" and produce broken output. With constrained decoding, you set all invalid token logits to -inf before selection, so the model can only ever pick a valid token.
After constraint:
"fn_add_numbers" logit: 3.2 â
stays
"fn_greet" logit: 2.9 â
stays
"The" logit: -inf â eliminated
"42" logit: -inf â eliminated
" " logit: -inf â eliminated
Selected token: "fn_add_numbers" â correct, guaranteed
Result: 100% valid, schema-compliant JSON every time â regardless of model size.
The Generation Loop¶
This is the fundamental loop you implement. Everything else in the project plugs into it.
import math
import json
from typing import List
def generate_constrained(
model,
prompt: str,
fn_def: FunctionDefinition,
all_fn_names: List[str],
str_to_id: dict[str, int], # token_string â token_id (real vocab format)
id_to_str: dict[int, str], # token_id â token_string (reverse lookup)
max_tokens: int = 200
) -> dict:
"""Generate a constrained function call JSON for a given prompt."""
# Step 1: tokenise the prompt â encode() returns a 2D tensor, use [0].tolist()
input_ids: List[int] = model.encode(prompt)[0].tolist()
generated_ids: List[int] = []
partial_json: str = ""
for _ in range(max_tokens):
# Step 2: get logits for all vocabulary tokens
all_ids = input_ids + generated_ids
logits: List[float] = list(model.get_logits_from_input_ids(all_ids))
# Step 3: determine valid tokens at this position
valid_ids = get_valid_token_ids(partial_json, fn_def, all_fn_names, str_to_id)
# Step 4: mask all invalid tokens
for i in range(len(logits)):
if i not in valid_ids:
logits[i] = -math.inf
# Step 5: pick the highest-scoring valid token
next_id = int(max(range(len(logits)), key=lambda i: logits[i]))
# Step 6: append and update state
generated_ids.append(next_id)
token_str = id_to_str[next_id] # use the reverse lookup: id â string
partial_json += token_str
# Step 7: check if generation is complete
if is_complete(partial_json, fn_def):
break
return json.loads(partial_json)
The key function is get_valid_token_ids â it answers the question: given what we've generated so far, which tokens are legal next?
The Vocabulary File¶
model.get_path_to_vocab_file() returns the path to a JSON file. The real format maps token strings to integer IDs:
import json
vocab_path = model.get_path_to_vocab_file()
with open(vocab_path, "r", encoding="utf-8") as f:
str_to_id: dict[str, int] = json.load(f)
# Real structure:
# {"!": 0, '"': 1, "#": 2, ..., "{": 5476, "}": 9313, ...}
# key = token string
# value = integer ID
# Build the reverse lookup for use after token selection
id_to_str: dict[int, str] = {v: k for k, v in str_to_id.items()}
â ī¸ The key is the token string, the value is the ID â not the other way around. Always build the reverse
id_to_strlookup so you can go in both directions.
What to look for in the vocabulary¶
You need to find the token IDs for specific characters and strings. Build these lookup sets once at startup, not inside the generation loop:
def build_vocab_lookups(str_to_id: dict[str, int]) -> dict:
"""Pre-compute useful token sets from the vocabulary."""
# Normalize helper â handles Ä (leading space) and other markers
def norm(s: str) -> str:
return s.replace("Ä ", " ").replace("Ä", "\n")
# Single structural characters â find all variants (with/without leading space)
open_brace = {tid for s, tid in str_to_id.items() if norm(s) == "{"}
close_brace = {tid for s, tid in str_to_id.items() if norm(s) == "}"}
open_quote = {tid for s, tid in str_to_id.items() if norm(s) == '"'}
colon = {tid for s, tid in str_to_id.items() if norm(s).strip() == ":"}
comma = {tid for s, tid in str_to_id.items() if norm(s).strip() == ","}
# Numeric tokens â digits, decimal point, minus, scientific notation
numeric = {tid for s, tid in str_to_id.items()
if norm(s) and all(c in "0123456789.-+eE" for c in norm(s))}
# Whitespace tokens
whitespace = {tid for s, tid in str_to_id.items()
if norm(s).strip() == "" and norm(s) != ""}
return {
"{": open_brace,
"}": close_brace,
'"': open_quote,
":": colon,
",": comma,
"numeric": numeric,
"whitespace": whitespace,
}
The leading-space problem¶
Tokenizers attach spaces to the beginning of the following token, not the end of the previous one. A space before " is part of the " token, not a separate space token.
Text: '{"name": "fn_greet"}'
Tokens: ['{', '"', 'name', '"', ':', 'Ä "', 'fn', '_greet', '"', '}']
â
space is part of this token: 'Ä "'
This matters when you search the vocabulary for :. There may be tokens for : and Ä : (with leading space). You need to account for all variants:
colon_variants = {tid for s, tid in str_to_id.items() if s.replace("Ä ", " ").strip() == ":"}
Tracking JSON State¶
To know which tokens are valid at each position, you need to know where you are in the JSON structure. The simplest approach is a state machine.
The JSON structure you're generating¶
{
"name": "<function_name>",
"parameters": {
"<param1>": <value1>,
"<param2>": <value2>
}
}
States¶
from enum import Enum, auto
class JSONState(Enum):
START = auto() # haven't written anything yet â expect '{'
AFTER_OPEN_BRACE = auto() # wrote '{' â expect '"name"'
IN_NAME_KEY = auto() # writing the literal key "name"
AFTER_NAME_KEY = auto() # wrote '"name"' â expect ':'
IN_NAME_VALUE = auto() # writing the function name string
AFTER_NAME_VALUE = auto() # wrote function name â expect ','
IN_PARAMS_KEY = auto() # writing '"parameters"'
AFTER_PARAMS_KEY = auto() # wrote '"parameters"' â expect ':'
IN_PARAMS_OPEN = auto() # wrote ':' â expect '{'
IN_ARG_KEY = auto() # writing an argument name key
AFTER_ARG_KEY = auto() # wrote key â expect ':'
IN_ARG_VALUE_NUM = auto() # writing a numeric argument value
IN_ARG_VALUE_STR = auto() # writing a string argument value
IN_ARG_VALUE_BOOL = auto() # writing a boolean argument value
AFTER_ARG_VALUE = auto() # wrote value â expect ',' or '}'
COMPLETE = auto() # wrote final '}' â done
Parsing current state from partial output¶
Rather than tracking state with a variable, you can infer it from what's been generated so far. This is more robust and easier to debug:
def get_current_state(partial: str, fn_def: FunctionDefinition) -> JSONState:
"""Determine current JSON generation state from partial output."""
partial = partial.strip()
if partial == "":
return JSONState.START
if partial == "{":
return JSONState.AFTER_OPEN_BRACE
if not partial.startswith('{"name"'):
return JSONState.IN_NAME_KEY
if ',' not in partial:
if partial.count('"') < 4:
return JSONState.IN_NAME_VALUE
return JSONState.AFTER_NAME_VALUE
if '"parameters"' not in partial:
return JSONState.IN_PARAMS_KEY
# We're inside the parameters object â expand this logic for your implementation
return JSONState.IN_ARG_KEY
Tip: For reliability, track state as a variable that you update token by token, rather than re-parsing the full partial string each time. The walkthrough section below shows this approach.
Valid Tokens at Each JSON Position¶
Here's what tokens are valid at each stage of generating the output JSON:
Stage 1: Start â {¶
# Only the open brace is valid
valid = {tid for s, tid in str_to_id.items() if s.replace("Ä ", " ").strip() == "{"}
Stage 2: After { â the key "name"¶
# Next tokens must spell out exactly '"name"'
already_written = len(partial) - 1 # chars after '{'
target = '"name"'
remaining = target[already_written:]
valid = {tid for s, tid in str_to_id.items() if remaining.startswith(s.replace("Ä ", " "))}
Stage 3: Inside function name value â restricted to valid function names¶
# partial ends with '{"name": "'
# next tokens must continue one of the valid function names
written_so_far = extract_name_value(partial) # e.g. "" or "fn_" or "fn_greet"
valid = set()
for fn_name in all_function_names:
if fn_name.startswith(written_so_far):
remaining = fn_name[len(written_so_far):]
for s, tid in str_to_id.items():
norm = s.replace("Ä ", " ")
if remaining.startswith(norm) and norm:
valid.add(tid)
Stage 4: Argument value â depends on schema type¶
param_type = current_fn.parameters[current_param].type
if param_type == "number":
valid = get_numeric_tokens(str_to_id, partial_value)
elif param_type == "string":
valid = get_string_tokens(str_to_id, in_string, partial_value)
elif param_type == "boolean":
valid = get_boolean_tokens(str_to_id, partial_value)
Enforcing Schema Types¶
This is where you use fn_def.parameters[param_name].type to decide which tokens are valid inside the argument value.
type: "number"¶
def get_valid_number_tokens(
str_to_id: dict[str, int],
partial_value: str
) -> set[int]:
"""Returns token IDs that can legally continue a JSON number value."""
valid = set()
for s, tid in str_to_id.items():
norm = s.replace("Ä ", " ").strip()
candidate = partial_value + norm
if is_valid_number_prefix(candidate):
valid.add(tid)
return valid
def is_valid_number_prefix(s: str) -> bool:
"""Return True if s is a valid prefix of a JSON number."""
import re
pattern = r'^-?(?:\d+(?:\.\d*)?(?:[eE][+-]?\d*)?)?$'
return bool(re.match(pattern, s))
type: "string"¶
def get_valid_string_tokens(
str_to_id: dict[str, int],
in_string: bool,
string_content: str
) -> set[int]:
if not in_string:
return {tid for s, tid in str_to_id.items() if s == '"'}
valid = set()
for s, tid in str_to_id.items():
if s == '"':
valid.add(tid) # closing quote
continue
if '"' not in s and '\\' not in s:
valid.add(tid) # any token without unescaped quote
elif '\\"' in s:
valid.add(tid) # escaped quote is fine
return valid
type: "boolean"¶
def get_valid_boolean_tokens(
str_to_id: dict[str, int],
partial_value: str
) -> set[int]:
"""Tokens that continue 'true' or 'false' from partial_value."""
valid = set()
for target in ["true", "false"]:
if target.startswith(partial_value):
remaining = target[len(partial_value):]
for s, tid in str_to_id.items():
norm = s.replace("Ä ", " ")
if remaining.startswith(norm) and norm:
valid.add(tid)
return valid
The -inf Trick¶
Negative infinity in Python's float system:
import math
NEGATIVE_INF = -math.inf
When you set a logit to -math.inf, the softmax calculation gives it a probability of exactly 0 â it can never be selected.
def apply_constraints(
logits: list[float],
valid_token_ids: set[int]
) -> list[float]:
"""Mask all tokens not in valid_token_ids to -inf."""
constrained = logits.copy()
for i in range(len(constrained)):
if i not in valid_token_ids:
constrained[i] = -math.inf
return constrained
Watch out: if
valid_token_idsis empty, all logits become-infandargmaxwill behave unpredictably. Always assert before applying constraints:
assert len(valid_token_ids) > 0, (
f"No valid tokens found at position '{partial_json}' â "
f"check your state machine logic"
)
Complete Walkthrough¶
Let's trace through generating the output for "Greet john" step by step.
Function definition:
{"name": "fn_greet", "parameters": {"name": {"type": "string"}}}
Expected output:
{"name": "fn_greet", "parameters": {"name": "john"}}
partial: "" state: START
valid: {tid for '{'}
model picks: "{" (id=5476 in Qwen3 vocab)
â partial: "{"
partial: "{" state: AFTER_OPEN_BRACE
valid: {tid for '"'}
model picks: '"' (id=1)
â partial: '{"'
partial: '{"' state: IN_NAME_KEY
valid: tokens continuing '"name"' from position 1
â tids for "n", "na", "nam", "name"
model picks: token for "name"
â partial: '{"name'
partial: '{"name' state: IN_NAME_KEY
valid: {tid for '"'} â closing the key
model picks: '"'
â partial: '{"name"'
partial: '{"name"' state: AFTER_NAME_KEY
valid: {tids for ":", "Ä :", ": "}
model picks: token for ": "
â partial: '{"name": '
partial: '{"name": ' state: BEFORE_NAME_VALUE
valid: {tid for '"'}
model picks: '"'
â partial: '{"name": "'
partial: '{"name": "' state: IN_NAME_VALUE
valid: tokens continuing any valid function name from ""
"fn_add_numbers" â tids for "fn", "fn_", "fn_a", ...
"fn_greet" â tids for "fn", "fn_", "fn_g", ...
"fn_reverse_string" â ...
model sees "Greet john" â assigns highest logit to "fn_greet" prefix
model picks: token for "fn_greet" (or "fn" then "_greet")
â partial: '{"name": "fn_greet'
partial: '{"name": "fn_greet' state: IN_NAME_VALUE (complete)
valid: {tid for '"'}
model picks: '"'
â partial: '{"name": "fn_greet"'
partial: '{"name": "fn_greet"' state: AFTER_NAME_VALUE
valid: {tid for ','}
model picks: ","
â partial: '{"name": "fn_greet",'
... (similar process for '"parameters"' key, ':', '{') ...
â partial: '{"name": "fn_greet", "parameters": {"'
state: IN_ARG_KEY
valid: tokens continuing '"name"' â only param for fn_greet is "name"
model picks: token for "name"
â partial: '{"name": "fn_greet", "parameters": {"name'
â partial: '{"name": "fn_greet", "parameters": {"name"'
â partial: '{"name": "fn_greet", "parameters": {"name": '
â partial: '{"name": "fn_greet", "parameters": {"name": "'
state: IN_ARG_VALUE_STR (type is "string")
valid: all printable non-quote tokens + closing '"'
model extracts "john" from "Greet john" â picks token for "john"
â partial: '{"name": "fn_greet", "parameters": {"name": "john'
valid: {tid for '"'} â must close the string
model picks: '"'
â partial: '{"name": "fn_greet", "parameters": {"name": "john"'
state: AFTER_ARG_VALUE
no more params â valid: {tid for '}'}
model picks: "}"
â partial: '{"name": "fn_greet", "parameters": {"name": "john"}'
state: AFTER_PARAMS_CLOSE
valid: {tid for '}'}
model picks: "}"
â partial: '{"name": "fn_greet", "parameters": {"name": "john"}}'
state: COMPLETE â stop generation
json.loads('{"name": "fn_greet", "parameters": {"name": "john"}}')
â {"name": "fn_greet", "parameters": {"name": "john"}} â
Putting It All Together¶
def get_valid_token_ids(
partial: str,
fn_def: FunctionDefinition,
all_fn_names: list[str],
str_to_id: dict[str, int], # token_string â token_id
state: JSONState,
context: dict
) -> set[int]:
"""
Central dispatch: given the current state and partial output,
return the set of token IDs that are valid next tokens.
"""
def find(target: str) -> set[int]:
"""Find token IDs whose normalized string equals target."""
return {tid for s, tid in str_to_id.items()
if s.replace("Ä ", " ") == target}
def find_strip(target: str) -> set[int]:
"""Find token IDs whose stripped normalized string equals target."""
return {tid for s, tid in str_to_id.items()
if s.replace("Ä ", " ").strip() == target}
if state == JSONState.START:
return find_strip("{")
if state == JSONState.AFTER_OPEN_BRACE:
return find('"')
if state == JSONState.IN_NAME_KEY:
return tokens_continuing(str_to_id, '"name"', context["name_key_written"])
if state == JSONState.AFTER_NAME_KEY:
return find_strip(":")
if state == JSONState.BEFORE_NAME_VALUE:
return find('"')
if state == JSONState.IN_NAME_VALUE:
written = context["name_value_written"]
return get_valid_name_tokens(str_to_id, all_fn_names, written)
if state == JSONState.AFTER_NAME_VALUE:
return find('"') # closing quote of name value
# ... continue for all states ...
if state == JSONState.IN_ARG_VALUE_NUM:
return get_valid_number_tokens(str_to_id, context["current_value"])
if state == JSONState.IN_ARG_VALUE_STR:
return get_valid_string_tokens(
str_to_id=str_to_id,
in_string=True,
string_content=context["current_value"]
)
if state == JSONState.IN_ARG_VALUE_BOOL:
return get_valid_boolean_tokens(str_to_id, context["current_value"])
raise ValueError(f"Unhandled state: {state}")
Common Mistakes¶
â Using vocab[str(next_id)] or vocab[next_id] to get the token string¶
The vocab format is str â int, not int â str. After picking next_id, you need the reverse lookup:
# â Wrong â vocab keys are strings, not IDs
token_str = vocab[str(next_id)]
token_str = vocab[next_id]
# â
Correct â use the pre-built reverse lookup
token_str = id_to_str[next_id]
â Forgetting .tolist() on encode output¶
model.encode() returns a 2D tensor, not a list of ints:
# â Wrong â returns a 2D tensor
input_ids = model.encode(prompt)
# â
Correct â flatten to a list of ints
input_ids = model.encode(prompt)[0].tolist()
â Searching the whole vocabulary in the hot loop¶
Pre-compute fixed sets at startup. Only do dynamic searches (function name prefixes, param name prefixes) when in those specific states.
â Assuming tokens are single characters¶
A token like "fn_greet" might be emitted as one token fn_greet, or as fn + _greet. Your prefix-matching logic must handle all cases:
# â Wrong: assuming 'fn_greet' is always one token
valid = {tid for s, tid in str_to_id.items() if s == "fn_greet"}
# â
Correct: allow any token that continues what's needed
def tokens_continuing(str_to_id, target: str, written: str) -> set[int]:
remaining = target[len(written):]
return {tid for s, tid in str_to_id.items()
if remaining.startswith(s.replace("Ä ", " ")) and s.replace("Ä ", " ")}
â Not handling the closing } of the root object¶
After the parameters object closes with }, you still need one more } to close the root object. Make sure your state machine has a state for this.
â Hardcoding function names¶
Your solution must work with any functions_definition.json. Never hardcode "fn_add_numbers" â always read function names dynamically from the loaded schema.
See also: TOKENIZATION.md for how the vocabulary file is structured and how to handle BPE token edge cases.
See also: FUNCTION_CALLING.md for the full input/output format and schema structure.