Skip to content

Commit 3718e2d

Browse files
authored
Normalize Mistral tokenizer regexes (#61)
1 parent 93f9312 commit 3718e2d

5 files changed

Lines changed: 112 additions & 9 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
</p>
1616

1717
## News
18+
* 07/22/2026 [0.0.14](https://github.com/ModelCloud/Tokenicer/releases/tag/v0.0.14): Auto-fix affected Mistral-family tokenizer regexes, including Laguna S 2.1.
1819
* 03/03/2026 [0.0.7](https://github.com/ModelCloud/Tokenicer/releases/tag/v0.0.7): Fix Qwen 3.5 MoE compat.
1920
* 02/09/2026 [0.0.6](https://github.com/ModelCloud/Tokenicer/releases/tag/v0.0.6): Fix ChatGLM compat.
2021
* 09/04/2025 [0.0.5](https://github.com/ModelCloud/Tokenicer/releases/tag/v0.0.5): Fix `pad_token_id` detection for `LongCat` model.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ build-backend = "setuptools.build_meta"
2121

2222
[project]
2323
name = "TokeNicer"
24-
version = "0.0.13"
24+
version = "0.0.14"
2525
description = "A (nicer) tokenizer you want to use for model `inference` and `training`: with all known peventable `gotchas` normalized or auto-fixed."
2626
readme = "README.md"
2727
requires-python = ">=3"

tests/test_loop_models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ def should_skip_model_error(exc: Exception) -> bool:
4242
"No module named",
4343
"maximum recursion depth exceeded",
4444
"module 'torch' has no attribute 'None'",
45+
"piece must not include null character",
4546
)
4647
)
4748

tests/test_mistral_regex.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# Copyright 2026 ModelCloud.ai
2+
# Copyright 2026 qubitium@modelcloud.ai
3+
# Contact: qubitium@modelcloud.ai, x.com/qubitium
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
17+
import unittest
18+
from unittest.mock import Mock, patch
19+
20+
from huggingface_hub.errors import StrictDataclassClassValidationError
21+
22+
from tokenicer import Tokenicer
23+
24+
25+
class TestMistralRegexNormalization(unittest.TestCase):
26+
@patch("tokenicer.tokenicer.AutoTokenizer.from_pretrained")
27+
def test_mistral_regex_fix_is_enabled_by_default(self, from_pretrained):
28+
expected = object()
29+
from_pretrained.return_value = expected
30+
31+
tokenizer = Tokenicer._load_tokenizer("poolside/Laguna-S-2.1", trust_remote_code=True)
32+
33+
self.assertIs(tokenizer, expected)
34+
from_pretrained.assert_called_once_with(
35+
"poolside/Laguna-S-2.1",
36+
trust_remote_code=True,
37+
fix_mistral_regex=True,
38+
)
39+
40+
@patch("tokenicer.tokenicer.AutoTokenizer.from_pretrained")
41+
def test_explicit_mistral_regex_setting_is_preserved(self, from_pretrained):
42+
from_pretrained.return_value = object()
43+
44+
Tokenicer._load_tokenizer("poolside/Laguna-S-2.1", fix_mistral_regex=False)
45+
46+
from_pretrained.assert_called_once_with(
47+
"poolside/Laguna-S-2.1",
48+
fix_mistral_regex=False,
49+
)
50+
51+
@patch("tokenicer.tokenicer.Tokenicer._resolve_tokenizer_class")
52+
@patch("tokenicer.tokenicer.tokenizer_class_name", return_value="FallbackTokenizer")
53+
@patch("tokenicer.tokenicer.tokenizer_special_token_overrides", return_value={"bos_token": "<s>"})
54+
@patch(
55+
"tokenicer.tokenicer.AutoTokenizer.from_pretrained",
56+
side_effect=StrictDataclassClassValidationError(
57+
validator="validate_layer_type",
58+
cause=ValueError("legacy model config"),
59+
),
60+
)
61+
def test_mistral_regex_fix_is_preserved_on_fallback(
62+
self,
63+
auto_from_pretrained,
64+
special_token_overrides,
65+
tokenizer_class_name,
66+
resolve_tokenizer_class,
67+
):
68+
fallback_from_pretrained = Mock(return_value=object())
69+
resolve_tokenizer_class.return_value = Mock(from_pretrained=fallback_from_pretrained)
70+
71+
Tokenicer._load_tokenizer("/tmp/Laguna-S-2.1", trust_remote_code=True)
72+
73+
auto_from_pretrained.assert_called_once_with(
74+
"/tmp/Laguna-S-2.1",
75+
trust_remote_code=True,
76+
fix_mistral_regex=True,
77+
)
78+
special_token_overrides.assert_called_once_with("/tmp/Laguna-S-2.1")
79+
tokenizer_class_name.assert_called_once_with("/tmp/Laguna-S-2.1")
80+
resolve_tokenizer_class.assert_called_once_with("/tmp/Laguna-S-2.1", "FallbackTokenizer")
81+
fallback_from_pretrained.assert_called_once_with(
82+
"/tmp/Laguna-S-2.1",
83+
trust_remote_code=True,
84+
fix_mistral_regex=True,
85+
bos_token="<s>",
86+
)
87+
88+
89+
if __name__ == "__main__":
90+
unittest.main()

tokenicer/tokenicer.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,14 @@
2828
from transformers.dynamic_module_utils import get_class_from_dynamic_module
2929

3030
try:
31-
from huggingface_hub.errors import StrictDataclassFieldValidationError
31+
from huggingface_hub.errors import StrictDataclassError
3232
except Exception: # pragma: no cover - optional dependency path
33-
StrictDataclassFieldValidationError = None
33+
try:
34+
# Compatibility with huggingface_hub versions that expose only the
35+
# concrete field-validation error.
36+
from huggingface_hub.errors import StrictDataclassFieldValidationError as StrictDataclassError
37+
except Exception:
38+
StrictDataclassError = None
3439

3540
from .const import DEFAULT_PAD_TOKENS, MODEL_PAD_TOKEN_MAP
3641
from .util import (
@@ -54,8 +59,8 @@
5459
KeyError,
5560
)
5661

57-
if StrictDataclassFieldValidationError is not None:
58-
_TOKENIZER_LOAD_EXCEPTIONS = _TOKENIZER_LOAD_EXCEPTIONS + (StrictDataclassFieldValidationError,)
62+
if StrictDataclassError is not None:
63+
_TOKENIZER_LOAD_EXCEPTIONS = _TOKENIZER_LOAD_EXCEPTIONS + (StrictDataclassError,)
5964

6065
_KNOWN_LOAD_WARNING_SUPPRESSIONS = [
6166
(
@@ -143,12 +148,18 @@ def load(
143148
@staticmethod
144149
def _load_tokenizer(pretrained_model_name_or_path: str, **kwargs):
145150
Tokenicer._install_tokenizer_compatibility_shims()
151+
load_kwargs = dict(kwargs)
152+
# Let Transformers repair affected Mistral-family pre-tokenizer regexes by
153+
# default. Transformers applies this only when its compatibility detector
154+
# matches, and callers can retain the serialized regex with an explicit
155+
# ``fix_mistral_regex=False``.
156+
load_kwargs.setdefault("fix_mistral_regex", True)
146157
try:
147158
# Keep the normal Transformers path first so standard checkpoints behave unchanged.
148-
return AutoTokenizer.from_pretrained(pretrained_model_name_or_path, **kwargs)
159+
return AutoTokenizer.from_pretrained(pretrained_model_name_or_path, **load_kwargs)
149160
except _TOKENIZER_LOAD_EXCEPTIONS:
150161
overrides = tokenizer_special_token_overrides(pretrained_model_name_or_path)
151-
retry_kwargs = dict(kwargs)
162+
retry_kwargs = dict(load_kwargs)
152163
retry_kwargs.update(overrides)
153164

154165
tokenizer_cls_name = tokenizer_class_name(pretrained_model_name_or_path)
@@ -185,10 +196,10 @@ def _load_tokenizer(pretrained_model_name_or_path: str, **kwargs):
185196
pretrained_model_name_or_path,
186197
)
187198

188-
if kwargs.get("trust_remote_code", False) or not has_custom_tokenizer_code(pretrained_model_name_or_path):
199+
if load_kwargs.get("trust_remote_code", False) or not has_custom_tokenizer_code(pretrained_model_name_or_path):
189200
raise
190201

191-
retry_kwargs = dict(kwargs)
202+
retry_kwargs = dict(load_kwargs)
192203
retry_kwargs["trust_remote_code"] = True
193204
# Local checkpoints with custom tokenizer code can still succeed once remote code is explicitly allowed.
194205
logger.warning(

0 commit comments

Comments
 (0)