Transformers config tokenizers expert
Skill air-gapped/skills/.claude/skills/transformers-config-tokenizers-expert
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.From its SKILL.md
npx -y skills add air-gapped/skills --skill transformers-config-tokenizers-expertAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 5 stars5 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
18.7 KB, ~4.9k tokens by cl100k_base, as published. Nobody here has run it
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 istokenizer.backend_tokenizer.get_added_tokens_decoder(). Python-sideall_special_ids/special_tokens_map/added_tokens_decoderare views; treat them as such. - Engines disagree. vLLM and sglang both union-merge
generation_config.eos_token_id, but apply it through different pipelines (seeengine-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, withspecialflag,lstrip/rstrip/normalizedattrs. Source:tokenization_utils_tokenizers.py:488-495(v5), passthrough. Only available forTokenizersBackend.tokenizer.added_tokens_decoder— Python mirror. ForTokenizersBackendit's a passthrough to #1. ForPythonBackendit's deserialized fromtokenizer_config.json["added_tokens_decoder"].tokenizer.all_special_tokens/all_special_ids— the narrow union of SEVEN named role slots (SPECIAL_TOKENS_ATTRIBUTESattokenization_utils_base.py:1040-1047) +extra_special_tokens. Does not include any added token registered withspecial=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 asdict[str, str]. No extras, no added_tokens. Legacy shape.tokenizer.extra_special_tokens— list internally (self._extra_special_tokens = []attokenization_utils_base.py:1074). v5.0.0rc0 serialized this as a list intotokenizer_config.json, crashing<5.0readers that call.keys(). GLM-5.1 ships extra_special_tokens as a list; GLM-4.6 ships it as{}(dict). Seeprecedence-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:
tojsondefaults toensure_ascii=False— stdlib Jinja's default isTrue. 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_nowuses 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.ImmutableSandboxedEnvironmentblocks mutation. Templates cannot.pop()messagesor write to passed objects. Workarounds copy into locals.loopcontrolsenables{% break %}and{% continue %}. Some templates depend on these; a stripped-down renderer missing the extension raisesTemplateSyntaxError.
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_kwargsmerges with dict-union (request wins) →safe_apply_chat_template→resolve_chat_template_kwargsallowlist filter atvllm/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 whoseapply_chat_templateuses**kwargs(Kimi K2). - sglang
chat_template_kwargs: literal dict update atserving_chat.py:524-527. No allowlist. Any key reachesapply_chat_template. Closer to pre-27622 vLLM. - vLLM
trust_request_chat_template: defaultFalse. Rejects per-requestchat_templateorchat_template_kwargsunless set True. Enforced atengine/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.toolspresent, mistral reasoning_effort). adjust_request(vLLM): runs atrender/serving.py:372-383, reasoning parser first then tool parser. Can mutatetools,stop,structured_outputs,response_formatbeforeto_sampling_params.- sglang has no
adjust_requestanalog. The three hardcodedskip_special_tokens=Falseoverrides atserving_chat.py:306/315/397are the equivalent. - Stop-token merge:
- vLLM:
update_from_generation_configatsampling_params.py:540-560appendsgeneration_config.eos_token_idlist tostop_token_idsunlessignore_eos=True. - sglang:
model_config._get_hf_eos_token_idatmodel_config.py:580-598unionshf_config.eos_token_idandhf_generation_config.eos_token_idintoSet[int].
- vLLM:
- Incremental detokenizer word boundaries: vLLM has fast
(
DecodeStreamfromtokenizers) and slow (detokenize_incrementallywithprefix_offset/read_offsetdiff + U+FFFD guard) paths atvllm/v1/engine/detokenizer.pyandvllm/tokenizers/detokenizer_utils.py:98-167. sglang usesDetokenizerManagersubprocess with four-offsetDecodeStatusatsglang/srt/managers/detokenizer_manager.py:57-63. sglang #22510 was a serving_chat.py double-slice bug (fixed PR #22549, not the detokenizer — despiteskip_special_tokens=Falsebeing 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]ingeneration_config.json. IDs 154827/154829 are<|user|>/<|observation|>turn primers. Engines unioning this list stop on turn boundaries;skip_special_tokens=Falseleaks them into output.extra_special_tokensas list, not dict.TokenizersBackendclass name — fails import on transformers<5.0. - GLM-5.1-FP8 orphan-commit trap — patch SHA
6ad52eenot reachable fromrefs/heads/main(a92f8155). Users assume fix is live; it isn't. Verification requires checking/refson HF API. - Gemma-4-26B-A4B-it — multi-ID EOS
[1, 106, 50].added_tokens_decoderabsent fromtokenizer_config.json(lives in LFStokenizer.json). Separatechat_template.jinjanot auto-loaded by transformers 5.5.0 (issue #45205). Scalar-null serialization bug informat_argumentmacro rendersNonenotnull. - Kimi-K2.6 half-fix —
config.json+generation_config.jsonflipped EOS to 163586 (<|im_end|>);tokenizer_config.jsonkept[EOS](163585). vLLM reads tokenizer_config, sglang reads generation_config. Different engines stop on different tokens. Notokenizer.json;tiktokenpackage required. - Kimi-K2.6 nested-config trap —
quantization_configlives atconfig["text_config"]["quantization_config"], NOT top-level. Top has onlydtype: bfloat16and an empty/absent quant block. A reader that grabsconfig["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 forlm_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_configare 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.jsonhas noadded_tokens_decoder.<think>/</think>on R1 live only inchat_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 matrixreferences/tokenizer-classes.md— v5 taxonomy,auto_map, aliasesreferences/precedence-rules.md— five-source discovery w/ file:linereferences/chat-template-contract.md— Jinja env, globals,add_generation_promptreferences/engine-knobs.md— vLLM + sglang tokenizer-adjacent flagsreferences/hall-of-shame.md— verified 2026 incidentsreferences/snippets.py— drop-in preflight Pythonreferences/sources.md— dated external references (freshen target)
What ships with it: 9 files
116.2 KB alongside SKILL.md, 1 of them executable
references/
- chat-template-contract.md10.4 KB
- config-files.md12.0 KB
- engine-knobs.md17.6 KB
- hall-of-shame.md16.3 KB
- improvement-backlog.md4.5 KB
- precedence-rules.md10.9 KB
- snippets.pyruns21.3 KB
- sources.md11.6 KB
- tokenizer-classes.md11.6 KB