mlxapache-2.0

froggeric/Qwen-Fixed-Chat-Templates

huggingface.co/froggeric/Qwen-Fixed-Chat-Templates

1,664Likes
0Downloads
2026-09-04Updated
mlxjinjachat-templateqwenqwen3.5qwen3.6qwen3.8lm-studiollama.cppvllmtool-callingthinkinglicense:apache-2.0region:us

Model card

Fixed jinja chat templates for Qwen 3.5, 3.6 & 3.8 (v22.5)

This is a universal drop-in Jinja template that fixes rendering errors, KV cache invalidation, token waste, empty think poisoning, and fatal agentic stalling across official Qwen chat templates.

It works across LM Studio, llama.cpp, vLLM, SGLang, MLX, oMLX, KoboldCPP, and any engine that supports Hugging Face Jinja templates. You only need the single chat_template.jinja file at the root of this repository for all Qwen 3.5, 3.6, 3.8, and 3.8 Flash-Next model sizes.


What is New with Qwen 3.8 (v22 / v22.1 / v22.2 / v22.3 / v22.4)

Qwen 3.8 introduces prompt-steered reasoning depth and new model architectures (Qwen3.8-2.4T-A95B, Qwen3.8-27B). While the official template added reasoning effort steering, it also introduced several lockdowns, regressions, and syntax crashes.

The v22 generation delivers full Qwen 3.8 support with all official bugs resolved:

| Feature / Fix | Official Qwen 3.8 Template | This Template (v22.5) | |---|---|---| | Default Reasoning Baseline | Hardcodes xhigh by default, often exhausting the token budget on reasoning before generating answers. | Defaults to medium (zero injected tokens), preserving KV cache parity with v21 and preventing empty-content timeouts. | | Non-Reasoning Fast Mode | The Qwen3.8-2.4T-A95B template throws a fatal exception if enable_thinking=false (the Qwen3.8-27B template accepts it). | Full non-reasoning mode restored via kwargs (enable_thinking=false, reasoning_effort="none") or inline `. | | History Reasoning Extraction | Drops in-content thinking extraction, prepending blank blocks to real thoughts in chat history. | Extracts reasoning across OpenAI (reasoning_content), Anthropic (thinking), and in-content tags () without tag duplication. | | Tool Argument Wire Format | Crashes on serialized JSON strings from standard OpenAI API clients. | Canonical XML default with universal argument handling: safely renders stringified JSON arguments in history without syntax crashes or KV cache invalidation. | | Client Reasoning Aliases | Rejects non-standard effort names. | Maps OpenAI, Claude Code, Cursor, and Cline aliases automatically: high, max, ultracode, extreme $\to$ xhigh; minimal $\to$ low; none, off $\to$ disabled. | | Inline Chat Tags | Not supported. | Inline steering via chat text: , , , , . Tags are stripped before inference and stay in effect for later turns until another tag replaces them. | | Leading System Prompts | Treats multiple leading system messages as separate turns. | Merges consecutive leading system and developer messages into a single system turn joined by double newlines. | | Tool Error Recovery | Not supported or triggers false warnings on search results containing words like "error". | Two-tier error recovery with smart code/grep disambiguation to avoid false warnings on search results like throw new Error(...). Structural signals escalate on payloads of any size. | | llama.cpp Flag Support | Requires manual template kwargs. | Native alias support for --reasoning-preserve CLI flag via preserve_reasoning. | | Diagnostic Tool | None provided. | Includes scripts/check_applied.py` to inspect model directories and GGUFs for active template versions. |


Quick Install & Engine Setup

llama.cpp / llama-server / koboldcpp

Run llama-server with the template file and DeepSeek reasoning format: ``bash llama-server -m your_model.gguf --jinja --chat-template-file chat_template.jinja --reasoning-format deepseek ` Why --reasoning-format deepseek matters: When connecting coding agents like OpenCode, Claude Code, or Pi.dev to llama-server, this flag extracts blocks into the dedicated reasoning_content` API response field. This prevents raw thinking tokens from leaking into the text stream and stopping tool calls midway.

Native CLI flag: On recent llama.cpp builds, you can pass --reasoning-preserve directly for 100% Prefix KV Cache retention.

LM Studio

