You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
File: src/transformers/tokenization_utils_base.py — lines 1813 and 1169 (on main @ 2026-08-07)
Python evaluates a call's arguments before the call, so .pop(...) always runs. If extra_special_tokens is already present — which is the case for any tokenizer_config.json that carries both keys — setdefault then declines to overwrite it and the popped list is thrown away. The tokenizer ends up receiving neither key.
The same file already contains the correct guarded form of this conversion at line 997 (PreTrainedTokenizerBase.__init__):
so the three conversion sites are inconsistent with each other. Two affected sites:
Line
Enclosing method
Guard
Impact
1813
PreTrainedTokenizerBase._from_pretrained
❌ eager-pop setdefault
every from_pretrained load
1169
PreTrainedTokenizerBase.add_special_tokens
❌ eager-pop setdefault
the publicadd_special_tokens() API
997
PreTrainedTokenizerBase.__init__
✅ explicit not in
correct
Introduced in b76caaa88d9d ("simplify extra tokens logic in base", #43230, merged 2026-01-29), which replaced the explicit ... and "extra_special_tokens" not in init_kwargs guard with setdefault. Present in v5.1.0 through current main; absent in v5.0.0.
Two distinct user-visible consequences, both observed on real checkpoints:
Hard failure for tokenizers whose __init__branches on additional_special_tokens is None — common in trust_remote_code tokenizers. They take their "no special tokens supplied, infer them" path on an ordinary load and can raise.
Silent correctness bug for everyone else: tokens the checkpoint explicitly declares as special are no longer special. len(tokenizer) and encode() of ordinary text are unchanged, so this passes unnoticed — only all_special_tokens / all_special_ids shrink.
Extra Environment Info
transformers: reproduced on 5.14.1; line present in v5.1.0 … main, absent in v5.0.0
Affected: any checkpoint whose tokenizer_config.json has a non-empty additional_special_tokensand an extra_special_tokens key. In a local corpus of 185 cached checkpoints, 40 match — e.g. allenai/Molmo2-8B (277 tokens), bharatgenai/Param2-17B (8007), baidu/ERNIE-4.5-* (1013), mistralai/Mistral-Large-3-675B / Mistral-Small-3.1-24B (1000), tiiuae/Falcon-H1-34B (843), skt/A.X-K1 (143), stepfun-ai/Step3-VL-10B (36), zai-org/GLM-4.5 / GLM-4.5V / GLM-4.7-Flash, internlm/internlm3-8b-instruct, internlm/Intern-S1-*, mispeech/midashenglm-7b, moonshotai/Kimi-VL-A3B-Instruct / Kimi-K2.5 / Kimi-Linear-48B, xlangai/OpenCUA-7B.
Severity: major — one class of tokenizer fails to load at all; the rest lose declared special tokens silently, which is the more dangerous outcome (no error, no warning, and ordinary-text tokenization is byte-identical, so tests that only check encode() still pass).
Why the existing v4→v5 tokenizer audit did not catch this: [vllm + v5 fix] handle TokenizersBackend fallback properly for v5 #44255 ("handle TokenizersBackend fallback properly for v5", merged 2026-03-04) audited 22 model converters and explicitly tested xlangai/OpenCUA-7B, reporting Samples compared: 32000 / Curr roundtrip OK: 32000/32000 (100.0%) / Changed samples: 0. That audit compares roundtrip_ok = (decoded == text) and the tokenizer's JSON serialization; it never inspects all_special_tokens / all_special_ids, which is the only thing this bug changes. It also ran with use_fast=True, resolving to TokenizersBackend rather than the repo's trust_remote_code class, so the hard-failure path was never exercised either. So OpenCUA passed that audit while being entirely unloadable via its own tokenizer class — the methodology, not the model, is why this stayed hidden. Downstream reports of the OpenCUA symptom, neither identifying this root cause: xlangai/OpenCUA-7B discussion #10, #11.
Suggested Fix
Restore the explicit guard at both buggy sites, so the value is only popped once it has a destination. Minimal and behavior-preserving whenever extra_special_tokens is absent:
--- a/src/transformers/tokenization_utils_base.py+++ b/src/transformers/tokenization_utils_base.py
@@ line ~1169, PreTrainedTokenizerBase.add_special_tokens
if "additional_special_tokens" in special_tokens_dict:
- special_tokens_dict.setdefault(- "extra_special_tokens", special_tokens_dict.pop("additional_special_tokens")- )+ if "extra_special_tokens" not in special_tokens_dict:+ special_tokens_dict["extra_special_tokens"] = special_tokens_dict.pop(+ "additional_special_tokens"+ )
@@ line ~1813, PreTrainedTokenizerBase._from_pretrained
# V5: Convert deprecated additional_special_tokens to extra_special_tokens
- if "additional_special_tokens" in init_kwargs:- init_kwargs.setdefault("extra_special_tokens", init_kwargs.pop("additional_special_tokens"))+ if "additional_special_tokens" in init_kwargs and "extra_special_tokens" not in init_kwargs:+ init_kwargs["extra_special_tokens"] = init_kwargs.pop("additional_special_tokens")
This matches the already-correct site at line 997 and makes all three consistent.
Worth deciding separately (a policy question, not part of the data-loss fix): when both keys are present and non-empty, silently preferring one is still lossy — merging them, or emitting a warning that the deprecated key is being ignored, would be friendlier than dropping data.
Two notes for whoever picks this up:
A fix that only stops the data loss but still strips the kwarg leaves consequence (1) intact for trust_remote_code tokenizers that declare additional_special_tokens in their own __init__ signature and branch on it. Consider passing the kwarg through when "additional_special_tokens" in inspect.signature(cls.__init__).parameters, or deprecating more loudly so those repos can migrate.
A regression test should assert on all_special_tokens / all_special_ids, not on len(tokenizer) or encode() — those are unaffected by this bug, which is exactly why it went unnoticed for ~6 months. This is not hypothetical: the audit in [vllm + v5 fix] handle TokenizersBackend fallback properly for v5 #44255 checked exactly those unaffected properties on this very checkpoint and reported 0 changed samples. Extending that comparison to the special-token sets would have caught it, and would guard the whole 22-model matrix against this class of regression.
Information
The official example scripts
My own modified scripts
Tasks
An officially supported task in the examples folder (such as GLUE/SQuAD, ...)
My own task or dataset (give details below)
Reproduction
No transformers, torch, model download, or hardware required — this is stdlib only:
d= {"additional_special_tokens": ["<|im_end|>", "<|media_placeholder|>"],
"extra_special_tokens": {}}
d.setdefault("extra_special_tokens", d.pop("additional_special_tokens")) # line 1813 verbatimprint(d) # {'extra_special_tokens': {}} <-- both special tokens are GONE
End-to-end with a real checkpoint (transformers>=5.1.0):
fromtransformersimportAutoTokenizer# tokenizer_config.json declares 9 additional_special_tokens AND extra_special_tokens: {}tk=AutoTokenizer.from_pretrained("moonshotai/Kimi-VL-A3B-Instruct", trust_remote_code=True)
print(len(tk.all_special_ids)) # 9 — expected 13fortin ("<|media_start|>", "<|media_content|>", "<|media_end|>", "<|media_pad|>"):
print(t, tintk.all_special_tokens) # all False, though all 4 are declared in the config
Hard-failure case (same root cause, raises instead of degrading silently):
# tokenizer_config.json declares 20 additional_special_tokens AND extra_special_tokens: {}AutoTokenizer.from_pretrained("xlangai/OpenCUA-7B", trust_remote_code=True)
# ValueError: unk_token should not be set in dumping mode when additional_special_tokens is None
Its remote-code TikTokenV3.__init__ guards a branch with if additional_special_tokens is None:; with the kwarg stripped, that branch runs on a normal load while unk_token/pad_token still arrive from the config, and it raises.
Stack Trace
File ".../transformers/models/auto/tokenization_auto.py", line 891, in from_pretrained
return tokenizer_class.from_pretrained(
File ".../transformers/tokenization_utils_base.py", line 1747, in from_pretrained
return cls._from_pretrained(
File ".../transformers/tokenization_utils_base.py", line 1943, in _from_pretrained
tokenizer = cls(*init_inputs, **init_kwargs)
File ".../transformers_modules/<hash>/tokenization_opencua.py", line 118, in __init__
raise ValueError("unk_token should not be set in dumping mode when additional_special_tokens is None")
ValueError: unk_token should not be set in dumping mode when additional_special_tokens is None
(Line numbers are from the installed v5.14.1; the setdefault is at :1823-1824 there and :1813 on main.)
Expected behavior
A checkpoint that declares additional_special_tokens should have those tokens honored regardless of whether extra_special_tokens is also present in its config. At minimum, the conversion must never destroy the value it fails to migrate.
System Info
transformersversion: 5.14.1Who can help?
Two v5 back-compat shims convert the deprecated
additional_special_tokenskwarg intoextra_special_tokensusing this expression:File:
src/transformers/tokenization_utils_base.py— lines 1813 and 1169 (onmain@ 2026-08-07)Python evaluates a call's arguments before the call, so
.pop(...)always runs. Ifextra_special_tokensis already present — which is the case for anytokenizer_config.jsonthat carries both keys —setdefaultthen declines to overwrite it and the popped list is thrown away. The tokenizer ends up receiving neither key.The same file already contains the correct guarded form of this conversion at line 997 (
PreTrainedTokenizerBase.__init__):so the three conversion sites are inconsistent with each other. Two affected sites:
PreTrainedTokenizerBase._from_pretrainedsetdefaultfrom_pretrainedloadPreTrainedTokenizerBase.add_special_tokenssetdefaultadd_special_tokens()APIPreTrainedTokenizerBase.__init__not inIntroduced in
b76caaa88d9d("simplify extra tokens logic in base", #43230, merged 2026-01-29), which replaced the explicit... and "extra_special_tokens" not in init_kwargsguard withsetdefault. Present in v5.1.0 through currentmain; absent in v5.0.0.Two distinct user-visible consequences, both observed on real checkpoints:
__init__branches onadditional_special_tokens is None— common intrust_remote_codetokenizers. They take their "no special tokens supplied, infer them" path on an ordinary load and can raise.len(tokenizer)andencode()of ordinary text are unchanged, so this passes unnoticed — onlyall_special_tokens/all_special_idsshrink.Extra Environment Info
main, absent in v5.0.0Context
b76caaa88d9d(simplify extra tokens logic in base #43230, v5.1.0). The pre-simplify extra tokens logic in base #43230 code was correct.tokenizer_config.jsonhas a non-emptyadditional_special_tokensand anextra_special_tokenskey. In a local corpus of 185 cached checkpoints, 40 match — e.g.allenai/Molmo2-8B(277 tokens),bharatgenai/Param2-17B(8007),baidu/ERNIE-4.5-*(1013),mistralai/Mistral-Large-3-675B/Mistral-Small-3.1-24B(1000),tiiuae/Falcon-H1-34B(843),skt/A.X-K1(143),stepfun-ai/Step3-VL-10B(36),zai-org/GLM-4.5/GLM-4.5V/GLM-4.7-Flash,internlm/internlm3-8b-instruct,internlm/Intern-S1-*,mispeech/midashenglm-7b,moonshotai/Kimi-VL-A3B-Instruct/Kimi-K2.5/Kimi-Linear-48B,xlangai/OpenCUA-7B.encode()still pass).extra_special_tokens, not this data loss: Fix_set_model_specific_special_tokensto accept list-formatextra_special_tokens#44781, Accept legacy list special_tokens in _set_model_specific_special_tokens #47372 (both closed, unmerged), and special_tokens, 'list' object has no attribute 'keys' #47110.TokenizersBackendfallback properly for v5", merged 2026-03-04) audited 22 model converters and explicitly testedxlangai/OpenCUA-7B, reportingSamples compared: 32000 / Curr roundtrip OK: 32000/32000 (100.0%) / Changed samples: 0. That audit comparesroundtrip_ok = (decoded == text)and the tokenizer's JSON serialization; it never inspectsall_special_tokens/all_special_ids, which is the only thing this bug changes. It also ran withuse_fast=True, resolving toTokenizersBackendrather than the repo'strust_remote_codeclass, so the hard-failure path was never exercised either. So OpenCUA passed that audit while being entirely unloadable via its own tokenizer class — the methodology, not the model, is why this stayed hidden. Downstream reports of the OpenCUA symptom, neither identifying this root cause: xlangai/OpenCUA-7B discussion #10, #11.Suggested Fix
Restore the explicit guard at both buggy sites, so the value is only popped once it has a destination. Minimal and behavior-preserving whenever
extra_special_tokensis absent:This matches the already-correct site at line 997 and makes all three consistent.
Worth deciding separately (a policy question, not part of the data-loss fix): when both keys are present and non-empty, silently preferring one is still lossy — merging them, or emitting a warning that the deprecated key is being ignored, would be friendlier than dropping data.
Two notes for whoever picks this up:
trust_remote_codetokenizers that declareadditional_special_tokensin their own__init__signature and branch on it. Consider passing the kwarg through when"additional_special_tokens" in inspect.signature(cls.__init__).parameters, or deprecating more loudly so those repos can migrate.all_special_tokens/all_special_ids, not onlen(tokenizer)orencode()— those are unaffected by this bug, which is exactly why it went unnoticed for ~6 months. This is not hypothetical: the audit in [vllm + v5 fix] handle TokenizersBackend fallback properly for v5 #44255 checked exactly those unaffected properties on this very checkpoint and reported 0 changed samples. Extending that comparison to the special-token sets would have caught it, and would guard the whole 22-model matrix against this class of regression.Information
Tasks
examplesfolder (such as GLUE/SQuAD, ...)Reproduction
No transformers, torch, model download, or hardware required — this is stdlib only:
End-to-end with a real checkpoint (
transformers>=5.1.0):Hard-failure case (same root cause, raises instead of degrading silently):
Its remote-code
TikTokenV3.__init__guards a branch withif additional_special_tokens is None:; with the kwarg stripped, that branch runs on a normal load whileunk_token/pad_tokenstill arrive from the config, and it raises.Stack Trace
(Line numbers are from the installed v5.14.1; the
setdefaultis at:1823-1824there and:1813onmain.)Expected behavior
A checkpoint that declares additional_special_tokens should have those tokens honored regardless of whether extra_special_tokens is also present in its config. At minimum, the conversion must never destroy the value it fails to migrate.