Transformers config + tokenizers expert
Target: engineers writing a preflight tool (or a vLLM/sglang operator)
that must decide, before handing a HuggingFace snapshot to an inference
engine, which files win, which tokens are structural, and which
class will actually instantiate.
Almost every major 2026 release has shipped with drift between
tokenizer_config.json, generation_config.json, config.json, and
the Rust-backed tokenizer state. The skill exists so a preflight tool
can answer that drift authoritatively — not guess.
Stance
- Cite, don't paraphrase. Every load-bearing claim has a file:line
or URL citation in
references/. Point at the source.
- Version-gate. Transformers v5 (GA 2026-01-26) renamed the
tokenizer classes and changed serialization shapes. Pre-5.0 and
post-5.0 diverge — check
transformers.__version__ before claiming.
- Rust is truth. For any model with
tokenizer.json, the
authoritative added-token state is
tokenizer.backend_tokenizer.get_added_tokens_decoder(). Python-side
all_special_ids / special_tokens_map / added_tokens_decoder are
views; treat them as such.
- Engines disagree. vLLM and sglang both union-merge
generation_config.eos_token_id, but apply it through different
pipelines (see engine-knobs.md). Predict per engine, not in the
abstract.
Triage: symptom → layer → reference
Use this table first. Deep dives live in references/.
| Symptom |
Layer |
Open |
tokenizer.eos_token_id disagrees with generation_config.eos_token_id |
Config drift |
config-files.md#eos-drift |
| Engine stops on token X, template emits token Y |
Config drift |
config-files.md#eos-drift + engine-knobs.md#stop-token-merge |
AutoTokenizer.from_pretrained wants trust_remote_code=True |
Class selection |
tokenizer-classes.md#tiktoken-path |
KeyError: 'TokenizersBackend' on import |
Version gate |
tokenizer-classes.md#version-aliases |
AttributeError: 'list' object has no attribute 'keys' on extra_special_tokens |
Cross-version serialization |
precedence-rules.md#extra-special-tokens-shape |
all_special_ids misses DeepSeek <|place▁holder…|> tokens |
Discovery precedence |
precedence-rules.md#backend-fallback |
added_tokens_decoder absent from tokenizer_config.json |
v5 consolidation |
config-files.md#v5-consolidation |
Cannot use chat template functions because tokenizer.chat_template is not set |
Template file not wired |
chat-template-contract.md#gemma-4-issue-45205 |
chat_template_kwargs silently dropped at request time |
Allowlist filter |
engine-knobs.md#chat-template-kwargs-allowlist |
enable_thinking=false has no effect |
Allowlist filter (pre-v0.11.1) |
engine-knobs.md#pr-27622 |
Tool-call arguments render as "None" instead of null |
Template scalar bug |
hall-of-shame.md#gemma-4-ap-45 |
Turn primers (<|user|>, <|observation|>) leak into output |
EOS list contains turn markers (GLM-5.1) |
hall-of-shame.md#glm-5-1-three-id-eos |
| Streaming chunks arrive as word fragments |
sglang serving_chat.py double-slice (#22549) OR vLLM skip_special_tokens=False |
engine-knobs.md#incremental-detokenizer |
apply_chat_template crashes with UndefinedError on tc.arguments.items() |
Arguments arrived as JSON string, not dict |
hall-of-shame.md#glm-5-1-ap-45 |
Kimi emits [EOS] but engine expects <|im_end|> (or vice versa) |
Kimi EOS split-brain |
hall-of-shame.md#kimi-k2-6-half-fix |
The precedence cheat sheet (memorize)
Five sources exist for "is this token structural?" They disagree.
Reach for them in this order when writing preflight code:
tokenizer.backend_tokenizer.get_added_tokens_decoder() — Rust
truth. dict[int, AddedToken]. Every added token, with special
flag, lstrip/rstrip/normalized attrs. Source:
tokenization_utils_tokenizers.py:488-495 (v5), passthrough.
Only available for TokenizersBackend.
tokenizer.added_tokens_decoder — Python mirror. For
TokenizersBackend it's a passthrough to #1. For
PythonBackend it's deserialized from
tokenizer_config.json["added_tokens_decoder"].
tokenizer.all_special_tokens / all_special_ids — the narrow
union of SEVEN named role slots (SPECIAL_TOKENS_ATTRIBUTES at
tokenization_utils_base.py:1040-1047) + extra_special_tokens.
Does not include any added token registered with
special=False. This is why DeepSeek's reserved-token slabs and
GLM-5.1's <\|user\|>-as-turn-primer are invisible.
tokenizer.special_tokens_map — only the seven role slots as
dict[str, str]. No extras, no added_tokens. Legacy shape.
tokenizer.extra_special_tokens — list internally
(self._extra_special_tokens = [] at tokenization_utils_base.py:1074).
v5.0.0rc0 serialized this as a list into tokenizer_config.json,
crashing <5.0 readers that call .keys(). GLM-5.1 ships
extra_special_tokens as a list; GLM-4.6 ships it as {} (dict).
See precedence-rules.md#extra-special-tokens-shape.
Preflight rule of thumb. For any structural question beyond "is
this a named role slot", go to #1. If #1 is unavailable (no
tokenizer.json, i.e. Kimi via TikTokenTokenizer), fall through to
#2 from tokenizer_config.json["added_tokens_decoder"], and
cross-ref against generation_config.json.
Full table with file:line per backend: references/precedence-rules.md.
Config-file precedence (memorize)
For the "which EOS wins" question:
| Consumer |
Reads |
Wins |
model.generate() (transformers) |
generation_config.eos_token_id |
Primary; config.json only fills unset fields |
apply_chat_template |
tokenizer.eos_token (from tokenizer_config.json) when template says {{ eos_token }} |
Render only; not enforcement |
| vLLM stop-matching |
Unions generation_config.eos_token_id list into stop_token_ids at sampling_params.py:540-560 |
Union |
| sglang stop-matching |
Unions hf_config.eos_token_id and hf_generation_config.eos_token_id at model_config.py:580-598 |
Union |
Consequence: a single-int eos_token in tokenizer_config.json
paired with a three-ID list in generation_config.json is fine for
engines (they union) but ambiguous for any tool that only reads the
tokenizer. Preflight must read both and diff.
Full catalogue per file: references/config-files.md.
Tokenizer-class cross-reference (2026)
Which class actually instantiates for major lab repos. Verified
against each repo's tokenizer_config.json.
| Lab / repo |
tokenizer_class |
Backend |
Files shipped |
Trust remote code? |
| moonshotai/Kimi-K2-Instruct, K2.6 |
TikTokenTokenizer (custom, auto_map) |
PythonBackend (slow) |
tiktoken.model, NO tokenizer.json |
Yes + pip install tiktoken |
| google/gemma-4-E4B, 26B-A4B-it |
GemmaTokenizer + Gemma3Processor |
TokenizersBackend |
tokenizer.json (LFS), sep chat_template.jinja (issue #45205) |
No |
| zai-org/GLM-5.1, GLM-5.1-FP8 |
TokenizersBackend (explicit) |
TokenizersBackend |
tokenizer.json, no special_tokens_map.json |
No; transformers ≥5.0 required |
| zai-org/GLM-4.6 |
PreTrainedTokenizer |
PythonBackend (alias) |
tokenizer.json + dict extra_special_tokens |
No |
| Qwen/Qwen3-0.6B |
default |
TokenizersBackend |
Full set |
No |
| Qwen/Qwen3.5-35B-A3B-Base |
default |
TokenizersBackend |
Base flips EOS to <|endoftext|> vs <|im_end|> on Instruct |
No |
| deepseek-ai/DeepSeek-V3 |
LlamaTokenizerFast |
TokenizersBackend |
tokenizer.json (7.85 MB LFS); added_tokens_decoder NOT in tokenizer_config.json |
No |
| deepseek-ai/DeepSeek-R1 |
LlamaTokenizerFast |
TokenizersBackend |
<think>/</think> only in chat_template.jinja, NOT in added_tokens_decoder |
No |
| microsoft/phi-4 |
GPT2Tokenizer |
TokenizersBackend |
EOS is <|im_end|>; BOS is <|endoftext|> (inverted vs Qwen-Base) |
No |
| mistralai/Mistral-Small-24B-Instruct-2501 |
LlamaTokenizer |
MistralCommonBackend if tekken.json present, else fast |
[INST]/[/INST] at ids 3/4 |
No |
Full taxonomy + auto_map mechanics: references/tokenizer-classes.md.
Chat-template Jinja rendering contract
Environment built at transformers/utils/chat_template_utils.py:234:
jinja_env = ImmutableSandboxedEnvironment(
trim_blocks=True, lstrip_blocks=True,
extensions=[AssistantTracker, jinja2.ext.loopcontrols]
)
jinja_env.filters["tojson"] = tojson # ensure_ascii=False default
jinja_env.globals["raise_exception"] = raise_exception # throws TemplateError
jinja_env.globals["strftime_now"] = strftime_now # LOCAL TZ, not UTC
Four gotchas operators hit:
tojson defaults to ensure_ascii=False — stdlib Jinja's
default is True. Templates that dump CJK/emoji tool schemas rely
on this override. A preflight tool that renders in a naive Jinja
env will produce HTML-escaped output the model never trained on.
strftime_now uses local time. Llama-3.1/3.2 templates inject
a date header; the host's timezone determines the value. A
container running in UTC produces different prompts than a laptop
in Europe.
ImmutableSandboxedEnvironment blocks mutation. Templates
cannot .pop() messages or write to passed objects. Workarounds
copy into locals.
loopcontrols enables {% break %} and {% continue %}.
Some templates depend on these; a stripped-down renderer missing
the extension raises TemplateSyntaxError.
add_generation_prompt semantics, continue_final_message,
apply_chat_template resolution order, AssistantTracker offsets:
references/chat-template-contract.md.
Engine knob precedence (vLLM + sglang)
Short form:
- vLLM
chat_template_kwargs: CLI --default-chat-template-kwargs
→ OpenAIServingChat.__init__ default → _prepare_extra_chat_template_kwargs
merges with dict-union (request wins) → safe_apply_chat_template →
resolve_chat_template_kwargs allowlist filter at
vllm/renderers/hf.py:352-377 → tokenizer.apply_chat_template(**resolved).
Allowlist fix PR #27622 shipped in v0.11.1 (2025-11-18). Pre-v0.11.1
silently dropped kwargs for tokenizers whose apply_chat_template
uses **kwargs (Kimi K2).
- sglang
chat_template_kwargs: literal dict update at
serving_chat.py:524-527. No allowlist. Any key reaches
apply_chat_template. Closer to pre-27622 vLLM.
- vLLM
trust_request_chat_template: default False. Rejects
per-request chat_template or chat_template_kwargs unless set
True. Enforced at engine/serving.py:415-425.
- sglang no equivalent: request kwargs always accepted; only
three sites hardcode overrides to
skip_special_tokens=False
(gpt-oss/gemma4 models, request.tools present, mistral
reasoning_effort).
adjust_request (vLLM): runs at
render/serving.py:372-383, reasoning parser first then tool
parser. Can mutate tools, stop, structured_outputs,
response_format before to_sampling_params.
- sglang has no
adjust_request analog. The three hardcoded
skip_special_tokens=False overrides at serving_chat.py:306/315/397
are the equivalent.
- Stop-token merge:
- vLLM:
update_from_generation_config at sampling_params.py:540-560
appends generation_config.eos_token_id list to stop_token_ids
unless ignore_eos=True.
- sglang:
model_config._get_hf_eos_token_id at model_config.py:580-598
unions hf_config.eos_token_id and hf_generation_config.eos_token_id
into Set[int].
- Incremental detokenizer word boundaries: vLLM has fast
(
DecodeStream from tokenizers) and slow (detokenize_incrementally
with prefix_offset/read_offset diff + U+FFFD guard) paths at
vllm/v1/engine/detokenizer.py and vllm/tokenizers/detokenizer_utils.py:98-167.
sglang uses DetokenizerManager subprocess with four-offset
DecodeStatus at sglang/srt/managers/detokenizer_manager.py:57-63.
sglang #22510 was a serving_chat.py double-slice bug (fixed PR
#22549, not the detokenizer — despite skip_special_tokens=False
being a red herring in the initial report).
Deep dive with file:line per knob: references/engine-knobs.md.
Hall of shame (verified 2026)
Pre-loaded real incidents. Each entry in references/hall-of-shame.md
has the exact file(s), token IDs, and — where known — the bead ID or
commit SHA. Summary:
- GLM-5.1 — three-ID EOS
[154820, 154827, 154829] in
generation_config.json. IDs 154827/154829 are
<|user|> / <|observation|> turn primers. Engines unioning this
list stop on turn boundaries; skip_special_tokens=False leaks
them into output. extra_special_tokens as list, not dict.
TokenizersBackend class name — fails import on transformers <5.0.
- GLM-5.1-FP8 orphan-commit trap — patch SHA
6ad52ee not
reachable from refs/heads/main (a92f8155). Users assume fix is
live; it isn't. Verification requires checking /refs on HF API.
- Gemma-4-26B-A4B-it — multi-ID EOS
[1, 106, 50]. added_tokens_decoder
absent from tokenizer_config.json (lives in LFS tokenizer.json).
Separate chat_template.jinja not auto-loaded by transformers 5.5.0
(issue #45205). Scalar-null serialization bug in format_argument
macro renders None not null.
- Kimi-K2.6 half-fix —
config.json + generation_config.json
flipped EOS to 163586 (<|im_end|>); tokenizer_config.json kept
[EOS] (163585). vLLM reads tokenizer_config, sglang reads
generation_config. Different engines stop on different tokens.
No tokenizer.json; tiktoken package required.
- Kimi-K2.6 nested-config trap —
quantization_config lives at
config["text_config"]["quantization_config"], NOT top-level. Top
has only dtype: bfloat16 and an empty/absent quant block. A
reader that grabs config["quantization_config"] returns {} and
concludes "BF16, no quantization" — wrong. Reality:
compressed-tensors, num_bits: 4, group_size: 32, format: pack-quantized (W4A16 routed-MoE INT4 with BF16 carve-outs for
lm_head, self_attn.*, shared_experts.*, dense MLP). Total
checkpoint 595 GB ≠ ~1 TB BF16. Always walk nested keys —
text_config, vision_config, audio_config, language_config
are common multimodal/MoE homes. Same trap on K2.5 (same nesting),
Llama-4 vision configs, GLM-4V, Qwen3-VL.
- Qwen3-0.6B —
<|im_end|> is simultaneously turn terminator AND
EOS. Qwen3.5-Base flips EOS to <|endoftext|> — preflight
hardcoding <|im_end|> emits runaway completions on base variants.
- DeepSeek-V3 — added tokens live only in
tokenizer.json (7.85
MB LFS). tokenizer_config.json has no added_tokens_decoder.
<think>/</think> on R1 live only in chat_template.jinja, not
as added tokens.
- Phi-4 inversion — EOS
<|im_end|>, BOS <|endoftext|>.
Opposite of Qwen-Base. Don't regex on string.
Full incidents with citations: references/hall-of-shame.md.
Drop-in snippets
references/snippets.py — copy-paste Python for preflight init-time
questions:
| Function |
Answers |
discover_added_tokens(tokenizer, snapshot_dir=None) |
Every added token ID, walked Rust→Python→config→tokenizer.json |
resolve_marker_to_id(tokenizer, marker_str) |
ID(s) for <|im_end|> / <|endoftext|> / <|end▁of▁sentence|> etc. Length >1 = vocab collision |
is_turn_marker_eos(snapshot_dir) |
[(eos_id, content, where_in_template)] for EOS entries that the template emits as turn primers (leak-on-stream set) |
cross_ref_files(snapshot_dir) |
Drift findings: EOS mismatch, extra_special_tokens shape, special_tokens_map drift, template sidecar-vs-inline |
version_gate_tokenizer_class(cfg) |
Minimum transformers version (TokenizersBackend → >=5.0; PreTrainedTokenizerFast → >=4.0 alias) |
build_chat_template_env() |
Minimal faithful ImmutableSandboxedEnvironment for offline render testing |
verify_commit_reachable(repo_id, sha) |
Guards against GLM-5.1-FP8-style orphan-commit traps via HF /refs |
find_nested_quantization_config(config) |
Walks text_config, vision_config, etc. — catches Kimi-K2.6 W4A16 hidden under text_config.quantization_config while top-level looks BF16. Returns [(dotted_path, value)]. |
summarize_quant_config(qc) |
One-line render of a quantization_config dict — compressed-tensors num_bits=4 group_size=32 format=pack-quantized ignore_patterns=4 kv_cache_scheme=None. Surfaces kv_cache_scheme:null (no shipped K/V scales → scale=1.0 fallback risk on --kv-cache-dtype fp8). |
Reference map
references/config-files.md — catalogue per file, drift matrix
references/tokenizer-classes.md — v5 taxonomy, auto_map, aliases
references/precedence-rules.md — five-source discovery w/ file:line
references/chat-template-contract.md — Jinja env, globals, add_generation_prompt
references/engine-knobs.md — vLLM + sglang tokenizer-adjacent flags
references/hall-of-shame.md — verified 2026 incidents
references/snippets.py — drop-in preflight Python
references/sources.md — dated external references (freshen target)
1---2name: transformers-config-tokenizers-expert3description: Preflight reference for HuggingFace snapshots — what vLLM, sglang, and transformers.generate see at runtime. Covers config-file precedence (tokenizer.json, tokenizer_config.json, generation_config.json, chat_template.jinja), transformers v5 tokenizer-class taxonomy (TokenizersBackend, PythonBackend, MistralCommonBackend, TikTokenTokenizer), special-token discovery (all_special_ids, added_tokens_decoder, extra_special_tokens, backend_tokenizer.get_added_tokens_decoder), chat-template Jinja contract (ImmutableSandboxedEnvironment, loopcontrols, raise_exception, strftime_now, tojson, add_generation_prompt), and engine knobs (skip_special_tokens, trust_request_chat_template, chat_template_kwargs allowlist, adjust_request, incremental detokenizer, EOS merge). Ships verified 2026 hall-of-shame for Kimi-K2.6, GLM-5.1, Gemma-4, Qwen3, DeepSeek-V3, plus drop-in Python for resolving markers to IDs, detecting turn-primer-as-EOS leaks, and cross-referencing tokenizer.json vs tokenizer_config.json.4---56# Transformers config + tokenizers expert78Target: engineers writing a preflight tool (or a vLLM/sglang operator)9that must decide, before handing a HuggingFace snapshot to an inference10engine, *which* files win, *which* tokens are structural, and *which*11class will actually instantiate.1213Almost every major 2026 release has shipped with drift between14`tokenizer_config.json`, `generation_config.json`, `config.json`, and15the Rust-backed tokenizer state. The skill exists so a preflight tool16can answer that drift authoritatively — not guess.1718---1920## Stance2122- **Cite, don't paraphrase.** Every load-bearing claim has a file:line23 or URL citation in `references/`. Point at the source.24- **Version-gate.** Transformers v5 (GA 2026-01-26) renamed the25 tokenizer classes and changed serialization shapes. Pre-5.0 and26 post-5.0 diverge — check `transformers.__version__` before claiming.27- **Rust is truth.** For any model with `tokenizer.json`, the28 authoritative added-token state is29 `tokenizer.backend_tokenizer.get_added_tokens_decoder()`. Python-side30 `all_special_ids` / `special_tokens_map` / `added_tokens_decoder` are31 views; treat them as such.32- **Engines disagree.** vLLM and sglang both union-merge33 `generation_config.eos_token_id`, but apply it through different34 pipelines (see `engine-knobs.md`). Predict per engine, not in the35 abstract.3637---3839## Triage: symptom → layer → reference4041Use this table first. Deep dives live in `references/`.4243| Symptom | Layer | Open |44|---|---|---|45| `tokenizer.eos_token_id` disagrees with `generation_config.eos_token_id` | Config drift | `config-files.md#eos-drift` |46| Engine stops on token X, template emits token Y | Config drift | `config-files.md#eos-drift` + `engine-knobs.md#stop-token-merge` |47| `AutoTokenizer.from_pretrained` wants `trust_remote_code=True` | Class selection | `tokenizer-classes.md#tiktoken-path` |48| `KeyError: 'TokenizersBackend'` on import | Version gate | `tokenizer-classes.md#version-aliases` |49| `AttributeError: 'list' object has no attribute 'keys'` on `extra_special_tokens` | Cross-version serialization | `precedence-rules.md#extra-special-tokens-shape` |50| `all_special_ids` misses DeepSeek `<|place▁holder…|>` tokens | Discovery precedence | `precedence-rules.md#backend-fallback` |51| `added_tokens_decoder` absent from `tokenizer_config.json` | v5 consolidation | `config-files.md#v5-consolidation` |52| `Cannot use chat template functions because tokenizer.chat_template is not set` | Template file not wired | `chat-template-contract.md#gemma-4-issue-45205` |53| `chat_template_kwargs` silently dropped at request time | Allowlist filter | `engine-knobs.md#chat-template-kwargs-allowlist` |54| `enable_thinking=false` has no effect | Allowlist filter (pre-v0.11.1) | `engine-knobs.md#pr-27622` |55| Tool-call arguments render as `"None"` instead of `null` | Template scalar bug | `hall-of-shame.md#gemma-4-ap-45` |56| Turn primers (`<\|user\|>`, `<\|observation\|>`) leak into output | EOS list contains turn markers (GLM-5.1) | `hall-of-shame.md#glm-5-1-three-id-eos` |57| Streaming chunks arrive as word fragments | sglang `serving_chat.py` double-slice (#22549) OR vLLM `skip_special_tokens=False` | `engine-knobs.md#incremental-detokenizer` |58| `apply_chat_template` crashes with `UndefinedError` on `tc.arguments.items()` | Arguments arrived as JSON string, not dict | `hall-of-shame.md#glm-5-1-ap-45` |59| Kimi emits `[EOS]` but engine expects `<\|im_end\|>` (or vice versa) | Kimi EOS split-brain | `hall-of-shame.md#kimi-k2-6-half-fix` |6061---6263## The precedence cheat sheet (memorize)6465Five sources exist for "is this token structural?" They disagree.66Reach for them in this order when writing preflight code:67681. **`tokenizer.backend_tokenizer.get_added_tokens_decoder()`** — Rust69 truth. `dict[int, AddedToken]`. Every added token, with `special`70 flag, `lstrip/rstrip/normalized` attrs. Source:71 `tokenization_utils_tokenizers.py:488-495` (v5), passthrough.72 Only available for `TokenizersBackend`.732. **`tokenizer.added_tokens_decoder`** — Python mirror. For74 `TokenizersBackend` it's a passthrough to #1. For75 `PythonBackend` it's deserialized from76 `tokenizer_config.json["added_tokens_decoder"]`.773. **`tokenizer.all_special_tokens` / `all_special_ids`** — the narrow78 union of SEVEN named role slots (`SPECIAL_TOKENS_ATTRIBUTES` at79 `tokenization_utils_base.py:1040-1047`) + `extra_special_tokens`.80 **Does not include** any added token registered with81 `special=False`. This is why DeepSeek's reserved-token slabs and82 GLM-5.1's `<\|user\|>`-as-turn-primer are invisible.834. **`tokenizer.special_tokens_map`** — only the seven role slots as84 `dict[str, str]`. No extras, no added_tokens. Legacy shape.855. **`tokenizer.extra_special_tokens`** — list internally86 (`self._extra_special_tokens = []` at `tokenization_utils_base.py:1074`).87 v5.0.0rc0 *serialized* this as a list into `tokenizer_config.json`,88 crashing `<5.0` readers that call `.keys()`. GLM-5.1 ships89 extra_special_tokens as a **list**; GLM-4.6 ships it as `{}` (dict).90 See `precedence-rules.md#extra-special-tokens-shape`.9192**Preflight rule of thumb.** For any structural question beyond "is93this a named role slot", go to #1. If #1 is unavailable (no94`tokenizer.json`, i.e. Kimi via TikTokenTokenizer), fall through to95#2 from `tokenizer_config.json["added_tokens_decoder"]`, and96cross-ref against `generation_config.json`.9798Full table with file:line per backend: `references/precedence-rules.md`.99100---101102## Config-file precedence (memorize)103104For the "which EOS wins" question:105106| Consumer | Reads | Wins |107|---|---|---|108| `model.generate()` (transformers) | `generation_config.eos_token_id` | Primary; `config.json` only fills unset fields |109| `apply_chat_template` | `tokenizer.eos_token` (from `tokenizer_config.json`) when template says `{{ eos_token }}` | Render only; not enforcement |110| vLLM stop-matching | Unions `generation_config.eos_token_id` list into `stop_token_ids` at `sampling_params.py:540-560` | Union |111| sglang stop-matching | Unions `hf_config.eos_token_id` and `hf_generation_config.eos_token_id` at `model_config.py:580-598` | Union |112113**Consequence:** a single-int `eos_token` in `tokenizer_config.json`114paired with a three-ID list in `generation_config.json` is fine for115engines (they union) but ambiguous for any tool that only reads the116tokenizer. Preflight must read both and diff.117118Full catalogue per file: `references/config-files.md`.119120---121122## Tokenizer-class cross-reference (2026)123124Which class actually instantiates for major lab repos. Verified125against each repo's `tokenizer_config.json`.126127| Lab / repo | `tokenizer_class` | Backend | Files shipped | Trust remote code? |128|---|---|---|---|---|129| moonshotai/Kimi-K2-Instruct, K2.6 | `TikTokenTokenizer` (custom, `auto_map`) | `PythonBackend` (slow) | `tiktoken.model`, NO `tokenizer.json` | **Yes** + `pip install tiktoken` |130| google/gemma-4-E4B, 26B-A4B-it | `GemmaTokenizer` + `Gemma3Processor` | `TokenizersBackend` | `tokenizer.json` (LFS), sep `chat_template.jinja` (issue #45205) | No |131| zai-org/GLM-5.1, GLM-5.1-FP8 | `TokenizersBackend` (explicit) | `TokenizersBackend` | `tokenizer.json`, no `special_tokens_map.json` | No; **transformers ≥5.0 required** |132| zai-org/GLM-4.6 | `PreTrainedTokenizer` | `PythonBackend` (alias) | `tokenizer.json` + dict `extra_special_tokens` | No |133| Qwen/Qwen3-0.6B | default | `TokenizersBackend` | Full set | No |134| Qwen/Qwen3.5-35B-A3B-Base | default | `TokenizersBackend` | Base flips EOS to `<\|endoftext\|>` vs `<\|im_end\|>` on Instruct | No |135| deepseek-ai/DeepSeek-V3 | `LlamaTokenizerFast` | `TokenizersBackend` | `tokenizer.json` (7.85 MB LFS); `added_tokens_decoder` NOT in `tokenizer_config.json` | No |136| deepseek-ai/DeepSeek-R1 | `LlamaTokenizerFast` | `TokenizersBackend` | `<think>`/`</think>` only in `chat_template.jinja`, NOT in `added_tokens_decoder` | No |137| microsoft/phi-4 | `GPT2Tokenizer` | `TokenizersBackend` | EOS is `<\|im_end\|>`; BOS is `<\|endoftext\|>` (inverted vs Qwen-Base) | No |138| mistralai/Mistral-Small-24B-Instruct-2501 | `LlamaTokenizer` | `MistralCommonBackend` if `tekken.json` present, else fast | `[INST]`/`[/INST]` at ids 3/4 | No |139140Full taxonomy + `auto_map` mechanics: `references/tokenizer-classes.md`.141142---143144## Chat-template Jinja rendering contract145146Environment built at `transformers/utils/chat_template_utils.py:234`:147148```python149jinja_env = ImmutableSandboxedEnvironment(150 trim_blocks=True, lstrip_blocks=True,151 extensions=[AssistantTracker, jinja2.ext.loopcontrols]152)153jinja_env.filters["tojson"] = tojson # ensure_ascii=False default154jinja_env.globals["raise_exception"] = raise_exception # throws TemplateError155jinja_env.globals["strftime_now"] = strftime_now # LOCAL TZ, not UTC156```157158Four gotchas operators hit:1591601. **`tojson` defaults to `ensure_ascii=False`** — stdlib Jinja's161 default is `True`. Templates that dump CJK/emoji tool schemas rely162 on this override. A preflight tool that renders in a naive Jinja163 env will produce HTML-escaped output the model never trained on.1642. **`strftime_now` uses local time.** Llama-3.1/3.2 templates inject165 a date header; the host's timezone determines the value. A166 container running in UTC produces different prompts than a laptop167 in Europe.1683. **`ImmutableSandboxedEnvironment` blocks mutation.** Templates169 cannot `.pop()` `messages` or write to passed objects. Workarounds170 copy into locals.1714. **`loopcontrols` enables `{% break %}` and `{% continue %}`.**172 Some templates depend on these; a stripped-down renderer missing173 the extension raises `TemplateSyntaxError`.174175`add_generation_prompt` semantics, `continue_final_message`,176`apply_chat_template` resolution order, `AssistantTracker` offsets:177`references/chat-template-contract.md`.178179---180181## Engine knob precedence (vLLM + sglang)182183Short form:184185- **vLLM `chat_template_kwargs`**: CLI `--default-chat-template-kwargs`186 → `OpenAIServingChat.__init__` default → `_prepare_extra_chat_template_kwargs`187 merges with dict-union (request wins) → `safe_apply_chat_template` →188 `resolve_chat_template_kwargs` **allowlist** filter at189 `vllm/renderers/hf.py:352-377` → `tokenizer.apply_chat_template(**resolved)`.190 Allowlist fix PR #27622 shipped in **v0.11.1** (2025-11-18). Pre-v0.11.1191 silently dropped kwargs for tokenizers whose `apply_chat_template`192 uses `**kwargs` (Kimi K2).193- **sglang `chat_template_kwargs`**: literal dict update at194 `serving_chat.py:524-527`. **No allowlist.** Any key reaches195 `apply_chat_template`. Closer to pre-27622 vLLM.196- **vLLM `trust_request_chat_template`**: default `False`. Rejects197 per-request `chat_template` or `chat_template_kwargs` unless set198 True. Enforced at `engine/serving.py:415-425`.199- **sglang no equivalent**: request kwargs always accepted; only200 three sites hardcode overrides to `skip_special_tokens=False`201 (gpt-oss/gemma4 models, `request.tools` present, mistral202 reasoning_effort).203- **`adjust_request` (vLLM)**: runs at204 `render/serving.py:372-383`, reasoning parser first then tool205 parser. Can mutate `tools`, `stop`, `structured_outputs`,206 `response_format` before `to_sampling_params`.207- **sglang has no `adjust_request` analog.** The three hardcoded208 `skip_special_tokens=False` overrides at `serving_chat.py:306/315/397`209 are the equivalent.210- **Stop-token merge**:211 - vLLM: `update_from_generation_config` at `sampling_params.py:540-560`212 appends `generation_config.eos_token_id` list to `stop_token_ids`213 unless `ignore_eos=True`.214 - sglang: `model_config._get_hf_eos_token_id` at `model_config.py:580-598`215 unions `hf_config.eos_token_id` and `hf_generation_config.eos_token_id`216 into `Set[int]`.217- **Incremental detokenizer word boundaries**: vLLM has fast218 (`DecodeStream` from `tokenizers`) and slow (`detokenize_incrementally`219 with `prefix_offset`/`read_offset` diff + U+FFFD guard) paths at220 `vllm/v1/engine/detokenizer.py` and `vllm/tokenizers/detokenizer_utils.py:98-167`.221 sglang uses `DetokenizerManager` subprocess with four-offset222 `DecodeStatus` at `sglang/srt/managers/detokenizer_manager.py:57-63`.223 sglang #22510 was a **serving_chat.py double-slice bug** (fixed PR224 #22549, not the detokenizer — despite `skip_special_tokens=False`225 being a red herring in the initial report).226227Deep dive with file:line per knob: `references/engine-knobs.md`.228229---230231## Hall of shame (verified 2026)232233Pre-loaded real incidents. Each entry in `references/hall-of-shame.md`234has the exact file(s), token IDs, and — where known — the bead ID or235commit SHA. Summary:236237- **GLM-5.1** — three-ID EOS `[154820, 154827, 154829]` in238 `generation_config.json`. IDs 154827/154829 are239 `<|user|>` / `<|observation|>` turn primers. Engines unioning this240 list stop on turn boundaries; `skip_special_tokens=False` leaks241 them into output. `extra_special_tokens` as **list**, not dict.242 `TokenizersBackend` class name — fails import on transformers `<5.0`.243- **GLM-5.1-FP8 orphan-commit trap** — patch SHA `6ad52ee` not244 reachable from `refs/heads/main` (`a92f8155`). Users assume fix is245 live; it isn't. Verification requires checking `/refs` on HF API.246- **Gemma-4-26B-A4B-it** — multi-ID EOS `[1, 106, 50]`. `added_tokens_decoder`247 absent from `tokenizer_config.json` (lives in LFS `tokenizer.json`).248 Separate `chat_template.jinja` not auto-loaded by transformers 5.5.0249 (issue #45205). Scalar-null serialization bug in `format_argument`250 macro renders `None` not `null`.251- **Kimi-K2.6 half-fix** — `config.json` + `generation_config.json`252 flipped EOS to 163586 (`<|im_end|>`); `tokenizer_config.json` kept253 `[EOS]` (163585). vLLM reads tokenizer_config, sglang reads254 generation_config. Different engines stop on different tokens.255 No `tokenizer.json`; `tiktoken` package required.256- **Kimi-K2.6 nested-config trap** — `quantization_config` lives at257 `config["text_config"]["quantization_config"]`, NOT top-level. Top258 has only `dtype: bfloat16` and an empty/absent quant block. A259 reader that grabs `config["quantization_config"]` returns `{}` and260 concludes "BF16, no quantization" — wrong. Reality:261 `compressed-tensors`, `num_bits: 4`, `group_size: 32`, `format:262 pack-quantized` (W4A16 routed-MoE INT4 with BF16 carve-outs for263 `lm_head`, `self_attn.*`, `shared_experts.*`, dense MLP). Total264 checkpoint 595 GB ≠ ~1 TB BF16. **Always walk nested keys** —265 `text_config`, `vision_config`, `audio_config`, `language_config`266 are common multimodal/MoE homes. Same trap on K2.5 (same nesting),267 Llama-4 vision configs, GLM-4V, Qwen3-VL.268- **Qwen3-0.6B** — `<|im_end|>` is simultaneously turn terminator AND269 EOS. Qwen3.5-**Base** flips EOS to `<|endoftext|>` — preflight270 hardcoding `<|im_end|>` emits runaway completions on base variants.271- **DeepSeek-V3** — added tokens live only in `tokenizer.json` (7.85272 MB LFS). `tokenizer_config.json` has no `added_tokens_decoder`.273 `<think>`/`</think>` on R1 live only in `chat_template.jinja`, not274 as added tokens.275- **Phi-4 inversion** — EOS `<|im_end|>`, BOS `<|endoftext|>`.276 Opposite of Qwen-Base. Don't regex on string.277278Full incidents with citations: `references/hall-of-shame.md`.279280---281282## Drop-in snippets283284`references/snippets.py` — copy-paste Python for preflight init-time285questions:286287| Function | Answers |288|---|---|289| `discover_added_tokens(tokenizer, snapshot_dir=None)` | Every added token ID, walked Rust→Python→config→tokenizer.json |290| `resolve_marker_to_id(tokenizer, marker_str)` | ID(s) for `<\|im_end\|>` / `<\|endoftext\|>` / `<|end▁of▁sentence|>` etc. Length >1 = vocab collision |291| `is_turn_marker_eos(snapshot_dir)` | `[(eos_id, content, where_in_template)]` for EOS entries that the template emits as turn primers (leak-on-stream set) |292| `cross_ref_files(snapshot_dir)` | Drift findings: EOS mismatch, extra_special_tokens shape, special_tokens_map drift, template sidecar-vs-inline |293| `version_gate_tokenizer_class(cfg)` | Minimum transformers version (`TokenizersBackend` → `>=5.0`; `PreTrainedTokenizerFast` → `>=4.0` alias) |294| `build_chat_template_env()` | Minimal faithful `ImmutableSandboxedEnvironment` for offline render testing |295| `verify_commit_reachable(repo_id, sha)` | Guards against GLM-5.1-FP8-style orphan-commit traps via HF `/refs` |296| `find_nested_quantization_config(config)` | Walks `text_config`, `vision_config`, etc. — catches Kimi-K2.6 W4A16 hidden under `text_config.quantization_config` while top-level looks BF16. Returns `[(dotted_path, value)]`. |297| `summarize_quant_config(qc)` | One-line render of a `quantization_config` dict — `compressed-tensors num_bits=4 group_size=32 format=pack-quantized ignore_patterns=4 kv_cache_scheme=None`. Surfaces `kv_cache_scheme:null` (no shipped K/V scales → scale=1.0 fallback risk on `--kv-cache-dtype fp8`). |298299---300301## Reference map302303- `references/config-files.md` — catalogue per file, drift matrix304- `references/tokenizer-classes.md` — v5 taxonomy, `auto_map`, aliases305- `references/precedence-rules.md` — five-source discovery w/ file:line306- `references/chat-template-contract.md` — Jinja env, globals, `add_generation_prompt`307- `references/engine-knobs.md` — vLLM + sglang tokenizer-adjacent flags308- `references/hall-of-shame.md` — verified 2026 incidents309- `references/snippets.py` — drop-in preflight Python310- `references/sources.md` — dated external references (freshen target)