1. Open your Qwen model in the right side panel. 2. Scroll down to Prompt Template. 3. Replace the template with the contents of chat_template.jinja. 4. Click Save.

vLLM

Replace the "chat_template" string in your tokenizer_config.json with chat_template_oneline.txt (or raw chat_template.jinja). ``bash vllm serve Qwen/Qwen3.8-2.4T-A95B --reasoning-parser qwen3 --tool-call-parser qwen3_xml ` Reasoning parser: Use --reasoning-parser qwen3 on vLLM to split blocks into the response reasoning field. Note that vLLM's parser checks the top-level enable_thinking: false request parameter or reasoning_effort: "none", but does not inspect Jinja-internal dynamic tags in prompt bodies. To disable reasoning on vLLM, pass "enable_thinking": false` in your request parameters.

Tool parser selection: Use --tool-call-parser qwen3_xml on current vLLM releases. If you are on an older vLLM build, use --tool-call-parser qwen3_coder. If you explicitly set tool_call_format="json", use --tool-call-parser hermes.

SGLang

Launch SGLang with --chat-template pointing directly to chat_template.jinja: ``bash python3 -m sglang.launch_server --model-path Qwen/Qwen3.8-27B --chat-template chat_template.jinja ` Why --chat-template matters on SGLang: By default, SGLang 0.5.x ignores a local chat_template.jinja file in the model directory and falls back to the embedded template in tokenizer_config.json. Passing --chat-template (or using chat-template: /path/to/chat_template.jinja` in YAML configuration) ensures the fixed template is loaded.

oMLX / MLX

Overwrite chat_template.jinja in your local model directory and launch with --jinja.

Recommended Sampling Parameters

Official Qwen sampling recommendations differ between thinking mode and non-thinking mode, reflecting an architectural refinement introduced in Qwen 3.8:

| Model & Mode | Temperature | Top-P | Top-K | Min-P | Presence Penalty | Description | |---|---|---|---|---|---|---| | Qwen 3.8: Thinking Mode (All Tasks / Coding & General) | 1.0 | 0.95 | 20 | 0.0 | 0.0 | Official 3.8 default: Prevents reasoning collapse in ` blocks, allowing broad exploration and self-correction on coding (SWE-bench) and reasoning tasks. Presence penalty must stay at 0.0. | | Qwen 3.5 / 3.6: Thinking Mode (Precise Coding & WebDev) | 0.6 | 0.95 | 20 | 0.0 | 0.0 | Official 3.5/3.6 coding default: Tailored for older checkpoints where lower temperature prevented logical drift during code synthesis. | | All Models: Non-Thinking / Instruct Mode (enable_thinking: false) | 0.7 | 0.80 | 20 | 0.0 | 1.5 | Stable direct instruction following when enable_thinking=false (or `). |

GitHub Copilot Desktop Canvas

GitHub Copilot Desktop requires OpenAI-standard JSON tool calls. Launch your server with tool_call_format="json" and use --tool-call-parser hermes (e.g. on vLLM or llama-server) so Copilot can interact seamlessly with canvas tools.


Why you need this

The official Qwen templates contain engine restrictions, Python-specific Jinja logic, and regressions that break local inference and agent workflows.

Critical Issues Fixed

