Ask a language model for JSON and it will happily hand you a stray brace, a trailing comma, or a confidently hallucinated field name. Structured generation attacks the problem at the root: instead of parsing and repairing whatever the model emits, you forbid invalid tokens before they are ever sampled. Outlines, maintained by dottxt, is the library that popularized this technique, and its fingerprints are all over modern serving stacks — the guided-decoding paths in vLLM, SGLang, and TGI all grew out of ideas this project proved out in production.
The current main branch of the repository is a good moment to read the source, because the codebase has been deliberately split in two. The pure-Python package under src/outlines handles everything users touch: output types, model wrappers, prompt templates, and the Generator factory. The hot path — compiling a regex against a tokenizer’s vocabulary and walking the resulting finite-state machine token by token — lives in a separate Rust-backed companion package called outlines-core, pinned to version 0.2.14 right in the project’s pyproject.toml. The Python side talks to it through a thin, well-documented boundary.
The whole architecture follows from one observation: whether a token is legal depends on which tokens came before it, and that dependency is exactly what a finite-state machine encodes. So a Python type or JSON schema becomes a Term, the Term becomes a regex, the regex plus the vocabulary of your tokenizer becomes an index over token IDs, and the index drives a per-step bitmask over the logits. Every file in the repository exists to serve one of those five stages. If you have read our tour of transformers, you will recognize the integration point immediately: a logits_processor callback handed to the model’s generate loop.
Overview of the Outlines architecture: the public API compiles output types through the Term DSL, the backend registry delegates to outlines-core or llguidance, and the resulting logits processors are wired into steerable model wrappers that mask logits at every decode step.
Reading the overview from left to right:
- The Generator factory, L347 is the front door. It inspects the model and returns a
SteerableGenerator, aBlackBoxGenerator, or an async variant, depending on whether Outlines controls decoding or merely forwards the output type to a hosted provider. - Inside the steerable path, SteerableGenerator.init, L237 normalizes whatever you passed via python_types_to_terms, L730 — dataclasses, TypedDicts, pydantic models, enums, literals, or bare Python values all become
Terminstances. - The term is lowered to a regex by to_regex, L978, which is where a pydantic model becomes a deterministic pattern over token strings rather than a hopeful prompt suffix.
- The compiled regex is handed to get_json_schema_logits_processor, L58 and its regex and CFG siblings, which route to a backend by name. The defaults live three lines up: JSON_SCHEMA_DEFAULT_BACKEND, L28 is
outlines_core, while context-free grammars default to llguidance. - The default backend, OutlinesCoreBackend, L176, extracts your tokenizer’s vocabulary, drops the EOS token from it, and compiles Index(regex, self.vocabulary), L250 inside the Rust core.
- At generation time, OutlinesCoreLogitsProcessor.process_logits, L140 advances one
Guideper batch row with the last sampled token and fills a token bitmask before scores are computed. - All of that machinery inherits from OutlinesLogitsProcessor, L14, the small base class that standardizes
input_idsand logits to 2D tensors and picks the right tensor adapter. - On the model side, Transformers, L172 is the canonical steerable wrapper: its
generatepasses the processor straight into the Hugging Facegeneratecall, while SteerableModel, L32 defines exactly which wrappers qualify.
From Types to Regex: the Term DSL
The most elegant file in the repository is src/outlines/types/dsl.py. It opens with a base Term class, L82 that overloads + and | so that regular-expression composition reads like arithmetic: Regex("[0-9]{4}") + "-" + Regex("[0-9]{2}") builds a Sequence, and either("yes", "no") builds Alternatives. On top of that sit the concrete terms — Regex, L222 for raw patterns, CFG, L241 for context-free grammars loaded from bundled Lark files, and JsonSchema, L282 for schema strings or pydantic models.
The converter that makes this feel magical is python_types_to_terms, L730. An int maps to an integer pattern, a Literal["Positive", "Negative"] maps to alternatives of escaped strings, a plain dict maps to CFG(grammars.json), and dataclasses, TypedDicts, and pydantic models are serialized to JSON schema and then translated to regex by the Rust core’s build_regex_from_schema. There is a recursion guard capped at depth ten, which is a pragmatic admission that recursive schemas will otherwise explode. The pydantic integration is bidirectional: Term.__get_pydantic_json_schema__ at L139 emits a "pattern" keyword, so custom regex types validate inside ordinary pydantic models too.
The Rust Core: Vocabulary, Index, Guide
Three Rust types do the heavy lifting, and the Python adapter around them is refreshingly honest about its job. create_outlines_core_vocabulary, L259 walks your tokenizer’s token -> id mapping, converts each token to its true string representation (so the BPE artifacts like Ġworld become " world"), groups IDs by string, and removes the EOS token — the core adds termination back on its own terms. A regex plus that vocabulary yields an Index, a finite-state machine whose states are sets of token sets; a Guide is a cursor moving through that index as tokens are sampled.
The per-step loop in process_logits is where correctness nuances live. Each batch element’s guide is advanced with the last sampled token id, but only when the guide is unfinished or still accepts the token — the code even cites outlines-core issue 227 as the reason for the workaround at L168. Bitmask allocation and application are delegated to per-library kernels exported by the core, so the hot loops stay in Rust and the Python layer stays thin. This is the same philosophy behind libraries like MLX keeping graph execution native: spend Python only on decisions, not on per-token arithmetic.
One Processor, Three Tensor Libraries
Every logits processor is tensor-library agnostic by construction. The base class selects an adapter from the registry in src/outlines/processors/tensor_adapters/__init__.py — TorchTensorAdapter, L6 for PyTorch, plus numpy and MLX implementations behind the same small protocol of shape, unsqueeze, concatenate, and device moves. When the first token is generated, the processor lazily builds one guide and one bitmask per batch row, choosing the kernel module that matches the library. For torch, bias_logits, L104 fills the bitmask on CPU, ships it to the device holding the logits, applies it in place, and ships it back — a deliberate trade of a couple of transfers per step for a single kernel implementation.
The lazy _setup is a subtle design win: batch size and vocabulary size are unknown until real tensors arrive, so the processor defers all allocation to the first call and then reuses the guides, resetting only the cursor between generations. That is also why the Generator resets its processor at the top of __call__, batch, and stream — reuse the expensive FSM, rewind the cheap state.
Black-Box Models and Provider Routing
Not every model hands you its logits. For hosted providers, Outlines draws a hard line with two protocol unions in src/outlines/models/__init__.py: steerable models (transformers, llama.cpp, L244, MLX-LM, L102) accept logits processors, while black-box models — OpenAI, Anthropic, Gemini, Mistral, Ollama, and the vLLM server client — receive the raw output type and decide enforcement themselves, usually via structured-output request fields. The BlackBoxGenerator, L28 simply forwards the type, which keeps the same model(prompt, output_type) ergonomics everywhere. For in-process serving there is VLLMOffline, L103 with from_vllm_offline, L278, which converts types with the same DSL and lets the vLLM engine apply masking internally. Provider errors are funneled through normalize_provider_errors into a uniform APIError hierarchy, so your retry logic does not need to know five SDK conventions.
From Install to Your First Model
Everything above reduces to a short, verified recipe. Install the library with the transformers extra:
pip install outlines
pip install "outlines[transformers]"
Then wire up any Hugging Face causal LM and constrain it. Both snippets below use only APIs present in the source we just read:
import outlines
from typing import Literal
from transformers import AutoModelForCausalLM, AutoTokenizer
model = outlines.from_transformers(
AutoModelForCausalLM.from_pretrained("Qwen/Qwen2.5-0.5B-Instruct"),
AutoTokenizer.from_pretrained("Qwen/Qwen2.5-0.5B-Instruct"),
)
# Direct call: the output type is compiled on the fly.
sentiment = model(
"Analyze: 'This product completely changed my life!'",
Literal["Positive", "Negative", "Neutral"],
)
print(sentiment) # exactly one of the three literals
For anything reused, build a Generator once so the FSM is compiled a single time, and validate with pydantic afterwards:
from pydantic import BaseModel
class ProductReview(BaseModel):
rating: int
pros: list[str]
cons: list[str]
summary: str
generator = outlines.Generator(model, ProductReview)
review = generator("Review: great battery life, runs hot", max_new_tokens=200)
parsed = ProductReview.model_validate_json(review)
The second form matters more than it looks: the generator holds the compiled index, so a loop over ten thousand reviews pays for compilation once. Compiled artifacts are additionally memoized on disk through get_cache, L43, which you can disable or clear via disable_cache and clear_cache. If you prefer prompt-as-function ergonomics, Template, L59 turns annotated Python functions into Jinja prompt builders, and Application, L14 binds a template plus an output type into a callable.
Honest Limits
Context-free grammars are the sharpest edge. The default JSON-schema and regex backends cannot handle them at all — the outlines-core backend raises NotImplementedError for CFGs at L253 — so grammatical outputs ride entirely on llguidance, and a plain dict output type inherits that route too. Streaming is not implemented for transformers models; generate_stream raises NotImplementedError at L355, pending upstream support in the Hugging Face generate loop. The Rust-backed processor also supports only three tokenizer extraction paths — transformers, llama.cpp, and MLX — so exotic steerable models cannot use the default backend until someone writes the vocabulary shim.
Two more caveats belong in any honest review. First, the JAX and TensorFlow model paths emit deprecation warnings in the source and explicitly do not support structured generation, so PyTorch is the practical target. Second, for black-box providers Outlines cannot guarantee anything: it serializes your schema into the request and trusts the provider’s enforcement, which varies. None of this diminishes the core achievement. The repository demonstrates, file by file, that the gap between “the model usually returns valid JSON” and “the model cannot return anything else” is a compiler problem — and one that a few thousand lines of Python plus a Rust core can close completely. Enjoyed this post? Never miss out on future posts by following us