hf-chat-template
Render a Hugging Face chat_template — the Jinja2 string embedded in a model's
tokenizer_config.json — into the exact prompt a model expects, byte-for-byte identical to
Python's transformers.apply_chat_template.
If you do local inference in Rust (candle, mistral.rs, llama-cpp bindings, a custom server), this
is the prompt-building layer you would otherwise reimplement by hand — and get subtly wrong. A
single stray newline or a re-sorted tojson key silently corrupts every prompt downstream. This
crate's job is to not do that, and to prove it against a golden corpus on every commit.
[]
= "0.1"
Example
use ;
let tmpl = from_str?;
let prompt = tmpl.render_messages?;
assert_eq!;
# Ok::
Or load a real model's config directly — special tokens (bos_token, eos_token, …) are
injected for you, and the named-template forms (tool_use, rag) are resolved:
use ;
let json = read_to_string?;
let cfg: TokenizerConfig = from_str?;
let tmpl = from_tokenizer_config?;
let prompt = tmpl.render_messages?;
# Ok::
For full control — tools, documents, or model-specific kwargs the typed model doesn't
name — build a RenderInput (or drop to render_value with an arbitrary minijinja::Value).
What it does
- Correctness is the product. The engine (
minijinja) already exists below us. The value here is the thintransformers-compatibility layer plus a corpus proving byte-identical output on real models. - Installs the globals templates actually use:
raise_exception,strftime_now, a Python-compatibletojson(matchingjson.dumps(…, ensure_ascii=False)separators and key order), and Python string/list/dict methods viapycompat. - Handles all three
chat_templateshapes (single string, named list, dict), special-token injection, and the string-or-parts multimodalcontent. - Emits a prompt string. Turning it into token IDs stays the caller's job (
tokenizers,tiktoken-rs, …) — that boundary is deliberate.
Verified compatibility
Every model below renders byte-identical to transformers in CI. See
COMPATIBILITY.md and the corpus.
| Model | Notes |
|---|---|
| Qwen2.5, Qwen3 | ChatML, tool calling (tojson) |
| SmolLM2 | ChatML |
| Phi-3 | `< |
| Hermes-3-Llama-3.1 | named tool_use sub-template, Jinja macros + recursion |
Feature flags
| Feature | Default | Effect |
|---|---|---|
pycompat |
✅ | Python methods on values (.strip(), .split(), | items, …) via minijinja-contrib. Disable to drop that dependency if your templates don't need them. |
Caveats
- No automatic BOS doubling. If a template emits
{{ bos_token }}, setadd_special_tokens = falseat encode time so the tokenizer doesn't add BOS again. This crate renders exactly what the template says and never strips silently. strftime_nowdefaults to UTC.transformersuses local time. Inject aFixedClock(or your ownClock) when you need to match a specific reference exactly.
License
Dual-licensed under MIT or Apache-2.0, at your option.
The files under tests/corpus/ are trimmed excerpts of upstream model configs, redistributed
under each model's own license — see tests/corpus/README.md.