| Area | Issue in Official Templates | The Fix | |---|---|---| | Qwen 3.8 Support | Official 3.8-2.4T-A95B crashes if enable_thinking=false. | Restored Fast Mode. Supports fast non-reasoning mode via kwargs or `. | | Qwen 3.8 Token Safety | Official xhigh default burns token budgets with zero content returned. | Safe medium Default. Zero prompt injection unless explicitly requested. | | Qwen 3.8 Regression | Official 3.8 injects duplicate blank in chat history. | Cured Empty Think Poisoning. Multi-format reasoning extraction. | | Reasoning Control | Inability to change reasoning effort in chat interfaces. | Inline Chat Tags. Full support for , , and . | | Compatibility | llama.cpp --reasoning-preserve CLI flag compatibility. | Native Alias Support. Supports both preserve_reasoning and preserve_thinking. | | Compatibility | JSON-string tool arguments (OpenAI / Ollama) crash official templates. | Universal Tool Parsing. Safely handles mappings, JSON strings, and scalar args. | | Agentic Loop | Model aborts turn when combining conversational text and a tool call. | Cured "Empty Think" poisoning and softened imperative system directives. | | Agentic Loop | Model gets stuck emitting the identical failing tool call. | Added two-tier error escalation to force correction while retaining reasoning. | | Agentic Loop | Model panics and debates internal rules after fetching data. | Broadened instructions to authorize conversational synthesis. | | Agentic Loop | API returns containing the word "error" trigger false retry loops. | Replaced broad matching with strict structural guards. | | Performance | Mutated past turns destroy the prefix cache. | Enforced chronological history for a 100% KV Cache hit rate. | | Performance | Deep Jinja nesting drops llama.cpp speed by 80%. | Flattened the AST architecture to maximize throughput. | | Compatibility | Python-specific filters crash C++ inference engines. | Rewrote all filters to be 100% minijinja safe. | | Compatibility | Qwen-native parsers (like vLLM) crash on JSON formatting. | Maintained canonical Qwen XML format as the default. | | Compatibility | Older API setups and wrappers crash on native XML. | Added a tool_call_format="json" opt-in override. | | Compatibility | Anthropic message.thinking` payloads are rejected. | Added native Anthropic reasoning support. | | Stability | Massive tool data returns blow out the context window. | Added dynamic payload truncation limits. | | Stability | Mid-conversation system prompts crash the template. | Added native support for arbitrary system and developer messages. | | Edge Cases | Text duplicates during streaming generation. | Restored canonical spacing to the generation prompt. | | Edge Cases | Model hallucinates reasoning tags when thinking is disabled. | Injected strict boundaries to force clean reasoning bypass. |


Customization & Kwarg Reference

1. Reasoning Effort Steering (Qwen 3.8)

Qwen 3.8 has 3 native prompt-steered reasoning levels. You can control this via template kwargs or directly inline in your chat messages:

Via Template Kwargs (chat_template_kwargs): ``json { "reasoning_effort": "xhigh" } ` * "medium" (Default / Safe Baseline): No extra instruction text injected. Preserves 100% Prefix KV Cache parity with v21 and lets the model reason naturally without token-budget traps. * "xhigh" (Deep Reasoning): Injects Qwen's official deep reasoning instruction: > "Reasoning effort is set to xhigh. Please think carefully through the task, validate key assumptions, consider plausible alternatives, and prioritize correctness, consistency, and clarity in the final answer." * "low"` (Concise Reasoning): Injects concise thinking instructions for fast, summary-oriented reasoning: > "Reasoning effort is set to low. Keep your thinking brief and focused, moving directly to the conclusion without unnecessary elaboration."

API Compatibility Aliases: To prevent errors when calling the model through standard API proxies and coding agent harnesses: * "high", "max", "ultracode", and "extreme" automatically map to "xhigh" (supported by OpenAI, Claude Code, Cline, Cursor). * "minimal" and "low" automatically map to "low". * "none" and "off" disable thinking entirely.

Via Inline Chat Tags (Per-Prompt Steering): * Solve this proof or ` (Deep reasoning) * Explain recursion briefly (Concise reasoning) * What is the capital of France? ` (No reasoning / fast mode)

Tags are sticky: the template scans the whole conversation and the last tag found wins, so a tag in an earlier message stays in effect for later turns until another tag replaces it.

(Note: When thinking is disabled, reasoning effort instructions are automatically suppressed. On vLLM, prefer request-level enable_thinking=false or reasoning_effort="none" over ``; see the vLLM setup notes above).

Editing defaults without kwargs (LM Studio and other UIs that cannot pass chat_template_kwargs): Three variables near the top of chat_template.jinja hold the defaults. Edit them before pasting the template: * _default_reasoning_effort = 'medium' - effort used when no reasoning_effort kwarg is passed: 'low', 'medium', 'xhigh', or 'none' (thinking off). The aliases listed above work too. * enable_thinking = enable_thinking if enable_thinking is defined else true - change true to false to default to non-thinking mode. * _tool_format = tool_call_format if tool_call_format is defined else 'xml' - change 'xml' to 'json' for harnesses that need Hermes JSON.

2. KV Cache Preservation (preserve_reasoning & preserve_thinking)

