Same Weights, Different Model: Chat-Template Drift and What It Breaks
Why the most-changed file in open-model re-releases is the one nobody diffs — and the concrete business failures it causes downstream.
Status: synthesis report, 2026-08-21. Census numbers come from two runs:
the 296-model research census (2026-08-17, REPORT.md) and the
1,000-model production census (2026-08-20, every result public on its
ingot.tools model page). External incidents cite public
issue trackers. Not a safety certification of any model.
TL;DR
The chat template is the small piece of Jinja code that turns a conversation into the token stream a model actually sees. It carries the default system prompt, the thinking-mode markers, the tool-call format, and often the model's stated identity. It is behavior, shipped as config.
And it churns:
- 67% of the 1,000 most-downloaded derivatives of 12 popular base models (668/1,000) changed the parent's chat template or ship none at all (production census, 2026-08-20: 816 warn, 184 pass, 0 fail).
- In the deeper 296-model research census, 56% differ from their claimed parent and 37% never mention it in the README.
- Among pure quantization re-releases — where users assume behavior-identical weights — 78% (25/32) changed or dropped the template.
Weight checksums don't catch any of this. The weights can be byte-identical to the parent while the template swaps the system prompt, rebrands the model, or breaks tool calling. Same weights with a different template is a different model — and the failure modes it causes are the silent kind: nothing crashes, the pipeline keeps shipping wrong output.
What lives in a chat template
A chat template is not formatting trivia. In current open models it controls:
| what | consequence when it changes |
|---|---|
| default system prompt | model persona, refusal posture, disclosure behavior |
| identity strings | who the model says it is ("You are X, made by Y") |
thinking-mode markers (<think>…</think>) |
whether reasoning is parsed out or leaks into user-visible output |
| tool-call serialization | whether agent frameworks get a tool_calls object or unparseable text |
| role handling (system folding, alternation rules) | silent prompt corruption on multi-turn conversations |
| generation-prompt / stop handling | truncation, runaway generation, empty turns |
Every row is a behavior contract. None of it is covered by a weight hash.
The data: template drift is the norm, not the exception
Production census (2026-08-20). We ran the 1,000 most-downloaded derivatives of 12 popular base models through the production scan pipeline: 668/1,000 (67%) either ship a template that differs from their claimed parent or ship no template at all. 816 models warn, 184 pass, 0 hard-fail. Every individual result is browsable on its public model page.
Research census (296 models, 3 parents). The deeper pass adds the disclosure cross-check:
| signal | rate |
|---|---|
| chat template differs from parent | 166 / 296 (56%) |
| ...undisclosed (README never mentions template/prompt format) | 110 / 296 (37%) |
| special/added tokens changed | 161 / 296 (54%) |
| pure quantization re-releases altering/dropping the template | 25 / 32 (78%) |
The 78% row is the one that should worry anyone deploying quantized models: repos whose only claim is "quantized" — where the entire value proposition is "identical behavior, smaller memory" — routinely ship a different or missing template than the parent they quantized.
What the edits actually are. In this sample, no template injection attacks (hidden instructions or URLs) — the edits were rebrands, default system-prompt swaps, and tool-format changes. Representative cases:
- AvitoTech/avibe (116k downloads) — labeled a plain "finetune" of Qwen3-8B; the template silently gained tool-calling machinery (alongside an undisclosed vocabulary swap, 151,936 → 116,394).
- FermionResearch/Neutrino-8B — the template rebrands Qwen3-8B as "You are Neutrino-1, made by Fermion Research."
- zeroentropy/zerank-2-reranker (598k downloads) — template replaced entirely, undocumented.
- Efficient-Large-Model/Fast_dLLM_v2_7B, HuatuoGPT-o1, EphAsad/Atem-4B — silent default-system-prompt swaps and identity rebrands inside templates.
And drift is not only derivative-vs-parent — upstreams mutate templates in
place. The DeepSeek-R1-Distill-Qwen-32B repo changed its chat template and
downstream vLLM deployments started emitting </think> with no opening
<think>, leaving reasoning_content=None
(vllm#12999). A model you
scanned last month is not attested this month.
The business problems
Each of these is a documented failure class, not a hypothetical. The common shape: the system keeps returning 200s while behaving differently than what you evaluated. Alongside each one we sketch what it looks like from inside a company — the incident classes are documented; the framing scenarios are illustrative.
1. Your evaluation no longer covers what you deployed
The standard workflow — evaluate the base model, deploy the FP8/AWQ/GGUF re-release — assumes quantization preserves behavior. With 78% of pure quantization re-releases altering or dropping the template, the deployed artifact frequently has a different default system prompt, different thinking-mode handling, or different tool-call format than the thing you benchmarked and red-teamed. Any safety attestation, bias audit, or capability benchmark you inherited from the parent is silently invalidated — while every weight checksum still passes.
What it looks like: a fintech team spends three weeks red-teaming the BF16 base model — refusal battery, jailbreak suite, PII probes — then deploys the FP8 re-release because that's what fits the GPUs. The quantizer's repo dropped the template (the most common drift we found), so the serving stack falls back to a generic one: no default system prompt, no thinking-mode delimiters. The refusal posture they certified was partly in the template they are no longer running. Six months later an auditor asks which artifact the red-team report covers, and the honest answer is: a different model. Nothing in their deployment pipeline — image digests, weight checksums, config review — was positioned to catch it.
2. Agent and tool-calling pipelines break without erroring
Tool calls live or die on the template. Public incident record:
- Small template/system-prompt changes decided whether Phi-4-mini produced a
parseable
tool_callsobject or malformed text — in one case the model invented a tool result in the same response (ollama#9437, ollama#9802). - Tool-call templates across DeepSeek, Gemma, gpt-oss, Llama, Qwen, GLM, and
MiniMax disagree on whether
argumentsis a dict or a JSON string; some combinations silently double-escape or miscompile the call (transformers#45419). - The Hugging Face gpt-oss template omitted Harmony's
<|constrain|>jsonmarker when reconstructing tool history (harmony#91).
The business translation: an agent that stops calling tools doesn't crash — it degrades into a chatbot that narrates what it would have done. Orders don't get placed, tickets don't get filed, and the failure surfaces as customer complaints, not alerts.
What it looks like: a customer-support agent is wired to a cancel_order
tool. After a model refresh pulls a re-release whose template serializes tool
calls differently, the framework stops receiving tool_calls objects — the
model still emits the call, but as plain text the parser ignores. The agent
then does what the Phi-4-mini incident showed models will do: it fabricates
the happy path, telling the customer "I've cancelled that order and issued
your refund" while no tool ever executed. Every response is HTTP 200. The
failure is discovered weeks later as a pattern in chargebacks and "you told me
this was refunded" escalations — by support staff, not monitoring. The
mechanics can be as small as one line:
- {{ '{"name": "' + tool.name + '", "arguments": ' + tool.arguments | tojson + '}' }}
+ {{ '{"name": "' + tool.name + '", "arguments": "' + tool.arguments | string + '"}' }}
— arguments as a dict versus a double-escaped string, the exact
disagreement transformers#45419 documents across seven model families. A
JSON-schema validator downstream sees "valid JSON, wrong shape" and drops it.
3. Private reasoning leaks — or answers get eaten
Thinking-mode markers are template content. When the template and the serving
parser disagree, one of two things happens: chain-of-thought spills into
customer-visible content (a trust and confidentiality incident — vendors
explicitly say CoT is not intended for end users), or the parser eats the real
answer along with the markers (silent data loss). The R1-distill incident
above is exactly a template change producing the second failure.
What it looks like: the leak direction — a healthcare triage assistant's
reasoning model deliberates before answering. The template update shifts where
<think> opens, the parser stops stripping the block, and the patient-facing
reply now begins with the model's raw deliberation: "The user is likely
anxious; symptoms could also indicate something more serious but I shouldn't
lead with that…" — speculation and hedging that was never supposed to leave
the server, now in a patient's chat history and in the audit trail. The eaten
direction is quieter: the parser consumes the reply along with the markers and
the assistant returns empty strings. Users retry, get nothing, and churn; the
dashboard shows normal latency and zero errors, because as far as the serving
stack is concerned, nothing failed.
4. Identity and system-prompt hijack — compliance and brand risk
A derivative's template deciding "You are Neutrino-1, made by Fermion Research" means the model's self-description, disclosure behavior, and default instructions were replaced by a third party — undisclosed. If your product promises a specific system prompt (compliance disclaimers, medical/financial hedging, PII handling rules), a template swap removes it without touching a single weight. 37% of drifted models in our census never mention the change anywhere.
What it looks like: the entire failure can be one default in the Jinja:
{%- if messages[0]['role'] != 'system' %}
- {{- '<|im_start|>system\nYou are a helpful assistant. You are not a
- licensed advisor; recommend consulting a professional for financial
- decisions.<|im_end|>\n' }}
+ {{- '<|im_start|>system\nYou are Atlas, an expert financial advisor.
+ Answer decisively.<|im_end|>\n' }}
{%- endif %}
Every API call that doesn't pass its own system message — which in most products is all of them, because the system prompt was assumed to be the model's default — now gets confident, unhedged financial advice from a bot that introduces itself under someone else's brand. The compliance language your lawyers signed off on didn't fail; it was never rendered. And when a customer screenshots the bot claiming to be "Atlas," your white-label story has a third party's identity embedded in it, exactly as Neutrino-8B embeds Fermion's inside repackaged Qwen weights.
5. Irreproducible bugs and misattributed regressions
When the template differs between dev (Transformers, reads the repo template),
staging (vLLM, maybe a --chat-template override), and prod (GGUF with the
template embedded in the file header), the same weights behave three ways.
Bug reports don't reproduce; regressions get blamed on the model or the
serving stack when the actual variable was the template. This burns
engineering time in the most expensive way — cross-team attribution hunts —
and it's why our recommended deployment identity pins a chat-template hash
alongside the weight digest and tokenizer revision.
What it looks like: QA reports the model "ignores instructions in prod but
not locally." Three teams spend a week on it: ML blames the quantization,
infra blames the serving upgrade, the app team bisects prompt changes. Nobody
suspects the template because nobody diffs it — dev renders the repo's
chat_template.jinja through Transformers, prod runs a GGUF whose embedded
copy predates a fix. The Gemma benchmark scare is the same trap from the other
side: a reported vLLM accuracy regression (GSM8K 0.862 vs 0.923) turned out to
be the eval harness mis-setting a single BOS token — prompt-rendering, not
kernels — after the finger-pointing had already started. A template SHA in the
bug ticket would have closed either investigation in an afternoon.
6. Yesterday's pass is not today's pass
Templates are mutable files in mutable repos. Upstreams fix and break them in
place; Hugging Face moved templates from tokenizer_config.json into
standalone chat_template.jinja files, so even the location churns. A
point-in-time scan is necessary but not sufficient — without re-scanning on
new revisions and alerting on template-hash change, an attestation decays
silently. (This is the monitoring gap Ingot's CI gate and re-scan pipeline
exist to close.)
What it looks like: procurement approves a model in March after a full
review. The deploy pins nothing — the Dockerfile pulls
huggingface.co/vendor/model at main, like most do. In May the upstream
"fixes" its template: new default system prompt, adjusted thinking-mode
markers (the R1-distill change was exactly this kind of in-place edit). June's
routine image rebuild silently absorbs it. No deploy of your code happened,
no dependency bumped, no alert fired — but the model in production is no
longer the one procurement reviewed, and the first symptom is a behavior
regression nobody can correlate with a change, because from inside the
company, nothing changed.
What to do about it
- Pin the template as part of the deployment identity. Weight digest + tokenizer revision + chat-template SHA + server version. If any of the four changes, your evaluation is stale. Ingot records the template hash in every model fingerprint.
- Diff against the claimed parent before deploying a derivative. This is
ingot scan: template drift vs. parent is a first-class finding, including for quantized re-releases ("same model, smaller" is a claim — check it). - Restore the parent template when drift is unintentional. Template drift
is the one finding class that is mechanically fixable:
ingot patchemits a pinned, content-hashed merge op restoring the parent's template. (Intentional drift — an instruct tune of a base model — is detected and left alone.) - Gate CI on template-hash change. A template edit in a model repo should be a reviewed diff, exactly like a code change, because it is one.
- Golden-prompt the contract, not just the weights. A short deterministic suite — tool call with nested args, zero-arg tool call, thinking-mode round-trip, system-prompt echo — catches template/parser disagreement that no static diff can.
Limitations
- The two censuses skew toward the most-downloaded derivatives, which over-samples corporate quantizers and under-samples the long tail; drift rates in the tail are unknown (plausibly worse, unverified).
- GGUF repos often "drop" the config-file template because it's embedded in the GGUF header; the census counts these as removed. Reading the embedded template is a known follow-up, and cuts both ways — some counted drops are really moves, and some GGUF-embedded templates drift undetectably today.
- External incidents are user-reported public issues; several were fixed in later versions. They establish the failure class, not current prevalence.
- The "what it looks like" scenarios are illustrative composites built on those documented incidents and census findings — the mechanisms are real and cited; the companies are not.
- "No injection attacks found" means none in these samples under our string-level checks — not that the vector doesn't exist. It's the cheapest place to hide an instruction; that's an argument for scanning, not comfort.
Sources
Census method and raw evidence: REPORT.md (stage A) and
out/FINDINGS.md; production census results are public per-model at
ingot.tools. External incidents are linked inline; deployment-quirk survey in
research/open-model-deployment-quirks.md.