agentsclimarketplace

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

Install
npx -y skills add air-gapped/skills --skill transformers-config-tokenizers-expert

Assembled 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 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/.

SymptomLayerOpen
tokenizer.eos_token_id disagrees with generation_config.eos_token_idConfig driftconfig-files.md#eos-drift
Engine stops on token X, template emits token YConfig driftconfig-files.md#eos-drift + engine-knobs.md#stop-token-merge
AutoTokenizer.from_pretrained wants trust_remote_code=TrueClass selectiontokenizer-classes.md#tiktoken-path
KeyError: 'TokenizersBackend' on importVersion gatetokenizer-classes.md#version-aliases
AttributeError: 'list' object has no attribute 'keys' on extra_special_tokensCross-version serializationprecedence-rules.md#extra-special-tokens-shape
all_special_ids misses DeepSeek <|place▁holder…|> tokensDiscovery precedenceprecedence-rules.md#backend-fallback
added_tokens_decoder absent from tokenizer_config.jsonv5 consolidationconfig-files.md#v5-consolidation
Cannot use chat template functions because tokenizer.chat_template is not setTemplate file not wiredchat-template-contract.md#gemma-4-issue-45205
chat_template_kwargs silently dropped at request timeAllowlist filterengine-knobs.md#chat-template-kwargs-allowlist
enable_thinking=false has no effectAllowlist filter (pre-v0.11.1)engine-knobs.md#pr-27622
Tool-call arguments render as "None" instead of nullTemplate scalar bughall-of-shame.md#gemma-4-ap-45
Turn primers (<|user|>, <|observation|>) leak into outputEOS list contains turn markers (GLM-5.1)hall-of-shame.md#glm-5-1-three-id-eos
Streaming chunks arrive as word fragmentssglang serving_chat.py double-slice (#22549) OR vLLM skip_special_tokens=Falseengine-knobs.md#incremental-detokenizer
apply_chat_template crashes with UndefinedError on tc.arguments.items()Arguments arrived as JSON string, not dicthall-of-shame.md#glm-5-1-ap-45
Kimi emits [EOS] but engine expects <|im_end|> (or vice versa)Kimi EOS split-brainhall-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:

  1. 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.
  2. 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"].
  3. 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.
  4. tokenizer.special_tokens_map — only the seven role slots as dict[str, str]. No extras, no added_tokens. Legacy shape.
  5. 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:

ConsumerReadsWins
model.generate() (transformers)generation_config.eos_token_idPrimary; config.json only fills unset fields
apply_chat_templatetokenizer.eos_token (from tokenizer_config.json) when template says {{ eos_token }}Render only; not enforcement
vLLM stop-matchingUnions generation_config.eos_token_id list into stop_token_ids at sampling_params.py:540-560Union
sglang stop-matchingUnions hf_config.eos_token_id and hf_generation_config.eos_token_id at model_config.py:580-598Union

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 / repotokenizer_classBackendFiles shippedTrust remote code?
moonshotai/Kimi-K2-Instruct, K2.6TikTokenTokenizer (custom, auto_map)PythonBackend (slow)tiktoken.model, NO tokenizer.jsonYes + pip install tiktoken
google/gemma-4-E4B, 26B-A4B-itGemmaTokenizer + Gemma3ProcessorTokenizersBackendtokenizer.json (LFS), sep chat_template.jinja (issue #45205)No
zai-org/GLM-5.1, GLM-5.1-FP8TokenizersBackend (explicit)TokenizersBackendtokenizer.json, no special_tokens_map.jsonNo; transformers ≥5.0 required
zai-org/GLM-4.6PreTrainedTokenizerPythonBackend (alias)tokenizer.json + dict extra_special_tokensNo
Qwen/Qwen3-0.6BdefaultTokenizersBackendFull setNo
Qwen/Qwen3.5-35B-A3B-BasedefaultTokenizersBackendBase flips EOS to <|endoftext|> vs <|im_end|> on InstructNo
deepseek-ai/DeepSeek-V3LlamaTokenizerFastTokenizersBackendtokenizer.json (7.85 MB LFS); added_tokens_decoder NOT in tokenizer_config.jsonNo
deepseek-ai/DeepSeek-R1LlamaTokenizerFastTokenizersBackend<think>/</think> only in chat_template.jinja, NOT in added_tokens_decoderNo
microsoft/phi-4GPT2TokenizerTokenizersBackendEOS is <|im_end|>; BOS is <|endoftext|> (inverted vs Qwen-Base)No
mistralai/Mistral-Small-24B-Instruct-2501LlamaTokenizerMistralCommonBackend if tekken.json present, else fast[INST]/[/INST] at ids 3/4No

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:

  1. 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.
  2. 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.
  3. ImmutableSandboxedEnvironment blocks mutation. Templates cannot .pop() messages or write to passed objects. Workarounds copy into locals.
  4. 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:

FunctionAnswers
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)

What ships with it: 9 files

116.2 KB alongside SKILL.md, 1 of them executable

Keep looking

Skills are one crate of 325,949. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.