By default, this template preserves all past `` blocks in the chat history. This prevents the model from suffering "amnesia stalls" during complex agentic loops and guarantees a 100% Prefix KV Cache hit rate on local inference engines.

* On recent llama.cpp builds, pass --reasoning-preserve directly. * Or pass via template kwargs: ``json { "preserve_thinking": true } ``

If you are on severely memory-constrained hardware and need to save context tokens, set "preserve_thinking": false (or "preserve_reasoning": false) to strip past thoughts.

3. Tool Call Format (XML vs JSON)

Qwen models are natively trained on XML tool calls (`). By default, this template uses xml format: * "xml" (Default): Generates canonical XML instructions for tool calls and safely handles both dictionary arguments and serialized JSON strings in assistant history without corrupting XML tags or mutating system prompts between turns. Dictionary arguments render as blocks; a serialized JSON string is rendered verbatim inside the block as a best-effort fallback (Jinja cannot parse JSON), so dictionaries are preferred. vLLM converts string arguments to dictionaries before rendering, so it always takes the path. * "json" (Optional Override): Forces Hermes JSON format ({"name": "...", "arguments": {...}}`) for both system prompt instructions and history rendering.

When to use the JSON override: If you are using a framework or harness (such as specific Hermes Agent configurations) that strictly requires Hermes JSON, pass: ``json { "tool_call_format": "json" } `` (When opting into JSON format, structured JSON responses and tool arguments are protected from truncation to preserve JSON syntax, while plain-text logs are safely sliced).

4. Dynamic Payload Truncation

To prevent oversized tool returns from blowing out context limits: * max_tool_arg_chars (default 0 / disabled): Slices oversized tool call arguments. Applies to mapping arguments and to serialized JSON string arguments alike. * max_tool_response_chars (default 0 / disabled): Slices oversized tool output data.

In JSON mode (tool_call_format="json"), tool call arguments and structured JSON responses ({...}, [...]) are preserved intact to prevent syntax corruption, while long plain-text outputs (e.g. bash transcripts) remain safely governed by max_tool_response_chars.


Diagnostic Script & Test Suite

Check Active Template on Your Model

Run the included diagnostic utility on your model folder or GGUF: ``bash python3 scripts/check_applied.py /path/to/your/model ``

Running the Test Suite

``bash python3 scripts/test_v22.py ` Tests cover 105 automated verification cells including reasoning_effort levels, monotonic API mappings, inline chat tags, multi-part and vision payloads, tool call serialization across wire formats, dynamic truncation, error escalation precision, malformed input handling, multi-turn history parsing, prefix KV cache stability, and parity between chat_template.jinja and chat_template_oneline.txt. Set QWEN_TEMPLATE_FILE=chat_template_oneline.txt to run the entire suite against the minified build. Test 105 is a deterministic property fuzzer (scripts/fuzz_template.py) asserting nine structural invariants over generated conversations; run it standalone with python3 scripts/fuzz_template.py --cases 2000`.


Authorship & Contributors

| Role | Author / Contributor | |------|----------------------| | Original models | Alibaba Cloud (Qwen team) | | Template fixes | Frédéric Guigand (@froggeric) | | Property fuzzer, error tiering & test suite | Juan Calderon-Perez (@g-a-b-y) | | Multi-tool alignment, reasoning fields & docs | Gabriel Devenyi (@gdevenyi) | | C++ AST optimizations | barubary / spiritbuun |

License

Apache-2.0, inherited from Qwen.


Technical Details of the Critical Fixes

1. The "Empty Think" Poisoning and Logic Trap Cure

Previous templates attempted to save tokens by replacing past thoughts with empty \n blocks, combined with an absolute system prompt demanding a tool be called immediately after `. This created a toxic pattern where the model associated empty thoughts with tools, causing an 80%+ premature turn abort rate. We abolished empty think injection and rewrote the ` directives to explicitly authorize conversational synthesis after thinking. In this release, we also cured official Qwen 3.8's history bug where missing in-content parsers created duplicate blank think tags.

2. Upfront Pre-Scan for Control Tags & Inline Effort Steering

