hf-chat-template
Render a Hugging Face chat_template into a prompt string, byte-for-byte identical to Python's
transformers.apply_chat_template. The template is the Jinja2 string stored in a model's
tokenizer_config.json.
[]
= "0.1"
Example
use ;
let tmpl = from_str?;
let prompt = tmpl.render_messages?;
assert_eq!;
# Ok::
Load a real model's config to inject its special tokens and resolve named templates:
use ;
let json = read_to_string?;
let cfg: TokenizerConfig = from_str?;
let tmpl = from_tokenizer_config?;
let prompt = tmpl.render_messages?;
# Ok::
Newer models ship the template as a standalone chat_template.jinja file instead of inside
tokenizer_config.json. Load that with from_template_and_config, passing the template string
and the config the special tokens come from.
use ;
let jinja = read_to_string?;
let cfg: TokenizerConfig = from_str?;
let tmpl = from_template_and_config?;
let prompt = tmpl.render_messages?;
# Ok::
For tools, documents, or model-specific kwargs, build a RenderInput. For an arbitrary
context, call render_value with a minijinja::Value.
What it does
The Jinja engine is minijinja. This crate adds the
transformers compatibility layer on top of it, plus a corpus that checks byte-identical output
against real models on every commit.
It installs the globals that templates use: raise_exception, strftime_now, and a
Python-compatible tojson that matches the separators and key order of
json.dumps(..., ensure_ascii=False). Python string, list, and dict methods come from pycompat.
It handles the three chat_template shapes (single string, named list, dict), special-token
injection, and the string-or-parts multimodal content.
It emits a prompt string. Turning that into token IDs stays the caller's job (tokenizers,
tiktoken-rs).
Verified compatibility
These models render byte-identical to transformers in CI. See
COMPATIBILITY.md and the corpus.
| Model | Notes |
|---|---|
| Qwen2.5, Qwen3, QwQ-32B | ChatML, tool calling (tojson), reasoning |
| SmolLM2 | ChatML |
| Phi-3 | <|user|> / <|end|> markers |
| Hermes-3-Llama-3.1 | named tool_use sub-template, Jinja macros and recursion |
| Mistral-7B-Instruct-v0.3 | [INST] / [AVAILABLE_TOOLS], tool calling |
| DeepSeek-R1-Distill, deepseek-llm | reasoning (<think>), User: / Assistant: |
| OpenChat-3.5, Zephyr, Yi-1.5, Falcon | varied prompt formats, pycompat methods |
| LFM2 | standalone chat_template.jinja file, tool list (tojson) |
| SmolLM3 | standalone file, {% generation %} reasoning block |
| Granite-3.1 | strftime_now date stamp (clock pinned for a reproducible match) |
| Gemma-2, Gemma-3 | <start_of_turn> format, no system role (Gemma-2), system merge and content parts (Gemma-3) |
| Command-R | named default / tool_use / rag templates, Jinja macros and recursion |
Nineteen models, sixty-four cases, all byte-identical in CI.
Loading from the Hub
The hub feature adds from_hub, which fetches a model's config and template and compiles it in
one call. It loads tokenizer_config.json, plus a standalone chat_template.jinja when the model
ships one (the standalone file wins over an inline chat_template, matching transformers). It
uses the synchronous hf-hub client with rustls, so there is no system OpenSSL dependency. Authentication follows hf-hub: the HF_TOKEN env var or the token
from huggingface-cli login, which gated repos need.
= { = "0.1", = ["hub"] }
use ;
let tmpl = from_hub?;
let prompt = tmpl.render_messages?;
# Ok::
Tokenizing
The tokenizers feature adds render_and_encode, which renders the prompt and encodes it to
token IDs in one step. It encodes with add_special_tokens = false, because the template already
emits the model's special tokens. This is what transformers.apply_chat_template(..., tokenize=True) does, and it avoids a doubled BOS.
= { = "0.1", = ["tokenizers"] }
use ;
use Tokenizer;
let tmpl = from_str?;
let tok = from_file.unwrap;
let input = RenderInput ;
let = tmpl.render_and_encode?;
# Ok::
Feature flags
pycompat is on by default. It adds Python methods on values (.strip(), .split(), | items)
through minijinja-contrib. Disable it to drop that dependency when your templates do not use
those methods.
hub is off by default. It adds from_hub and from_hub_revision, pulling in hf-hub and a
TLS stack that the core string-rendering path does not need.
tokenizers is off by default. It adds render_and_encode and re-exports tokenizers, pulling
in that crate and its onig regex backend.
strftime is off by default. It adds LocalClock, a strftime_now clock that reads local wall
time to match transformers, pulling in chrono. The default SystemClock is UTC and needs no
extra dependency.
Caveats
This crate does not add or strip a BOS token. If a template emits {{ bos_token }}, set
add_special_tokens = false at encode time so the tokenizer does not add BOS a second time. It
renders what the template says.
strftime_now defaults to UTC, while transformers uses local time. Enable the strftime
feature and inject LocalClock to match local time, or inject a FixedClock to pin a specific
date.
License
Dual-licensed under MIT or Apache-2.0, at your option.
Files under tests/corpus/ are trimmed excerpts of upstream model configs, redistributed under
each model's own license. See tests/corpus/README.md.