# hf-chat-template
[](https://github.com/GregoryBolshakov/hf-chat-template/actions/workflows/ci.yml)
[](https://crates.io/crates/hf-chat-template)
[](https://docs.rs/hf-chat-template)
[](#license)
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`.
```toml
[dependencies]
hf-chat-template = "0.1"
```
## Example
```rust
use hf_chat_template::{ChatTemplate, Message};
let tmpl = ChatTemplate::from_str(
"{% for m in messages %}<|im_start|>{{ m.role }}\n{{ m.content }}<|im_end|>\n{% endfor %}\
{% if add_generation_prompt %}<|im_start|>assistant\n{% endif %}",
)?;
let prompt = tmpl.render_messages(&[Message::user("Hello!")], true)?;
```
Load a real model's config to inject its special tokens and resolve named templates:
```rust,no_run
use hf_chat_template::{ChatTemplate, Message, TokenizerConfig};
let json = std::fs::read_to_string("tokenizer_config.json")?;
let cfg: TokenizerConfig = serde_json::from_str(&json)?;
let tmpl = ChatTemplate::from_tokenizer_config(&cfg)?;
let prompt = tmpl.render_messages(&[Message::user("Hi")], true)?;
# Ok::<(), Box<dyn std::error::Error>>(())
```
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`](https://crates.io/crates/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`](COMPATIBILITY.md) and the [corpus](tests/corpus/).
| Qwen2.5, Qwen3 | ChatML, tool calling (`tojson`) |
| SmolLM2 | ChatML |
| Phi-3 | `<|user|>` / `<|end|>` markers |
| Hermes-3-Llama-3.1 | named `tool_use` sub-template, Jinja macros and recursion |
## 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.
## 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](LICENSE-MIT) or [Apache-2.0](LICENSE-APACHE), 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`.
[`RenderInput`]: https://docs.rs/hf-chat-template/latest/hf_chat_template/struct.RenderInput.html