Grammar-constrained LLM token generation via Lark + HuggingFace Transformers
Project description
transformers-grammar-constraint
Enforce formal grammar constraints on LLM token generation with HuggingFace Transformers. Uses Lark to define grammars and masks invalid tokens at each generation step, so the model can only produce output that conforms to your grammar.
jsonformer exists if you need something more light weight that only supports JsonSchema.
from grammar_constrain import GrammarConstrainedLogitsProcessor, JsonGrammar
grammar = JsonGrammar(schema={"type": "object", "properties": {"name": {"type": "string"}}, "required": ["name"]})
processor = GrammarConstrainedLogitsProcessor(grammar=grammar, tokenizer=tokenizer)
output = model.generate(input_ids, logits_processor=[processor], max_new_tokens=50)
# Guaranteed to produce valid JSON matching the schema
Installation
uv add transformers-grammar-constraint
Requirements: Python >= 3.12, PyTorch >= 2.10, Transformers 4.x or 5.x (>= 4.17)
How It Works
GrammarConstrainedLogitsProcessor is a LogitsProcessor that plugs into the model.generate() call. Before each token is sampled, it computes which token IDs are valid given the current parse state and sets all others to -inf. The model never sees invalid candidates.
Usage
With model.generate()
from transformers import AutoTokenizer, AutoModelForCausalLM
from grammar_constrain import GrammarConstrainedLogitsProcessor, JsonGrammar
tokenizer = AutoTokenizer.from_pretrained("your-model")
model = AutoModelForCausalLM.from_pretrained("your-model")
grammar = JsonGrammar()
processor = GrammarConstrainedLogitsProcessor(grammar=grammar, tokenizer=tokenizer)
input_ids = tokenizer("Output JSON: ", return_tensors="pt").input_ids
output = model.generate(
input_ids,
logits_processor=[processor],
max_new_tokens=100,
do_sample=False,
)
text = tokenizer.decode(output[0][input_ids.shape[1]:], skip_special_tokens=True)
# text is guaranteed to be valid JSON
With pipeline()
from transformers import pipeline
from grammar_constrain import GrammarConstrainedLogitsProcessor, JsonGrammar
pipe = pipeline("text-generation", model="your-model")
grammar = JsonGrammar()
processor = GrammarConstrainedLogitsProcessor(grammar=grammar, tokenizer=pipe.tokenizer)
result = pipe(
"Output JSON: ",
logits_processor=[processor],
max_new_tokens=100,
return_full_text=False,
do_sample=False,
)
text = result[0]["generated_text"].strip()
# text is guaranteed to be valid JSON
Reusing a processor across calls
Create one processor and call it repeatedly — it resets its state automatically between independent generate() / pipeline calls.
Thinking models
For models that use <think>...</think> reasoning blocks, pass the think tokens so constraints are suspended inside them:
processor = GrammarConstrainedLogitsProcessor(
grammar=grammar,
tokenizer=tokenizer,
think_start_token="<think>",
think_end_token="</think>",
)
Set either to None to disable think-block handling entirely.
Available Grammars
JsonGrammar
Constrains generation to valid JSON. Optionally accepts a JSON Schema to further restrict structure.
from grammar_constrain import JsonGrammar
# Any valid JSON
grammar = JsonGrammar()
# Object matching a schema
grammar = JsonGrammar(schema={
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"},
},
"required": ["age", "name"], # enforced in alphabetical order
"additionalProperties": False,
})
# Array of strings
grammar = JsonGrammar(schema={
"type": "array",
"items": {"type": "string"},
})
# Enum
grammar = JsonGrammar(schema={
"type": "string",
"enum": ["red", "green", "blue"],
})
Supported JSON Schema keywords: type, properties, required, items, enum, additionalProperties.
Note:
requiredkeys are enforced in alphabetical order. Schema keywords such asoneOf,anyOf,allOf,$ref,pattern,minLength,maxLength,minimum, andmaximumare not yet supported and will be ignored with a warning.
SanGrammar and PgnGrammar
Constrain generation to valid chess notation. Both are implemented as
hand-crafted character-level DFAs (not Lark grammars), because SAN
disambiguation (e.g. Nbd2, R1e2) is ambiguous for LALR(1). PgnGrammar
embeds the SAN DFA for individual move bodies and adds structural states
for move numbers, spacing, and game results.
from grammar_constrain import SanGrammar, PgnGrammar
# Single move in Standard Algebraic Notation
grammar = SanGrammar()
# Valid output examples: "e4", "Nf3", "Rxe5+", "O-O", "e8=Q#", "Nbd2"
# Full game in Portable Game Notation
grammar = PgnGrammar()
# Valid output example: "1. e4 e5 2. Nf3 Nc6 1-0"
Extending to New Grammars
Subclass Grammar and implement the grammar_string property using Lark EBNF syntax.
from grammar_constrain import Grammar
class TemperatureGrammar(Grammar):
@property
def grammar_string(self) -> str:
return r"""
start: number unit
number: /-?\d+(\.\d+)?/
unit: "°C" | "°F" | "K"
%import common.WS
%ignore WS
"""
Use it like any built-in grammar:
processor = GrammarConstrainedLogitsProcessor(
grammar=TemperatureGrammar(),
tokenizer=tokenizer,
)
Choosing a parser backend
The default parser is LALR (fast). If your grammar is ambiguous or has shift-reduce conflicts, switch to Earley:
class MyGrammar(Grammar):
lark_parser = "earley" # override class variable
@property
def grammar_string(self) -> str:
return r"..."
LALR is significantly faster; prefer it when possible.
Inline grammars (no subclass needed)
Pass a raw Lark grammar string directly to the processor:
processor = GrammarConstrainedLogitsProcessor(
grammar=r"""
start: ("yes" | "no") /\n/
""",
tokenizer=tokenizer,
)
Validating output
Every Grammar exposes is_valid_complete(text) to check whether a string is a complete, valid parse:
grammar = JsonGrammar()
assert grammar.is_valid_complete('{"key": 1}') # True
assert not grammar.is_valid_complete('{"key":') # False — incomplete
Running Tests
# Unit tests (no model download required)
uv run pytest tests/ -v
# Integration tests with a real model (requires network access)
uv run pytest tests/test_integration.py -v -m slow
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file transformers_grammar_constraint-0.2.2.tar.gz.
File metadata
- Download URL: transformers_grammar_constraint-0.2.2.tar.gz
- Upload date:
- Size: 16.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
413f75472624736fab5ef3346e5fbe907d6b70d9e24c28a4096c581743cb2a8c
|
|
| MD5 |
f44275c4b21558ea8cb409dddece448d
|
|
| BLAKE2b-256 |
ab1a928f21346f44953aa16132c439f598cb6a9a30f38a3fa0e9961abfb84022
|
Provenance
The following attestation bundles were made for transformers_grammar_constraint-0.2.2.tar.gz:
Publisher:
release.yml on SrzStephen/transformers-grammar-constraint
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
transformers_grammar_constraint-0.2.2.tar.gz -
Subject digest:
413f75472624736fab5ef3346e5fbe907d6b70d9e24c28a4096c581743cb2a8c - Sigstore transparency entry: 1279114363
- Sigstore integration time:
-
Permalink:
SrzStephen/transformers-grammar-constraint@d066bb082f9a169a2fc1faa5b6cde80d37255122 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/SrzStephen
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@d066bb082f9a169a2fc1faa5b6cde80d37255122 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file transformers_grammar_constraint-0.2.2-py3-none-any.whl.
File metadata
- Download URL: transformers_grammar_constraint-0.2.2-py3-none-any.whl
- Upload date:
- Size: 21.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9dff4afa5bfe28e5daa9436cf5f50ecf59d6fb40c888e1885943d14aac1a372f
|
|
| MD5 |
84e41596759f9e8418a80bac58ace2bc
|
|
| BLAKE2b-256 |
e36ed3d7a9f27643dc0213f247fe4f89f83031b192f80b7670309ef341070368
|
Provenance
The following attestation bundles were made for transformers_grammar_constraint-0.2.2-py3-none-any.whl:
Publisher:
release.yml on SrzStephen/transformers-grammar-constraint
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
transformers_grammar_constraint-0.2.2-py3-none-any.whl -
Subject digest:
9dff4afa5bfe28e5daa9436cf5f50ecf59d6fb40c888e1885943d14aac1a372f - Sigstore transparency entry: 1279114382
- Sigstore integration time:
-
Permalink:
SrzStephen/transformers-grammar-constraint@d066bb082f9a169a2fc1faa5b6cde80d37255122 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/SrzStephen
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@d066bb082f9a169a2fc1faa5b6cde80d37255122 -
Trigger Event:
workflow_dispatch
-
Statement type: