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 | ChatML, tool calling (tojson) |
| SmolLM2 | ChatML |
| Phi-3 | `< |
| Hermes-3-Llama-3.1 | named tool_use sub-template, Jinja macros and recursion |
| LFM2 | standalone chat_template.jinja file, tool list (tojson) |
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.
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. Inject a FixedClock or
your own Clock to match a specific reference.
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.