In Jinja templates, system prompts are assembled before iterating over message history. We introduce an upfront pre-scan covering raw strings, string lists, and multi-part content lists ([{'type': 'text', 'text': '...'}]). This resolves `, , , , and ` states before the system message is built, preventing reasoning instructions from being injected into non-reasoning turns and cleanly stripping all tags during rendering.

3. KV Cache Safety and Autoregressive Normalization

Llama.cpp and vLLM utilize prefix KV caching to speed up generation. Because this template preserves historical thoughts chronologically by default and defaults reasoning_effort to medium (zero system tokens), rendered history perfectly synchronizes with cached generated tokens. Combined with strict single newline normalization at autoregressive boundaries, this achieves a 100% KV Cache hit rate in multi-turn sessions.

4. Native XML Tool Format and Universal Serialization

The model was trained with the XML tool format used by Qwen3-Coder. We restored this format natively while bypassing the |items crash by handling both mapping dictionaries and JSON strings. This eliminates crashes when standard OpenAI proxies pass stringified arguments.

5. Two-Tier Agentic Error Escalation

When a tool call fails validation repeatedly, the model can enter a degenerate reasoning spiral. This template leverages a two-tier escalation system driven by a forward-tracked consecutive_failures counter. On the first error, a diagnostic warning is injected. On the second consecutive error, an urgent system warning forces a fundamentally different approach while retaining the reasoning block so the model can plan its correction.

6. Smart False-Positive Detection

Instead of broad substring matching that triggers false retry loops on successful database returns containing words like "error", this template utilizes strict structural guards evaluated over the first 120 characters of the payload.

Signals are split into two tiers. Strong signals are unambiguous structural markers ("error":, Traceback (most recent call last):, command not found, Exception:, fatal:, nonzero exit codes, invalid syntax) and fire unconditionally. Weak signals are bare prefixes (error:, err!) and remain gated by a length ceiling plus shell echo ($ ) and timing (took ) exclusions. Code and search results are excluded up front via throw new, console.error, logger.error and similar patterns.

Tiering matters because the earlier flat gate suppressed every signal on payloads of 600 characters or more: a routine multi-frame Python traceback exceeds that ceiling, and any output echoing a shell prompt or a timing figure was discarded outright. Strong signals now escalate regardless of payload size.

7. minijinja Compatibility Constraints

Python-only Jinja2 features crash or misbehave on minijinja (the C++ runtime used by llama.cpp, LM Studio, and MLX). All instances have been refactored for universal support: * content | replace('', '') became content.split('') | join('') (fixes a bug where minja silently drops the entire text payload if the replaced string is found at index 0). * | items became mapping.items() iteration, which minijinja implements natively. * loop.previtem became explicit array indexing. * map('string') became join('|'). * | first became '$ ' in content.

8. AST Flattening for C++ Throughput

Deeply nested Jinja loops and macros create severe parsing bottlenecks in C++ inference engines. We flattened the AST architecture, effectively curing an 80% inference throughput drop on llama.cpp by streamlining how ns_state tracking and historical rendering loops are evaluated.

9. Dynamic Payload Truncation

Massive API or database returns can instantly blow out a model's context window. We implemented max_tool_arg_chars and max_tool_response_chars limiters that safely slice oversized payloads. Argument truncation covers both mapping arguments and serialized JSON string arguments, so the OpenAI proxy wire format is limited on the same terms as native dictionaries. In JSON mode (tool_call_format="json"), structured JSON payloads ({...}, [...]) and tool arguments are protected from truncation to guarantee JSON parse validity, while plain-text outputs (such as bash logs) are safely truncated according to max_tool_response_chars.

10. Reasoning Bypass Hallucination Mitigation

When thinking is disabled, Qwen models often hallucinate reasoning tags due to their training bias. In v22.5, we fully aligned the # Tools system prompt block: when thinking is disabled (enable_thinking=false or reasoning_effort="none"), the template omits ` examples and planning directives entirely, instructing the model to output tool calls immediately without conversational prefixes. This prevents the model from attempting to reason or hallucinating closing tags after the prefilled closed \n\n\n\n` block.


Update History & Changelog

Mirrored from the Hugging Face Hub and served from the Conceptio Open Knowledge Archive. Read the original card at https://huggingface.co/froggeric/Qwen-Fixed-Chat-Templates.