vllm-chat-templates — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited vllm-chat-templates (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Target audience: operators deploying vLLM in production. Assumes OpenAI-API-compatible frontend (/v1/chat/completions or /v1/responses), multi-GPU, mid-2024 through 2026 model families.
Chat-template bugs span three layers. This skill owns the prompt-rendering layer; route to a sibling when the problem is output-side.
| Layer | Direction | Skill | What it covers |
|---|---|---|---|
| Chat template (Jinja) | messages → prompt string | this skill | template precedence, --chat-template, chat_template_kwargs, examples/tool_chat_template_*.jinja, bundled fallbacks |
| Reasoning parser | model output → reasoning_content + content | vllm-reasoning-parsers | --reasoning-parser, extract_reasoning, is_reasoning_end, <think> splitting |
| Tool parser | model output → tool_calls[] | vllm-tool-parsers | --tool-call-parser, streaming state machines, partial-JSON parsing |
When in doubt: if the operator complains about what the server receives, it's this skill; if about what the client receives, it's one of the parser skills. Template and parser bugs often present the same symptom ("reasoning_content is null"), so all three skills name each other in diagnostics. This file stays on the Jinja side.
Chat templates are the Jinja layer between structured messages and the raw prompt string the model sees. Tool-calling and reasoning models are extremely sensitive to this layer — a one-character whitespace difference in a template can send tool-call success rate from 99% to 0%, silently.
The hardest class of bug in vLLM deployments is template silent failure: the server starts, requests return 200s, but the model emits garbled tool calls, or reasoning_content is null, or enable_thinking=false has no effect. No exception, no log line, just wrong outputs. Operators often chase parser bugs when the real fix is a template override (or vice versa — override a template when the real fix is a parser flag).
This skill's job: given a symptom, identify whether it's a template, a parser, a kwarg, or an API-endpoint issue, and point at the known fix.
vLLM picks a chat template in this order. First hit wins. Source: resolve_chat_template() in vllm/renderers/hf.py.
.jinja file, literal Jinja string, or a named template ("tool_use") that the tokenizer knows.tools=None. Multimodal models often expose their template via the processor, not the tokenizer.tokenizer_config.json..jinja files: blip-2, clip, colpali, deepseek_ocr, deepseek_ocr2, unlimited-ocr, unlimited_ocr, deepseek_vl_v2, minicpmv, minicpmv4_6, minicpmv4_7, paligemma, siglip, siglip2. No fuyu (model dropped in v0.26.0) and no qwen/Qwen-VL entry — a Qwen checkpoint with its template stripped has no fallback. Source: _MODEL_TYPE_TO_CHAT_TEMPLATE_FALLBACK in chat_templates/registry.py.If none resolves, `ChatTemplateResolutionError` raised by safe_apply_chat_template() with: "As of transformers v4.44, default chat template is no longer allowed, so you must provide a chat template if the tokenizer does not define one."
Do not "modernize" that v4.44 — it is vLLM's verbatim error string, not a version requirement. The realtransformerspin and the template-name security floor (below v0.32.0, pintransformersexplicitly):references/debugging.md§ Step 4.
validate_chat_template() in vllm/renderers/chat_utils.py; vllm/entrypoints/chat_utils.py before v0.32.0). A bare name is also looked up in the bundled chat_templates/ dir. A value containing `{`, `}` or a newline skips that check and, if it does not open, is used as literal Jinja (_load_chat_template()) — a mangled Helm/env-interpolated value renders silently. examples/… resolves against the process cwd: vllm/vllm-openai has it at /vllm-workspace/examples (its WORKDIR); the -nonroot image's WORKDIR is /home/vllm; pip wheels ship no examples/. Every launch recipe that passes `--chat-template` must tell the operator to `ls` the path inside the serving container and to confirm the logged prompt matches that file.tools, vLLM skips it and falls through to tokenizer. This is intentional — tool templates must be explicit. Without --chat-template, image-only and image+tools requests to the same model render with two different templates — a smoke test must cover both shapes; an explicit --chat-template pins one template for both.TimeoutError: Chat template rendering timed out after 30.0s — raise VLLM_CHAT_TEMPLATE_RENDER_TIMEOUT (seconds, default 30; <= 0 disables) for a legitimately huge prompt. Rust frontend: fixed MiniJinja fuel budget (500,000 instructions), no knob. /v1/responses now refuses chat_template inside chat_template_kwargs unless --trust-request-chat-template is set, as chat-completions already did.Use this table first. Load references/model-families.md for the specific model bug + flags.
| Symptom | First suspect | Second suspect |
|---|---|---|
reasoning_content is null or missing from response | --reasoning-parser flag missing/wrong, or client reads reasoning_content off a response that names it reasoning (pattern #15) | Template prefills <think> (DeepSeek-R1) — a cause only on vLLM ≤ v0.7.2 (pattern #3) |
tool_calls empty array [] but model emitted text | --tool-call-parser wrong for model family | tool_choice="auto" (try "required" or a named tool) |
Tool calls have malformed JSON / arguments empty | Wrong tool-call parser (Hermes vs pythonic vs XML vs model-specific) | Template doesn't inject tools correctly |
enable_thinking=False has no effect | Kwarg dropped by vLLM allowlist (need PR #27622 or ≥v0.11.1) | Downstream harness not forwarding chat_template_kwargs |
Tool calling works on /v1/completions but not /v1/chat/completions | Chat template doesn't render tool result (GLM-5.1 #39611, fixed 2026-04-12) | Endpoint-specific parser path |
| Prompt in server log doesn't match model card | Wrong template resolving — check precedence order above | --chat-template pointing at file that doesn't exist (silent for some tokenizer modes) |
<think> content leaks into final output | --reasoning-parser missing | skip_special_tokens=true stripping control tokens before parser (#38855) |
Garbled prompt with repeated <image> placeholders | Template has placeholder AND multimodal parser adds one | interleave_strings mismatch — see _get_full_multimodal_text_prompt() in renderers/chat_utils.py |
IndexError / Jinja exception on multi-turn tool calls | Tool-call ID format regex too strict (Kimi-K2, Mistral) | Historical tool_calls=[] in assistant msg crashes template (Llama #13978) |
| Server starts, first request works, subsequent requests fail | State-ful template bug: leading whitespace accumulating (DeepSeek V3.1 #28804) | Concurrent-request regression (Gemma-4 <pad> tokens #39392) |
| "this model is reasoning but reasoning_effort=none" | gpt-oss #37909 — "none" not honored | Wrong reasoning parser |
| Works on SGLang / TGI, broken on vLLM | vLLM applies template differently (HF precedence + kwarg allowlist) | Check --tokenizer-mode differences |
Template, parser (output), and kwarg (chat_template_kwargs) are orthogonal layers — diagnose one at a time, don't change multiple at once.
Per-family flag matrix lives in references/flags-matrix.md. Quick cheat-sheet for the production-critical ones:
| Model family | --chat-template | --tool-call-parser | --reasoning-parser |
|---|---|---|---|
| Qwen3 dense / MoE | tokenizer default | hermes | qwen3 |
| Qwen3-Coder (480B-A35B, 30B-A3B) | examples/tool_chat_template_qwen3coder.jinja | qwen3_xml — not hermes: emits XML <function=…><parameter=…>, hermes drops it → tool_calls=[] | — |
| DeepSeek-R1 | examples/tool_chat_template_deepseekr1.jinja | deepseek_v3 | deepseek_r1 |
| DeepSeek-V3-0324 | examples/tool_chat_template_deepseekv3.jinja | deepseek_v3 | — |
| DeepSeek-V3.1 | examples/tool_chat_template_deepseekv31.jinja | deepseek_v31 | deepseek_r1 |
| DeepSeek-V3.2 | (uses --tokenizer-mode deepseek_v32) | deepseek_v32 | deepseek_r1 |
| GPT-OSS 20B/120B | prefer /v1/responses endpoint | openai (broken on chat-completions #22578) | harmony-native |
| Kimi-K2 | tokenizer default (post-fix commit) | kimi_k2 | kimi_k2 |
| Llama-4 Maverick/Scout | examples/tool_chat_template_llama4_pythonic.jinja | llama4_pythonic | — |
| Mistral (HF format) | examples/tool_chat_template_mistral_parallel.jinja | mistral | — |
Mistral (mistral tokenizer-mode) | ignored (don't set) | mistral | — |
| Gemma-3 | examples/tool_chat_template_gemma3_pythonic.jinja | pythonic | — |
| Gemma-4 | examples/tool_chat_template_gemma4.jinja | gemma4 (flaky concurrent) | gemma4 |
| Phi-4-mini | examples/tool_chat_template_phi4_mini.jinja | llama3_json | — |
| Phi-4-reasoning | tokenizer default | — | not deepseek_r1 (#18141) |
| Hermes / Hunyuan / Granite / Toolace / XLAM | examples/tool_chat_template_<name>.jinja | hermes/matching | — |
Commit-gated models (Kimi-K2) require minimum HF revision — check references/model-families.md before recommending.
Consult this list for any "broken chat template" symptom. Most reports reduce to one of these.
--tokenizer-mode mistral ignores --chat-template with a warning only (#25401); gpt-oss ignores it by design (#23015). Symptom: edit template, nothing changes. Fix: Mistral → HF tokenizer mode, or accept the built-in Mistral template as canonical; gpt-oss → /v1/responses.extra_body={"skip_special_tokens": false} per request. Gemma-4 (#38855) also needs a re-pulled HF template, not only a vLLM upgrade — §Gemma-4 in model-families.md.<think>\n (HF #144). This nulled reasoning_content only on vLLM ≤ v0.7.2 (#12999). Since v0.7.3 (#13025) the `deepseek_r1` parser (and every `BaseThinkingReasoningParser`) treats all text before `</think>` as reasoning when `<think>` is absent, so on current vLLM the prefill never empties the reasoning field. Reasoning null and chain-of-thought inside content on ≥ v0.7.3 means the parser did not split that response: confirm --reasoning-parser reached the server (startup log, wrapper/Helm args), capture raw output (debugging step 3), then work in vllm-reasoning-parsers. Do not swap --chat-template for this — the parser already tolerates the prefill either way. Only on ≤ v0.7.2: upgrade, or prepend <think> client-side before parsing.message.content is at risk. Fix: updated chat template or vLLM version.functions.X:idx exactly. vLLM generates longer IDs → IndexError → all tool calls silently dropped. Fix: Kimi-K2 fixed in PR #27565; Mistral template truncates to last 9 digits.**kwargs forwarding for security; tokenizers relying on non-standard params broke. Fix: PR #27622 whitelists standard HF params — requires vLLM ≥ v0.11.1.\n escape sequences double-converted when shell-interpolated. Fix: pass the file path, not the contents; don't shell-escape.tool_calls before rendering (vllm/renderers/chat_utils.py).auto = free-form generation + regex parse (schema-free, brittle). required/named = structured-outputs FSM (schema-valid). Many "tool calling is broken" reports vanish when switching to required."low".--max-num-seqs 1 only as a last resort (it throttles the whole server)..name sends a {"type":"text",...} tool result to its else branch and renders <tools></tools> instead of the result (GLM-5.1 #39614, fixed upstream). Fix: upgrade, or pass --chat-template-content-format openai/string./v1/responses (#22578, permanent). Tool results ignored on chat-completions but honoured on /v1/completions → the template drops the tool-result message (GLM-5.1 #39611, fixed upstream).reasoning, not reasoning_content — at v0.32.0: ChatMessage.reasoning (chat_completion/protocol.py), DeltaMessage.reasoning (generate/base/protocol.py). The request side still accepts reasoning_content and normalizes it (RFC #27755). A client reading reasoning_content off a response sees null every time even when the parser ran perfectly — this looks exactly like a parser failure. Check with jq '.choices[0].message | keys' before debugging parsers._detect_content_format() (vllm/renderers/hf.py) looks for a for item in message['content'] loop; before v0.30.0 (#53824) a template that reaches message.content only through a macro parameter was detected as string, silently flattening multimodal content. Fix: v0.30.0+, or pass --chat-template-content-format openai.chat_template_kwargs acceptsThreaded as extra_body={"chat_template_kwargs": {...}} in OpenAI client. Source: ChatParams (vllm/renderers/params.py), resolve_chat_template_kwargs() (hf.py).
_resolve_chat_template_kwargs()).apply_chat_template signature (_get_hf_base_chat_template_params()).add_generation_prompt (bool)continue_final_message (bool) — mutually exclusive with add_generation_prompt=True (enforced in chat_completion/protocol.py). Frontend-dependent: almost no HF chat template actually reads this variable, so on the Python renderer it is close to a no-op. The Rust frontend implements Transformers-v5 semantics properly (#47844, v0.26.0) — it appends a sentinel to the last message and strips it from the rendered output, so the template never needs to know. The Rust Harmony renderer rejects it outright ("Harmony renderer does not support continue_final_message"). If it silently does nothing, check which frontend serves the request before editing the template. v0.30.0+: the Rust frontend also parses {% generation %}...{% endgeneration %} blocks (rust/src/chat/src/renderer/hf/generation.rs, #56378), rendering only their text — templates that mark assistant spans this way no longer fail there. return_assistant_tokens_mask on /render and the assistant_tokens_mask response field were removed in v0.31.0 (#57520); do not send them.documents (list of {title, contents}) for RAG templatesenable_thinking (Qwen3 and some others)chat_template, tokenize.enable_thinking doesn't appear in the detected Jinja vars, it is dropped with no warning and no error. Confirm the kwarg reached the template: references/debugging.md § Verifying the kwarg actually reached the template.Merging: server defaults (--default-chat-template-kwargs) ← request kwargs via ChatParams.with_defaults() / merge_kwargs() — request wins.
<think> tags go going in to the model, and how tool definitions get rendered.vllm-reasoning-parsers) extracts <think> content from output into reasoning_content. On vLLM ≥ v0.7.3 no template prefill nulls reasoning_content (pattern #3); every current root cause lives in the parser skill.vllm-tool-parsers) extracts tool_calls[] from output. Template-side causes of empty tool_calls are wrong tool template or tool_choice="auto" vs "required" (pattern #9); parser-selection and streaming state-machine debugging live in that skill.Raw-output capture (--enable-log-requests + no --tool-call-parser/--reasoning-parser) determines which layer is at fault: if the model emits the expected tags/JSON, the prompt is fine and the problem is parser-side; if not, the template is misrendering.
Models use model-specific placeholders (<image>, <|image_start|>, etc.) — vLLM's internal placeholder is <##IMAGE##> / <##AUDIO##> / <##VIDEO##> (renderers/chat_utils.py).
_get_full_multimodal_text_prompt()). Some models tolerate this; others produce garbage.ValueError: "Found more '<image>' placeholders in input prompt than actual multimodal data items."interleave_strings=True (via multimodal_config.interleave_mm_strings) enables in-order substitution; default is prepend-all.add_special_tokens=True; a template that emits {{ bos_token }} (Gemma-3, bundled deepseek_vl2/deepseek_ocr templates) then got a second BOS from the tokenizer. LLM._preprocess_chat() (vllm/entrypoints/common/offline.py at v0.32.0; offline_utils.py at v0.30.0) now defaults add_special_tokens=False for chat, matching ChatCompletionRequest on the online server; pass tokenization_kwargs={"add_special_tokens": True} to opt back in. LLM.generate() on raw prompts is unaffected — the processor default still applies there.For Pixtral, Mistral-Small-3.1 multimodal, and other MM-tool-calling combinations, see references/model-families.md.
Load references/debugging.md. The short version:
--enable-log-requests + VLLM_LOGGING_LEVEL=DEBUG (or --log-level DEBUG, v0.31.0+); prompts are logged only at DEBUG. --disable-log-requests was removed in v0.17.0 and fails to parse. Compare against model card example. If they differ, template is wrong.tokenizer.apply_chat_template(messages, add_generation_prompt=True), compare. If same bad output, template is wrong. If different, vLLM's kwarg forwarding is eating something.--tool-call-parser to none, read what the model actually emits. Then work backward to pick the right parser.vllm --version, transformers.__version__, model revision SHA. Many template bugs track a specific tokenizer_config.json commit — mention in bug reports.Grep the symbol; line anchors per release are in references/sources.md.
vllm/renderers/chat_utils.py — message parsing, multimodal placeholder injection, ChatTemplateResolutionError, validate_chat_template(). Moved from `vllm/entrypoints/chat_utils.py` in v0.32.0 (#60522).vllm/renderers/hf.py — resolve_chat_template() (precedence), safe_apply_chat_template() (error surface), resolve_chat_template_kwargs() (allowlist), resolve_chat_template_content_format(), _render_with_timeout() (VLLM_CHAT_TEMPLATE_RENDER_TIMEOUT).vllm/renderers/params.py — ChatParams dataclass.vllm/reasoning/__init__.py — registered reasoning parsers (38 names at v0.32.0).vllm/transformers_utils/chat_templates/registry.py — _MODEL_TYPE_TO_CHAT_TEMPLATE_FALLBACK, get_chat_template_fallback_path().vllm/entrypoints/openai/chat_completion/serving.py — parser instantiation (serving_chat.py no longer exists).examples/tool_chat_template_*.jinja (27 files at v0.30.0–v0.32.0) — ready-made tool templates per model family.When the operator names a model family, load references/model-families.md — it has per-family flag recipes, minimum commit SHAs, known bugs with issue numbers, and recovery strategies.
references/model-families.md — Qwen3, DeepSeek, GPT-OSS, Kimi-K2, Llama-4, Mistral, Gemma, Phi-4, GLM, Hermes/Granite/Hunyuan — with inline issue URLs.references/debugging.md — stepwise debugging workflow + full error-message catalog.references/flags-matrix.md — quick CLI-flag lookup per model.references/sources.md — verification log for external URLs/issues/PRs cited in this skill, with Last verified dates.Last verified: 2026-10-18 against vLLM v0.32.0 (see references/sources.md for per-ref status).
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.