On 20 April 2026, CERT published an advisory about an inference server that would run
attacker-supplied Python if you loaded the wrong model file. The payload was not in the weights
and it was not in the prompt. It rode inside the model's chat template, and SGLang rendered that
template through a plain jinja2.Environment() instead of a sandboxed one, on the /v1/rerank
endpoint. CVE-2026-5760, rated 9.8. One missing constructor argument.
Under vendor response, CERT recorded: "No response or patch was obtained during the coordination process."
Nine days later the fix landed in SGLang's main. Three lines - an import, a swapped constructor,
a comment. The pull request was opened by ccullen-cert. The CERT coordinator who had just
published the advisory ended up writing the vendor's patch.
This was not a hard bug. Nobody had looked at that constructor call, so nobody noticed the argument missing from it, and it took someone outside the project to go and add it.
You did not write your prompt
The chat template is the last unowned layer in a production LLM stack. Most other components in that path can at least have a team, a version pin and a test suite, and when they do not, somebody knows it is a gap. The template is different. It does not look like code, so nobody files it under code. It arrives inside the model, in a file most engineers have never opened, and it runs on every inference call.
Other candidates exist and deserve naming: generation_config.json defaults, stop strings,
double-added beginning-of-sequence tokens, cache quantization settings. All unpinned in plenty of
stacks. The template is the sharpest case because it is the only one that is executable
third-party code, and the only one whose output is the exact string the model was trained on.
The consensus belief I am challenging is narrow and widely held: that calling
apply_chat_template means your prompt is correct. It does not. It means you handed prompt
construction to a Jinja program you did not write, did not pin, did not test, and cannot see -
and that program is executed by a different implementation on every stack that runs it.
Hand-rolling an f-string prompt is a beginner mistake with a well-known fix. That is not the argument. The argument is about the code that is already correct.
What a chat template actually is
A chat template is a Jinja program. It ships with the tokenizer, either as a standalone
chat_template.jinja file or in the chat_template field of tokenizer_config.json. It turns
your portable list of role-tagged messages into the one exact string a specific model was
trained to read.
In The Chat Templates Handbook I call that relationship the render contract: the template binds the message model you author in application code to the wire format the model saw during training. The contract is what the benchmark numbers were measured under. Render outside it and those numbers describe a different system than the one you are running.
Breaking it is quiet. The model does not raise. It answers, and the answer is just worse. Hugging Face named this in October 2023, in a post titled Chat Templates: An End to the Silent Performance Killer, and the wording was blunt:
Using a format different from the format a model was trained with will usually cause severe, silent performance degradation.
I call that quality loss the silent format penalty. If you want the ancestry of the message format itself, I wrote that up separately in ChatML: What It Is, Why OpenAI Removed It, What Replaced It.
flowchart LR
A["messages[]<br/>you author this"] --> B["chat_template.jinja<br/>you did not write this"]
B --> C["Jinja engine<br/>varies by stack"]
C --> D["wire format string"]
D --> E["token ids"]
E --> F["model"]
style A fill:#6BCF7F,color:#2C2C2A
style B fill:#E74C3C,color:#FFFFFF
style C fill:#E74C3C,color:#FFFFFF
style D fill:#FFD93D,color:#2C2C2A
style E fill:#FFD93D,color:#2C2C2A
style F fill:#4A90E2,color:#FFFFFF
Green is the part you own. Red is the part that decides what the model actually reads. Yellow is the artifact you almost never look at. The whole thesis is in the colour of the middle two boxes.
Code that is already correct, and still unowned
Every article on this topic shows you a broken f-string and then the fix. That is the easy half, and this audience solved it years ago. So here is the wrong way that survives code review.
from transformers import AutoTokenizertokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-0.6B")messages = [ {"role": "system", "content": "Answer in one sentence."}, {"role": "user", "content": "Why did the deploy fail?"},]prompt = tokenizer.apply_chat_template( messages, tokenize=False, add_generation_prompt=True,)Nothing there is wrong. add_generation_prompt=True is set, which is the flag people forget.
The call is the sanctioned call.
Plenty of teams do not even have this code, which is worse rather than better. They post OpenAI-shaped JSON at a server and let it do the rendering:
client.chat.completions.create(model="qwen3", messages=messages)If that is your shape, you have the same problem with one fewer thing you can see. The template is applied inside a process you did not start, loaded from a file you did not fetch, by a Jinja engine you did not pick.
Either way, ask what the code guarantees, and the list is empty:
- It does not record which template ran. A model upgrade that ships a revised
chat_template.jinjachanges your prompt with no diff in your repository. - It does not assert what came out. Some templates render differently tomorrow from the same messages, for reasons covered under the impure prompt below.
- It does not prove the engine in production renders these bytes. This is Python. Your serving stack may not be.
- It does not fail. There is no assertion here that can go red.
That is a dependency with no pin, no lockfile, and no test. Any other component shipped that way would not survive review.
Silent delegation
Silent delegation is calling apply_chat_template and believing you authored the prompt.
The call is correct; the ownership is missing.
It sits one level above the silent format penalty. The penalty is the bill. Silent delegation is why the bill never arrives. A team hand-rolling an f-string at least has something to grep for, whereas a team practising silent delegation has nothing to find until the template moves underneath them - and then there is a quality regression with no commit attached to it.
The right way: pin what you reviewed, assert what it produced
The fix is not a better call. The call was fine. The fix is to treat the template like every other dependency: pin the version you reviewed, and lock the bytes it produces.
# filename: template_guard.py"""Pin the chat template you reviewed, and the wire format it produced."""from __future__ import annotationsimport hashlibimport sysfrom pathlib import Pathimport transformersfrom transformers import AutoTokenizerMODEL_ID = "Qwen/Qwen3-0.6B"# The commit you reviewed. Without this you are hashing whatever `main`# happens to be today, which is drift detection, not pinning.REVISION = "PASTE_THE_MODEL_REPO_COMMIT_SHA"# Printed by record(). Paste it back here.REVIEWED_SHA256 = "PASTE_THE_VALUE_PRINTED_BY_record"HERE = Path(__file__).resolve().parentGOLDEN = HERE / "golden" / "canary.txt"CANARY = [ {"role": "system", "content": "Answer in one sentence."}, {"role": "user", "content": "Why did the deploy fail?"},]def load(): return AutoTokenizer.from_pretrained(MODEL_ID, revision=REVISION)def template_source(tokenizer) -> str: """The template text, from either place a model may keep it.""" source = tokenizer.chat_template if isinstance(source, dict): # legacy multi-template models source = source.get("default") if source: return source sidecar = Path(tokenizer.name_or_path) / "chat_template.jinja" if sidecar.is_file(): return sidecar.read_text(encoding="utf-8") raise RuntimeError(f"{MODEL_ID} exposes no chat template")def fingerprint(source: str) -> str: return hashlib.sha256(source.encode("utf-8")).hexdigest()def render(tokenizer, messages: list[dict[str, str]]) -> bytes: text = tokenizer.apply_chat_template( messages, tokenize=False, add_generation_prompt=True, ) return text.encode("utf-8")def record() -> None: """Run once, by a human, who reads the output before committing it.""" tokenizer = load() wire = render(tokenizer, CANARY) print(fingerprint(template_source(tokenizer))) print(wire.decode("utf-8")) GOLDEN.parent.mkdir(parents=True, exist_ok=True) GOLDEN.write_bytes(wire)def check() -> None: """Run in continuous integration, on every build.""" tokenizer = load() actual = fingerprint(template_source(tokenizer)) if actual != REVIEWED_SHA256: raise SystemExit( f"template changed: {actual[:12]} != {REVIEWED_SHA256[:12]} " f"- re-review before shipping" ) if not GOLDEN.is_file(): raise SystemExit(f"no golden recorded - run: {__file__} --record") if render(tokenizer, CANARY) != GOLDEN.read_bytes(): raise SystemExit( f"wire format drifted on transformers " f"{transformers.__version__} - diff against {GOLDEN}" )if __name__ == "__main__": record() if "--record" in sys.argv else check()Run it once with --record, read the wire format it prints with your own eyes, then commit the
revision, the hash, and golden/canary.txt. After that, python template_guard.py belongs in
continuous integration forever.
The day a vendor pushes a template revision, or someone bumps the pinned commit, you get this instead of a slow slide on a quality dashboard:
template changed: 8a1f0c2d94b3 != 3f9a1c7e05d2 - re-review before shippingThat is the entire pattern. It is unglamorous, it is about sixty lines, and when I went looking
for apply_chat_template alongside a stored hash or a committed golden file across public
serving repositories, I did not find it anywhere.
What this guard does not cover. It renders through Transformers, which means it renders
through CPython jinja2. If your serving
layer renders somewhere else, this stays
green while production diverges. That is the next section's problem.
Three notes on the code. Reading chat_template.jinja from disk when the attribute is empty is
not defensive padding - google/gemma-4-E2B-it has an open Transformers issue where the template
ships only as the sidecar file and the loader misses it. Comparing bytes rather than strings
matters because Path.read_text normalises newlines, so a string comparison is not the
byte-for-byte check it looks like. And if your template calls strftime_now, freeze the clock
before recording a golden, or the build goes red at midnight and someone re-records it to get
green - which is how this whole pattern dies.
Why one pinned template is still not enough
Pinning fixes drift over time. It does nothing about divergence across space, which is the harder half - and where silent delegation stops being a habit of one team and becomes a property of the ecosystem.
Five Jinja implementations, one template
Executing a Jinja template requires a Jinja engine, and there is no single one. As of August 2026, five distinct implementations run chat templates in production, across four languages - which makes your choice of inference framework a prompt-formatting decision as well as a throughput one:
| Implementation | Language | Runs in |
|---|---|---|
CPython jinja2 | Python | Transformers, vLLM, SGLang |
minja | C++ | GPT4All; historically llama.cpp, Jan, Docker Model Runner |
common/jinja | C++ | llama.cpp, since January 2026 |
@huggingface/jinja | JavaScript | transformers.js |
minijinja | Rust | Hugging Face text-generation-inference |
That second row's list of users comes from minja's own README, and it is wrong. llama.cpp replaced minja in January 2026, and Jan and Docker Model Runner both vendor llama.cpp, so they inherited the replacement. The README has not been touched since September 2025. Even the engines' own record of who runs them has drifted.
Hugging Face's Python library renders your template through CPython jinja2. Its own Rust
serving stack renders the same template through minijinja.
These are not ports of each other. They are independent reimplementations of a language whose
original implementation, jinja2, last shipped a release in March 2025 and, as of August 2026,
has not shipped another - while four rewrites of it churned. Call the resulting behaviour
difference the parity gap. It is structural, not a bug queue: there is nothing for five
engines to converge on.
You can watch the gap being papered over in public. TGI's router does not just call minijinja, it
installs a compatibility shim - pycompat::unknown_method_callback - to supply the Python string
methods that model templates assume and minijinja does not have. It also needed a dedicated pull
request to add strftime_now, a function Transformers injects and templates in the wild already
called. Support for a template feature is not a property of the template. It is a property of
whichever engine happens to be loading it.
The sharpest case is a filter that simply is not there. minja implements a deliberately small
filter set - count, dictsort, join, tojson, trim and a handful more - and says so
plainly: "Only the ones actually used in templates of major (or trendy) models are/will be
implemented." replace is not among them. So content | replace(a, b) runs on CPython, runs on
llama.cpp's common/jinja (which registers replace as a builtin), and dies on minja. Nothing
in that template is wrong, and nothing you can read in it tells you whether it will run. That
depends entirely on where it lands.
And there is a sixth divergence that is not a Jinja engine at all: Ollama renders chat templates
with Go's text/template. A Hugging Face template cannot run there in any form. It has to be
translated by hand.
flowchart TD
T["one chat_template.jinja<br/>shipped with the weights"] --> P["CPython jinja2"]
T --> M["minja (C++)"]
T --> L["llama.cpp common/jinja"]
T --> J["@huggingface/jinja (JS)"]
T --> R["minijinja (Rust)"]
P --> O["five rendered strings"]
M --> O
L --> O
J --> O
R --> O
O --> Q{"identical?"}
Q -->|"no spec says they must be"| X["divergence, unannounced"]
style T fill:#4A90E2,color:#FFFFFF
style P fill:#98D8C8,color:#2C2C2A
style M fill:#98D8C8,color:#2C2C2A
style L fill:#98D8C8,color:#2C2C2A
style J fill:#98D8C8,color:#2C2C2A
style R fill:#98D8C8,color:#2C2C2A
style O fill:#FFD93D,color:#2C2C2A
style Q fill:#7B68EE,color:#FFFFFF
style X fill:#E74C3C,color:#FFFFFF
llama.cpp's own numbers are the best case against me. When it merged its own engine in January 2026, the pull request reported testing against 370 real templates: 14 failures for the new engine, 8 for minja. Both are measured against CPython's output as the oracle. Someone can fairly say that 96% parity with an enumerated failure list is ordinary engineering, and they would be right about a rendering engine.
They would be wrong about this one. Ask what 96% buys you in each case. For a component that draws a page, the 4% is visible the moment it happens. For this component, the 4% is a model that answers slightly worse and tells nobody. The failures are not comparable, so the same number does not mean the same thing.
The direction of those numbers is worth sitting with too. The replacement engine fails more templates than the thing it replaced, and it was merged anyway. The reasons given were decoupling from a JSON library and adding input marking against special-token injection, both worth having. A serving stack accepted six more broken templates to get them, and the trade barely registered anywhere.
That suite is also not what it appears to be. It is a de facto oracle: a pile of real templates for which CPython's output defines the right answer. It is not a normative specification. Nothing obliges a sixth engine to pass it. Nothing lets a model author declare which semantics their template assumes. Nothing versions those semantics, so a template cannot say what it needs and an engine cannot say what it provides. Add no version pin on the artifact itself, and - as CVE-2026-5760 showed - a path from a downloaded file to code execution.
The counter-argument defends Jinja as a reasonable choice, and on that ground it wins. It has nothing to say about what never followed the choice, which is ownership.
The prompt is not a pure function of your messages
Transformers compiles every template in an ImmutableSandboxedEnvironment and injects a small
set of globals into it. One of them is strftime_now. Llama 3.2's template calls it:
{%- if strftime_now is defined %} {%- set date_string = strftime_now("%d %b %Y") %}{%- else %} {%- set date_string = "26 Jul 2024" %}{%- endif %}That is defined guard is doing more than it looks like. On Transformers you get today's date.
On an engine that never injected strftime_now, you silently get 26 July 2024 - a date from two
years ago, presented to the model as today, with no warning. The template did not change and
neither did the messages. The system prompt did, because a different engine loaded it. This is
the divergence TGI's strftime_now pull request existed to close.
Even on the happy path the consequence stands: a template that calls strftime_now renders a
different string tomorrow than it did today, from an identical message list. Your prompt is not a
pure function of your input. Every cache key derived from it, every golden test written without
pinning the clock, and every "it worked yesterday" reproduction inherits that.
A template can make your prefix cache useless
Two different things get confused here, so separate them. A template edit invalidates every cached prefix once, and then the new shape is stable. That is a one-time flush and it is not very interesting.
The expensive one needs no edit at all. Reasoning models make it concrete: Qwen3-style templates strip historical reasoning blocks from earlier turns, and a positional condition inside the template decides whether an empty block gets emitted in place of the stripped one. Because the condition depends on where a message sits in the list, the same historical turn can render one way at turn 3 and another way at turn 5. The prompt prefix mutates as the conversation grows, with the template unchanged.
vLLM's automatic prefix caching hashes each block together with its parent's hash, so a changed token invalidates that block and every block downstream of it. Reuse survives only up to the earliest affected turn, which is what sets how much cache you keep.
Put numbers on it, and substitute your own where mine are guesses. Say each turn contributes about 200 tokens once you count the question, the answer, and the reasoning. A working cache means turn n pays for only those 200. A prefix that fractures every turn means turn n pays for everything before it too, another 200 x (n - 2) tokens of prefill it should never have needed. Run that to ten turns and the waste lands near 7,000 tokens against a conversation whose real content was 2,000. At a modest 5,000 conversations a day, that is roughly a billion prefill tokens a month buying nothing - every day, not once.
I am not printing a cost. On self-hosted GPUs that billion is throughput you already paid for and did not get; on a hosted endpoint it is a line item at your provider's prefill rate. Either way it arrives with no application commit and no error anywhere.
Rendering has no inverse, so everyone wrote a parser
The template renders a tool call out to the wire format. Nothing in it parses the model's call
back in. So every serving stack wrote that half by hand. vLLM ships dozens of model-specific
tool-call parsers behind --tool-call-parser, roughly one per model family, and the list grows
every time a new family ships. The book calls this the render/parse asymmetry; the parser
registry is the bill for it.
Nobody is extending this design any more
Let me be careful here, because the easy version of this argument is wrong. Hugging Face has not
abandoned chat templates. They promoted the template to its own first-class chat_template.jinja
file, they maintain a JavaScript port of Jinja to run it, and the documentation is actively
edited. Anyone claiming a retraction gets buried by that evidence.
The interesting pattern is narrower, and it holds. When people have had a free hand to design the next piece of this system, none of them has reached for Jinja again.
Transformers v5.0.0 shipped Response Parsing, which runs the round trip backwards: raw output tokens in, structured message out. Parsing was never something Jinja could do - it is a text emission language - so the question was what to build instead. The answer was declarative JSON, and the documentation gives the reason in passing:
Unlike chat templates, we save them inside
tokenizer_config.jsonand not as a separate file, because their format fits naturally in JSON, unlike a chat template Jinja script.
Read narrowly, that sentence is about where a file lives. Read alongside what the same page does next, it is more than that: the portability section opens by addressing "everyone who had to implement an entire Jinja parser to get non-Python chat templating to work." That is a design team writing to the people its previous choice stranded, while choosing data over code for the new layer.
PrimeIntellect went further in May 2026 and skipped the template entirely. Their renderers
project builds the token sequence with a Python program, and their stated reason is that a Jinja
template describes only one direction of the problem - it cannot parse sampled tokens back into
structure, attribute tokens for loss masking, or extend a token stream without re-rendering
history. Their test suite checks token-level parity against apply_chat_template, which is the
tell: they treat Hugging Face's implementation as the oracle, because there is nothing else to
check against.
Three teams, three free hands, and none of them picked Jinja.
The reference documentation reads that way too, on the same page across versions. Transformers
v4.47.1's chat templating page said formatting mismatches had been "haunting the field and
silently harming performance for too long." On main today, the words "silent" and "silently"
do not appear on that page at all. The strongest line left is that with the wrong control tokens
"these models would have drastically worse performance" - true, and quieter. The 2023 blog post
is still live and still titled An End to the Silent Performance Killer, so this is not a
disavowal. It is the difference between what a team says in a launch post and what it leaves in
the reference docs three years on.
No announcement marks any of this. If you have been waiting for something official to tell you this layer needs owning, this is as close as it gets.
The chat template as an attack surface
Security research reached the same layer from a different direction, and it moved fast:
- 2024 - ChatBug (arXiv:2406.12935, AAAI 2025). The template is an exploitable format. Templates bind the model to a rigid structure that the user is free to violate, and prompts that deviate from the expected shape bypass safety alignment on eight tested models.
- February 2026 - inference-time backdoors (arXiv:2602.04653, ICLR 2026 Trustworthy AI
Workshop). The template is an attacker-controlled artifact. Weights unmodified, no training
access, no runtime control: a modified
chat_template.jinjaredistributed in a GGUF file cut triggered factual accuracy from 90% to 15%, produced attacker-chosen URLs at success rates above 80%, and left benign inputs with no measurable degradation. It was evaluated across eighteen models in seven families and four inference engines, and the poisoned artifacts evaded all automated security scans on the largest open-model distribution platform. - April 2026 - TEMPLATEFUZZ (arXiv:2604.12232). Then someone automated the search. A fuzzer mutates the template itself to maximise jailbreak success while holding task accuracy steady, so the attack no longer needs a human with taste.
The backdoor paper puts the structural reason in one sentence:
Chat templates are executable Jinja2 programs invoked at every inference call, occupying a privileged position between user input and model processing.
That privileged position is why this class of attack outperforms prompt injection. An injected prompt arrives as one untrusted message and has to win an argument with your system instruction. A tampered template never has that argument, because it is the code that emits the system instruction in the first place. It does not compete for authority; it issues it. This is silent delegation with an adversary on the other end of it.
People reach for the sandbox as the answer to this. The sandbox is real: Transformers renders
templates inside an ImmutableSandboxedEnvironment, and a template running there cannot open
your files or shell out. The backdoor needs none of that. It needs to return a string, which is
the one thing every template exists to do. The sandbox constrains what a template can touch,
and this threat lives entirely in what a template can say.
Which is why the SGLang fix in the opening was three lines. Getting the sandbox right is easy once somebody is looking at the code that renders the template.
How to take ownership of your chat template: a seven-step checklist
Not "think about templates." Do these, in this order.
- Name the one component that calls
apply_chat_template. Write the name down. If you cannot name exactly one, you have zero (someone is hand-rolling) or two (you are double-templating). Both are broken. - Fingerprint the template you reviewed and fail the build when the hash changes. Check
both
chat_template.jinjaand thechat_templatefield intokenizer_config.json- models use both. - Commit a golden wire format for a fixed canary conversation and assert it byte for byte in continuous integration. Never re-record a golden to turn a build green.
- Run the assertion on the engine that serves production, not only in Python. If you render in Transformers and serve on llama.cpp, your green test is measuring the wrong engine.
- Grep your templates for
strftime_now. If it is there, your prompt is time-dependent and your cache keys and golden tests need to know. - Diff the wire format before and after any template change, and check whether the first differing token is near the front. Front-loaded changes invalidate the prefix cache.
- Treat a template update as a dependency update. Review the diff. A template arriving from a model hub is third-party executable code with a direct line into your system prompt.
Item 4 is the one teams skip, and it is the one the parity gap punishes.
What is inside The Chat Templates Handbook
I spent this year writing the long version.
Buy on Amazon: United States | India
The Chat Templates Handbook is twelve chapters in three parts, and it is a standalone book - Chapter 2 rebuilds the message model, so it does not assume you read The ChatML Handbook first.
Part I - Foundations. The gap between messages and tokens, the message model, the narrow
slice of Jinja that chat templates actually use, and how a template is put together. The goal for
these four chapters is a specific skill: open an unfamiliar model's chat_template.jinja, read
it, and know what string it will hand the model before you run it.
Part II - The Hard Parts. Tool calling, reasoning modes, multimodal content, and writing a template for a model you trained yourself. Each of those arrived as a feature request on a format that started out simple, and together they are why a template that used to fit on a screen now runs to a few hundred lines of control flow.
Part III - Production and Operations. Cross-engine behaviour, debugging a template that is producing the wrong bytes, the security surface, and assembling everything into one tool.
Each chapter contributes a module to Template Studio. Finished, it renders through a real
tokenizer, compares what different engines produce from the same template, flags the patterns
that cause production incidents, and pins wire formats so a change cannot pass unnoticed. MIT
licensed, and the core installs against jinja2 alone if you would rather not pull in
Transformers:
github.com/ranjankumar-gh/template-studio.
Nothing about it requires buying the book.
The August 2026 printing corrected three claims the field falsified between June and August.
Response templates shipped, which closed the render/parse asymmetry the book had described as
open. Qwen3-2507 retired enable_thinking. And llama.cpp replaced minja by writing a fifth
Jinja engine. Two of those could have sunk the argument and did not - the layer moved underneath
everyone's templates, and almost nobody's templates moved.
If the book is useful to you, an Amazon review helps it reach other engineers.
Which component in your stack calls apply_chat_template?
Then answer the second question: what happens on the day that component's output changes?
If the honest answer is "nothing happens, and we would not know," you have found your unowned layer. The fix is a pinned revision, a hash, and a golden file, and it costs an afternoon.
None of this will show up in your changelog. The template will keep rendering, the tests will keep passing, and the only thing that will tell you the layer moved underneath you is a hash you decided to write down.
References
- Carrigan, M. (2023, October 3). Chat Templates: An End to the Silent Performance Killer. Hugging Face Blog. https://huggingface.co/blog/chat-templates
- Hugging Face. Chat templates. Transformers documentation. https://huggingface.co/docs/transformers/main/en/chat_templating
- Hugging Face. Chat templates, Transformers v4.47.1 documentation (for the superseded wording). https://huggingface.co/docs/transformers/v4.47.1/en/chat_templating
- Hugging Face. Response Parsing. Transformers documentation (introduced in the v5 line). https://huggingface.co/docs/transformers/main/en/chat_response_parsing
- Hugging Face.
chat_template_utils.py. Transformers source. https://github.com/huggingface/transformers/blob/main/src/transformers/utils/chat_template_utils.py - Hugging Face.
router/src/infer/chat_template.rs. text-generation-inference source (minijinja +pycompatshim). https://github.com/huggingface/text-generation-inference/blob/main/router/src/infer/chat_template.rs - Bartolome, A. Add
strftime_nowcallable function forminijinjachat templates (PR #2983). text-generation-inference. https://github.com/huggingface/text-generation-inference/pull/2983 - CERT Coordination Center. (2026, April 20). VU#915947: SGLang is vulnerable to remote code execution when rendering chat templates from a model file (CVE-2026-5760). https://www.kb.cert.org/vuls/id/915947
- National Vulnerability Database. CVE-2026-5760 (CVSS 3.1 base 9.8,
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H). https://nvd.nist.gov/vuln/detail/CVE-2026-5760 - sgl-project. (2026, April 29). Fix for CVE-2026-5760 (PR #23660). SGLang. https://github.com/sgl-project/sglang/pull/23660
- Fogel, A., Hofman, O., Cohen, E., & Vainshtein, R. (2026). Inference-Time Backdoors via Chat Templates: From LLM Supply Chains to Agentic System Compromise. arXiv:2602.04653. ICLR 2026 Trustworthy AI Workshop. https://arxiv.org/abs/2602.04653
- Jiang, F., Xu, Z., Niu, L., Lin, B. Y., & Poovendran, R. (2024). ChatBug: A Common Vulnerability of Aligned LLMs Induced by Chat Templates. arXiv:2406.12935. AAAI 2025. https://arxiv.org/abs/2406.12935
- Shen, Q., Xiao, Z., Huang, L., Hu, E., Tian, Y., & Chen, J. (2026). TEMPLATEFUZZ: Fine-Grained Chat Template Fuzzing for Jailbreaking and Red Teaming LLMs. arXiv:2604.12232. https://arxiv.org/abs/2604.12232
- ggml-org. (2026, January 16). Implement new jinja template engine (PR #18462). llama.cpp. https://github.com/ggml-org/llama.cpp/pull/18462
- minja: A minimalistic C++ Jinja templating engine for LLM chat templates (supported-filter list; repo last updated September 2025). https://github.com/google/minja
- mitsuhiko.
minijinja/src/filters.rs. minijinja source. https://github.com/mitsuhiko/minijinja/blob/main/minijinja/src/filters.rs - Hugging Face.
@huggingface/jinja. https://github.com/huggingface/huggingface.js/tree/main/packages/jinja - Ollama. Template. https://ollama.readthedocs.io/en/template/
- Meta. Llama-3.2-3B-Instruct chat template (
strftime_nowwith a 2024 fallback). https://huggingface.co/unsloth/Llama-3.2-3B-Instruct - Qwen Team. Qwen3-235B-A22B-Thinking-2507 model card. https://huggingface.co/Qwen/Qwen3-235B-A22B-Thinking-2507
- Python Packaging Index. Jinja2 release history (3.1.6, 5 March 2025; checked 14 August 2026). https://pypi.org/project/Jinja2/#history
- vLLM. Automatic Prefix Caching. https://docs.vllm.ai/en/latest/features/automatic_prefix_caching.html
- vLLM. Tool Parsers API. https://docs.vllm.ai/en/stable/api/vllm/tool_parsers/
- PrimeIntellect. (2026, May 12). Renderers: Programmable chat templates for LLM training and inference. https://www.primeintellect.ai/blog/renderers
- Kumar, R. (2026). The Chat Templates Handbook: A Developer's Guide to Jinja, apply_chat_template, and Rendering Model-Ready Prompts. https://www.amazon.com/dp/B0H6STBYWT
Related Articles
- The ChatML Handbook, Second Edition: What Won and What Fragmented
- Provenance in AI: Why It Matters for AI Engineers - Part 1
- When Models Stand Between Us and the Web: The Future of the Internet in the Age of Generative AI
- ChatML: What It Is, Why OpenAI Removed It, What Replaced It



