From 2f86b55b3935435ee837cc8360a5d83b440d1445 Mon Sep 17 00:00:00 2001 From: Qubitium Date: Wed, 22 Jul 2026 05:05:40 +0000 Subject: [PATCH] Normalize Mistral tokenizer regexes --- README.md | 1 + pyproject.toml | 2 +- tests/test_loop_models.py | 1 + tests/test_mistral_regex.py | 90 +++++++++++++++++++++++++++++++++++++ tokenicer/tokenicer.py | 27 +++++++---- 5 files changed, 112 insertions(+), 9 deletions(-) create mode 100644 tests/test_mistral_regex.py diff --git a/README.md b/README.md index 9081ba8..f672900 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@

## News +* 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. * 03/03/2026 [0.0.7](https://github.com/ModelCloud/Tokenicer/releases/tag/v0.0.7): Fix Qwen 3.5 MoE compat. * 02/09/2026 [0.0.6](https://github.com/ModelCloud/Tokenicer/releases/tag/v0.0.6): Fix ChatGLM compat. * 09/04/2025 [0.0.5](https://github.com/ModelCloud/Tokenicer/releases/tag/v0.0.5): Fix `pad_token_id` detection for `LongCat` model. diff --git a/pyproject.toml b/pyproject.toml index f3132d3..65ca576 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ build-backend = "setuptools.build_meta" [project] name = "TokeNicer" -version = "0.0.13" +version = "0.0.14" description = "A (nicer) tokenizer you want to use for model `inference` and `training`: with all known peventable `gotchas` normalized or auto-fixed." readme = "README.md" requires-python = ">=3" diff --git a/tests/test_loop_models.py b/tests/test_loop_models.py index cb34b6a..42cc9bc 100644 --- a/tests/test_loop_models.py +++ b/tests/test_loop_models.py @@ -42,6 +42,7 @@ def should_skip_model_error(exc: Exception) -> bool: "No module named", "maximum recursion depth exceeded", "module 'torch' has no attribute 'None'", + "piece must not include null character", ) ) diff --git a/tests/test_mistral_regex.py b/tests/test_mistral_regex.py new file mode 100644 index 0000000..0267b7d --- /dev/null +++ b/tests/test_mistral_regex.py @@ -0,0 +1,90 @@ +# Copyright 2026 ModelCloud.ai +# Copyright 2026 qubitium@modelcloud.ai +# Contact: qubitium@modelcloud.ai, x.com/qubitium +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from unittest.mock import Mock, patch + +from huggingface_hub.errors import StrictDataclassClassValidationError + +from tokenicer import Tokenicer + + +class TestMistralRegexNormalization(unittest.TestCase): + @patch("tokenicer.tokenicer.AutoTokenizer.from_pretrained") + def test_mistral_regex_fix_is_enabled_by_default(self, from_pretrained): + expected = object() + from_pretrained.return_value = expected + + tokenizer = Tokenicer._load_tokenizer("poolside/Laguna-S-2.1", trust_remote_code=True) + + self.assertIs(tokenizer, expected) + from_pretrained.assert_called_once_with( + "poolside/Laguna-S-2.1", + trust_remote_code=True, + fix_mistral_regex=True, + ) + + @patch("tokenicer.tokenicer.AutoTokenizer.from_pretrained") + def test_explicit_mistral_regex_setting_is_preserved(self, from_pretrained): + from_pretrained.return_value = object() + + Tokenicer._load_tokenizer("poolside/Laguna-S-2.1", fix_mistral_regex=False) + + from_pretrained.assert_called_once_with( + "poolside/Laguna-S-2.1", + fix_mistral_regex=False, + ) + + @patch("tokenicer.tokenicer.Tokenicer._resolve_tokenizer_class") + @patch("tokenicer.tokenicer.tokenizer_class_name", return_value="FallbackTokenizer") + @patch("tokenicer.tokenicer.tokenizer_special_token_overrides", return_value={"bos_token": ""}) + @patch( + "tokenicer.tokenicer.AutoTokenizer.from_pretrained", + side_effect=StrictDataclassClassValidationError( + validator="validate_layer_type", + cause=ValueError("legacy model config"), + ), + ) + def test_mistral_regex_fix_is_preserved_on_fallback( + self, + auto_from_pretrained, + special_token_overrides, + tokenizer_class_name, + resolve_tokenizer_class, + ): + fallback_from_pretrained = Mock(return_value=object()) + resolve_tokenizer_class.return_value = Mock(from_pretrained=fallback_from_pretrained) + + Tokenicer._load_tokenizer("/tmp/Laguna-S-2.1", trust_remote_code=True) + + auto_from_pretrained.assert_called_once_with( + "/tmp/Laguna-S-2.1", + trust_remote_code=True, + fix_mistral_regex=True, + ) + special_token_overrides.assert_called_once_with("/tmp/Laguna-S-2.1") + tokenizer_class_name.assert_called_once_with("/tmp/Laguna-S-2.1") + resolve_tokenizer_class.assert_called_once_with("/tmp/Laguna-S-2.1", "FallbackTokenizer") + fallback_from_pretrained.assert_called_once_with( + "/tmp/Laguna-S-2.1", + trust_remote_code=True, + fix_mistral_regex=True, + bos_token="", + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tokenicer/tokenicer.py b/tokenicer/tokenicer.py index 24e3de1..2896528 100644 --- a/tokenicer/tokenicer.py +++ b/tokenicer/tokenicer.py @@ -28,9 +28,14 @@ from transformers.dynamic_module_utils import get_class_from_dynamic_module try: - from huggingface_hub.errors import StrictDataclassFieldValidationError + from huggingface_hub.errors import StrictDataclassError except Exception: # pragma: no cover - optional dependency path - StrictDataclassFieldValidationError = None + try: + # Compatibility with huggingface_hub versions that expose only the + # concrete field-validation error. + from huggingface_hub.errors import StrictDataclassFieldValidationError as StrictDataclassError + except Exception: + StrictDataclassError = None from .const import DEFAULT_PAD_TOKENS, MODEL_PAD_TOKEN_MAP from .util import ( @@ -54,8 +59,8 @@ KeyError, ) -if StrictDataclassFieldValidationError is not None: - _TOKENIZER_LOAD_EXCEPTIONS = _TOKENIZER_LOAD_EXCEPTIONS + (StrictDataclassFieldValidationError,) +if StrictDataclassError is not None: + _TOKENIZER_LOAD_EXCEPTIONS = _TOKENIZER_LOAD_EXCEPTIONS + (StrictDataclassError,) _KNOWN_LOAD_WARNING_SUPPRESSIONS = [ ( @@ -143,12 +148,18 @@ def load( @staticmethod def _load_tokenizer(pretrained_model_name_or_path: str, **kwargs): Tokenicer._install_tokenizer_compatibility_shims() + load_kwargs = dict(kwargs) + # Let Transformers repair affected Mistral-family pre-tokenizer regexes by + # default. Transformers applies this only when its compatibility detector + # matches, and callers can retain the serialized regex with an explicit + # ``fix_mistral_regex=False``. + load_kwargs.setdefault("fix_mistral_regex", True) try: # Keep the normal Transformers path first so standard checkpoints behave unchanged. - return AutoTokenizer.from_pretrained(pretrained_model_name_or_path, **kwargs) + return AutoTokenizer.from_pretrained(pretrained_model_name_or_path, **load_kwargs) except _TOKENIZER_LOAD_EXCEPTIONS: overrides = tokenizer_special_token_overrides(pretrained_model_name_or_path) - retry_kwargs = dict(kwargs) + retry_kwargs = dict(load_kwargs) retry_kwargs.update(overrides) 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): pretrained_model_name_or_path, ) - if kwargs.get("trust_remote_code", False) or not has_custom_tokenizer_code(pretrained_model_name_or_path): + if load_kwargs.get("trust_remote_code", False) or not has_custom_tokenizer_code(pretrained_model_name_or_path): raise - retry_kwargs = dict(kwargs) + retry_kwargs = dict(load_kwargs) retry_kwargs["trust_remote_code"] = True # Local checkpoints with custom tokenizer code can still succeed once remote code is explicitly allowed. logger.warning(