Instructions to use froggeric/Qwen-Fixed-Chat-Templates with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- MLX
How to use froggeric/Qwen-Fixed-Chat-Templates with MLX:
# Download the model from the Hub pip install huggingface_hub[hf_xet] huggingface-cli download --local-dir Qwen-Fixed-Chat-Templates froggeric/Qwen-Fixed-Chat-Templates
- Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- LM Studio
- Atomic Chat
v22.3: Error detection tiering, reasoning fixes, test suite 44→101 + property fuzzer
Summary
Extends the test suite from 44 to 101 cells and adds a deterministic property-based
fuzzer. The new coverage exposed 8 template defects, all fixed here, plus several
doc/behavior mismatches. Everything is validated against both chat_template.jinja
and the minified chat_template_oneline.txt.
Files: chat_template.jinja, chat_template_oneline.txt (regenerated),README.md, scripts/test_v22.py, scripts/check_applied.py (version strings),scripts/fuzz_template.py (new).
Template fixes (v22.2 → v22.3)
Error detection
- Signals split into strong/weak tiers. The previous flat gate suppressed every
signal on payloads ≥ 600 chars (an ordinary multi-frame Python traceback is
longer) and on any output containing$ortookanywhere. Structural
markers now escalate at any payload size; bareerror:/err!stay gated,
with exclusions scoped to the 120-char head. - Success payloads excluded from the strong tier:
Exit code: 0/process exited with code 0, and JSON success envelopes
("error": null/false/"").
Reasoning extraction
- Explicit
reasoning_content/thinkingplus a leading<think>block incontentno longer emits duplicate think blocks. Only the leading block is
stripped, so literal tags later in the answer (e.g. inside code fences when the
model explains template syntax) are preserved verbatim. Heuristic mid-content
extraction is unchanged when no explicit field is present. - Single-line
<think>...</think>(no surrounding newlines) is now extracted
instead of leaking raw tags into rendered history.
Tool serialization
- Scalar and list arguments serialize via
| tojsonin both wire formats instead
of being silently dropped. max_tool_arg_charsnow also covers serialized JSON-string arguments;max_tool_response_charsis bypassed undertool_call_format="json", matching
what §9 already documented.
Documentation corrections
preserve_thinking: falserewrites rendered history at each new user turn and
therefore voids the prefix-cache guarantee — now stated explicitly (the 100%
claim applies to the default setting). Verified not code-fixable: the retention
rule reads the whole message array.- The prefix KV claim is precisely scoped: stability holds at generation
boundaries, with merged system blocks and consecutive tool-result batches
atomic (the model only generates after a full batch is appended). - §7's
| itemsclaim corrected to match the actual.items()call; §6/§9
updated to the new detection and truncation behavior.
Tests & tooling
- Suite 44 → 101: control-tag/alias completeness, exception paths (the previously
unusedexpect_errorharness parameter), vision payloads, agentic wire-format
shapes, reasoning-extraction variants, error-detection precision, prefix KV
stability, jinja↔oneline parity with atemplate_versionstaleness check,
success envelopes, and input edge shapes. - New
scripts/fuzz_template.py: deterministic conversation fuzzer asserting nine
invariants (render success, build parity, token balance, verbatim content
preservation, XML parameter fidelity, JSON tool-call validity, warning
precision, prefix stability, no-think prefill). Wired in as test 101 (300
seeded cases); standalone via--cases N --seed S. QWEN_TEMPLATE_FILE=chat_template_oneline.txtruns the entire suite against
the minified build — previously only 4 render cases validated it.- Two intermediate versions of this diff introduced regressions (an
Exit code: 0false positive; answer mangling with explicit reasoning + a
literal</think>in the answer) that adversarial review caught. Both are now
pinned by dedicated regression tests (85/86 and 81).
Validation
- 101/101 vs
chat_template.jinja; 101/101 vschat_template_oneline.txt - 4,000 fuzzed conversations clean across two seeds
- Legacy
test_v21.py9/9;check_applied.pyverifies v22.3 - Oneline regenerated with
minify_jinja.pyand byte-verified against source
Known limitations / review ask
All validation is Python jinja2. The changes deliberately use only constructs
already present in v22.2 (startswith, in, single-arg split, lstrip,| tojson, ~), but a minijinja smoke test (llama-server --jinja / LM Studio)
was not run here and is recommended before merge.
Accepted residuals: numeric error codes ({"error": 0}) still escalate; a nested"error": null within the head alongside a real error would suppress the warning
(contrived); fatal: substring-matches non-fatal: (pre-existing semantics);
markers split at the 120-char head boundary can misclassify (pre-existing class);
an answer literally starting with <think> is treated as reasoning (inherent
to heuristic extraction).
@froggeric These fixes were found by running multiple iterations using claude opus-5 (max effort) and fable 5 (max effort).
It started by adding unit-tests to cover the whole chat template, and the fixing the bugs it found. After that fuzz tests were added that exposed additional bugs.
Fantastic! Thank you. I am now reviewing them.
If anyone else can test this template, please report here. Thanks!
Hi @g-a-b-y ,
Thank you for this incredible contribution! The two-tier error signal architecture, reasoning deduplication, test expansion, and property fuzzer are a huge step forward for the project.
This has been merged and is now live as part of the v22.3 release.
I made a few minor follow-up adjustments on top of your PR before releasing:
Script directory paths: Moved test_v22.py, fuzz_template.py, and check_applied.py into the scripts/ folder to maintain repo structure.
KV cache prefix fix: I removed the and reasoning_content condition on the history rendering loop. This ensures empty think blocks are preserved when re-rendering history in non-thinking mode, keeping 100% prefix cache matching.
Fuzzer prefix check: Updated the generation prompt prefix check in fuzz_template.py to compare history rendering directly against add_generation_prompt=True.
You are officially credited in the README.md changelog and release notes. Thanks again for your fantastic work!