# PR #54192 完整报告

- 仓库：`vllm-project/vllm`
- 标题：[Bugfix] Avoid MistralCommonBackend for HF tokenizers
- 合并时间：2026-09-11 01:57
- 原文链接：http://prhub.com.cn/vllm-project/vllm/pull/54192

---

# 执行摘要

- 一句话：hf 模式禁用 mistral-common 回退
- 推荐动作：值得精读，尤其是负责 tokenizer、结构化输出与 Mistral 模型的同学。关注三个设计决策：(1) 为什么选择“在注册层强制 `mistral_format=False`”而不是继续在 processor 层包装 tokenizer（对比被 supersede 的 #50752）；(2) 对老版本 transformers 采用“显式报错”而非“静默降级”，以及反对“让用户卸载 mistral-common”的理由（mistral 工具解析器与 hf 分词器是合法组合）；(3) 删除兼容层时同步收缩了哪些隐式契约（`CachedTokenizer` 不再覆写解码函数、语法后端不再接受 MCommon）。阅读时建议同时看 `tests/tokenizers_/test_registry.py` 的 8 组参数矩阵，理解闸门的触发条件；并在升级 transformers 到 5.15+ 后确认那个 TODO 能否删除。

# 功能与动机

Issue #50706 报告：HF 格式的 Mistral3 checkpoint 用默认参数初始化时，在 multimodal memory profiling 阶段（用户尚未发请求）就抛 `ValueError: Failed to apply PixtralProcessor on data={'text': '[IMG]'}`；根因是 transformers 5.x 对双格式仓库的 AutoTokenizer 返回 MistralCommonBackend，而 mistral-common 刻意不为文本中的 special token 编码，`encode("[IMG]")` 得到字面量而非 `[IMG]` 的 id，`PixtralProcessor._check_special_mm_tokens` 在文本里数到 1 个占位符、在 input_ids 里数到 0 个。作者 juliendenize 在 #50706 评论中提出：tokenizer_mode="hf" 是用户显式要求走 HF 格式，这条路径本就不该被触达，因此“removing MistralCommonBackend support for HF tokenizer path”，并引用 transformers 新能力（huggingface/transformers#47507 允许 `mistral_format=False` 永不回退到 MistralCommonBackend）；hmellor 回复 “I agree that this path should never be reached by vLLM”，并要求在 #50706 对齐后再合。PR body 同时声明本 PR supersedes #50752——后者在 processor 层给 tokenizer 打补丁（换一个会编码 special token 的子类），本 PR 改为在注册层直接不让该后端出现。显式 tokenizer_mode="mistral" 的行为保持不变。

# 实现拆解

1. **入口参数归一（`vllm/tokenizers/registry.py::resolve_tokenizer_args`）**：在模式解析末尾新增 `hf` 分支——若 kwargs 中已有 `mistral_format=True` 则直接 `raise ValueError("mistral_format=True is not supported with tokenizer_mode='hf'")`，否则统一写入 `kwargs["mistral_format"] = False`。这样即便仓库同时存在 `tekken.json` 与 `tokenizer.json`，transformers 也不会把 `AutoTokenizer` 解析成 `MistralCommonBackend`；`slow` 模式先归一到 `hf`，自然继承同一约束；`auto` 模式仍保留“Mistral 仓库 + tekken 文件 → `mistral` 模式”的既有优先逻辑。顺带把 `token` 透传给 `is_mistral_model_repo`、`any_pattern_in_repo_files` 与新增的 `get_config(..., token=...)`，使私有仓库鉴权在注册阶段生效。
2. **版本闸门（`vllm/tokenizers/registry.py::get_tokenizer`）**：新增模块常量 `_MIN_TRANSFORMERS_VERSION_FOR_HF_MISTRAL = Version("5.15.0")`（5.15 是首个支持 `mistral_format` 的版本）。当 `tokenizer_mode == "hf"`、transformers 版本偏低、`is_mistral_common_available()` 为真、`_HF_TOKENIZER_MAPPING_NAMES.get(config.model_type) == "MistralCommonBackend"` 且仓库含 `tekken.json`/`tokenizer.model.v*` 时抛出可操作的 `ValueError`（升级 transformers 或删除 Mistral 分词器文件），并留 `TODO: delete when Transformers version dependency is bumped >= 5.15.0`，属有明确回收期限的临时闸门。
3. **删除兼容层（4 个文件）**：`vllm/tokenizers/hf.py::get_cached_tokenizer` 不再探测 `tokenizer.tokenizer.instruct_tokenizer` 构造 `mistral_tekkenizer`，`CachedTokenizer` 同时去掉 `convert_ids_to_tokens` / `convert_tokens_to_string` 覆写（含 `NotImplementedError` → `"".join(tokens)` 兜底），代理退回“纯缓存”职责；`vllm/v1/structured_output/utils.py` 删除 `maybe_wrap_mistral_common_tokenizer` 与 `from transformers import MistralCommonBackend`；`vllm/v1/structured_output/backend_guidance.py::GuidanceBackend.__post_init__` 删除 `isinstance(self.tokenizer, MistralCommonBackend)` + `mistral_common.guidance.tokenizer.from_mistral_tokenizer` 分支；`vllm/v1/structured_output/__init__.py` 调用点改为直接用 `cached_tokenizer_from_config`。
4. **测试配套（一删一增）**：删除 `tests/v1/structured_output/test_mistral_common_tokenizer.py`（102 行，原覆盖 wrap 后 `MistralTokenizer` 类型、`_reduced_vocabulary` 的等价 id 去重、xgrammar/outlines 接受合法 JSON 与拒绝非法 token、xgrammar 拒绝未包装 tokenizer）；在 `tests/tokenizers_/test_registry.py` 新增三个参数化用例——`test_resolve_tokenizer_args_forces_hf_mistral_format_false`（hf/slow × 带不带 `mistral_format=False`）、`test_resolve_tokenizer_args_rejects_hf_mistral_format_true`、`test_get_tokenizer_handles_hf_mistral_transformers_compatibility`（transformers 5.14/5.15 × mistral/mistral3/mixtral/ 无 tekken 文件共 8 组矩阵）。测试通过 patch `get_config`、`vllm.tokenizers.registry.transformers.__version__`、`CachedHfTokenizer.from_pretrained` 实现，CPU-only、无网络、不需权重。
5. **顺带清理**：`_TokenizerRegistry.register` 去掉多余的 `return None`；`import vllm.envs as envs` 改为更规范的 `from vllm import envs`。

| 文件 | 动作 | 关键符号 |
| --- | --- | --- |
| `vllm/tokenizers/registry.py` | +41/-4 | `resolve_tokenizer_args`、`get_tokenizer`、`_MIN_TRANSFORMERS_VERSION_FOR_HF_MISTRAL` |
| `vllm/tokenizers/hf.py` | +2/-30 | `get_cached_tokenizer`、`CachedTokenizer.convert_ids_to_tokens/convert_tokens_to_string`（删） |
| `vllm/v1/structured_output/utils.py` | +0/-13 | `maybe_wrap_mistral_common_tokenizer`（删） |
| `vllm/v1/structured_output/backend_guidance.py` | +0/-5 | `GuidanceBackend.__post_init__` |
| `vllm/v1/structured_output/__init__.py` | +2/-3 | `StructuredOutputManager.__init__` 调用点 |
| `tests/tokenizers_/test_registry.py` | +84/-0 | 三个新参数化测试 |
| `tests/v1/structured_output/test_mistral_common_tokenizer.py` | +0/-102（删除） | 旧 MCommon 结构化输出测试 |

关键文件：
- `vllm/tokenizers/registry.py`（模块 分词器注册；类别 source；类型 core-logic；符号 resolve_tokenizer_args, get_tokenizer, _MIN_TRANSFORMERS_VERSION_FOR_HF_MISTRAL, _TokenizerRegistry.register）: 本 PR 的核心：在 `resolve_tokenizer_args` 强制 hf 模式 `mistral_format=False`、拒绝冲突的 `mistral_format=True`，并在 `get_tokenizer` 新增 transformers < 5.15 的版本闸门；同时补上 `token` 透传。
- `vllm/tokenizers/hf.py`（模块 分词器缓存；类别 source；类型 refactor；符号 get_cached_tokenizer, CachedTokenizer.convert_ids_to_tokens, CachedTokenizer.convert_tokens_to_string, mistral_tekkenizer）: `get_cached_tokenizer` 移除 mistral_tekkenizer 探测，`CachedTokenizer` 去掉 tekken 特化的 `convert_ids_to_tokens`/`convert_tokens_to_string` 与 NotImplementedError 兜底，是“兼容层删除”最实质的一处（+2/-30）。
- `tests/tokenizers_/test_registry.py`（模块 分词器测试；类别 test；类型 test-coverage；符号 test_resolve_tokenizer_args_forces_hf_mistral_format_false, test_resolve_tokenizer_args_rejects_hf_mistral_format_true, test_get_tokenizer_handles_hf_mistral_transformers_compatibility）: 新增 84 行测试，用参数化矩阵锁定新契约：hf/slow 都强制 mistral_format=False、显式 True 被拒绝、transformers 5.14/5.15 下的兼容性差异。
- `vllm/v1/structured_output/utils.py`（模块 结构化输出；类别 source；类型 cleanup；符号 maybe_wrap_mistral_common_tokenizer）: 删除 `maybe_wrap_mistral_common_tokenizer` —— 结构化输出侧把 MCommon 包成 MistralTokenizer 的唯一入口；其消失是“hf 路径不再产出该后端”的直接后果。
- `vllm/v1/structured_output/backend_guidance.py`（模块 引导后端；类别 source；类型 cleanup；符号 GuidanceBackend.__post_init__）: `GuidanceBackend.__post_init__` 删除 `MistralCommonBackend` + `mistral_common.guidance.tokenizer.from_mistral_tokenizer` 分支，语法后端只保留 mistral 原生与 llguidance HF 两条路径。
- `vllm/v1/structured_output/__init__.py`（模块 结构化引擎；类别 source；类型 dependency-wiring；符号 StructuredOutputManager.__init__）: `StructuredOutputManager.__init__` 不再调用 `maybe_wrap_mistral_common_tokenizer`，改为直接 `cached_tokenizer_from_config`，是兼容层删除的调用点收口。
- `tests/v1/structured_output/test_mistral_common_tokenizer.py`（模块 输出测试；类别 test；类型 deletion；符号 loaded_tokenizer, wrapped_tokenizer, test_wrapping_fixes_the_outlines_vocabulary, test_backend_accepts_a_valid_document）: 整文件删除（102 行），是唯一直接验证 MCommon 包装后 vocab 等价性与 xgrammar/outlines grammar 接受度的测试，删除后语法后端侧端到端覆盖实际下降。

关键符号：resolve_tokenizer_args, get_tokenizer, get_cached_tokenizer, CachedTokenizer.convert_ids_to_tokens（删除）, CachedTokenizer.convert_tokens_to_string（删除）, maybe_wrap_mistral_common_tokenizer（删除）, GuidanceBackend.__post_init__, StructuredOutputManager.__init__

## 关键源码片段

### `vllm/tokenizers/registry.py`

本 PR 的核心：在 `resolve_tokenizer_args` 强制 hf 模式 `mistral_format=False`、拒绝冲突的 `mistral_format=True`，并在 `get_tokenizer` 新增 transformers < 5.15 的版本闸门；同时补上 `token` 透传。

```python
# vllm/tokenizers/registry.py（PR 后整理出的关键实现，含两处核心改动）

# HF 原生 Mistral 分词器要接受 `mistral_format` 参数，需要 transformers >= 5.15；
# 低于该版本时 transformers 仍会把双格式仓库解析成 MistralCommonBackend，
# 这正是 #50706 崩溃的根因，因此设一个带 TODO 的版本闸门。
_MIN_TRANSFORMERS_VERSION_FOR_HF_MISTRAL = Version("5.15.0")


def resolve_tokenizer_args(
    tokenizer_name: str | Path,
    *args,
    runner_type: "RunnerType" = "generate",
    tokenizer_mode: str = "auto",
    **kwargs,
):
    # ...（revision / download_dir / truncation_side 等未改动逻辑略）

    if tokenizer_mode == "slow":
        if kwargs.get("use_fast", False):
            raise ValueError("Cannot use the fast tokenizer in slow tokenizer mode.")
        # slow 模式只是 use_fast=False 的 hf，后续同样受 mistral_format 约束
        tokenizer_mode = "hf"
        kwargs["use_fast"] = False

    # auto 模式下，只有 Mistral 仓库且带 tekken.json / tokenizer.model.v* 才选官方分词器；
    # token= 是本次新增的透传，用于私有仓库的 Hub 鉴权
    if (
        tokenizer_mode == "auto"
        and is_mistral_model_repo(
            model_name_or_path=str(tokenizer_name),
            revision=revision,
            token=kwargs.get("token"),
        )
        and any_pattern_in_repo_files(
            model_name_or_path=str(tokenizer_name),
            allow_patterns=["tekken.json", "tokenizer.model.v*"],
            revision=revision,
            token=kwargs.get("token"),
        )
    ):
        tokenizer_mode = "mistral"

    if tokenizer_mode == "auto":
        tokenizer_mode = "hf"

    # 核心改动：hf 模式显式声明 mistral_format=False，
    # 让 transformers 走 HF 的 tokenizer.json、永不回退到 MistralCommonBackend；
    # 用户显式要求 mistral_format=True 时直接报错，而不是静默改写其语义
    if tokenizer_mode == "hf":
        if kwargs.pop("mistral_format", False):
            raise ValueError(
                "mistral_format=True is not supported with tokenizer_mode='hf'"
            )
        kwargs["mistral_format"] = False

    return tokenizer_mode, tokenizer_name, args, kwargs


def get_tokenizer(tokenizer_name, *args, tokenizer_cls=TokenizerLike, **kwargs):
    # ...（tokenizer_cls 解析、config 加载、config_format 选择等逻辑略）

    # TODO: delete when Transformers version dependency is bumped >= 5.15.0
    # 老版本 transformers 没有 mistral_format，无法保证走 HF 后端；
    # 与其继续崩在 PixtralProcessor，不如在加载前给出可操作的报错
    if (
        tokenizer_mode == "hf"
        and Version(transformers.__version__) < _MIN_TRANSFORMERS_VERSION_FOR_HF_MISTRAL
        and is_mistral_common_available()
        and _HF_TOKENIZER_MAPPING_NAMES.get(getattr(config, "model_type", None))
        == "MistralCommonBackend"
        and any_pattern_in_repo_files(
            model_name_or_path=str(tokenizer_name),
            allow_patterns=["tekken.json", "tokenizer.model.v*"],
            revision=revision,
            token=kwargs.get("token"),
        )
    ):
        raise ValueError(
            "Loading Mistral models with tokenizer_mode='hf' requires "
            f"transformers>={_MIN_TRANSFORMERS_VERSION_FOR_HF_MISTRAL}. "
            "Please upgrade transformers or delete Mistral tokenizer files."
        )

```

### `vllm/tokenizers/hf.py`

`get_cached_tokenizer` 移除 mistral_tekkenizer 探测，`CachedTokenizer` 去掉 tekken 特化的 `convert_ids_to_tokens`/`convert_tokens_to_string` 与 NotImplementedError 兜底，是“兼容层删除”最实质的一处（+2/-30）。

```python
# vllm/tokenizers/hf.py — 缓存代理在 PR 之后的形态（净删 28 行）

def get_cached_tokenizer(tokenizer: HfTokenizer) -> HfTokenizer:
    """transformers 默认每次调用都重算属性，这里用子类代理把它们缓存下来。"""
    cached_tokenizer = copy.copy(tokenizer)

    tokenizer_all_special_ids = tokenizer.all_special_ids
    tokenizer_all_special_tokens = tokenizer.all_special_tokens
    tokenizer_vocab = tokenizer.get_vocab()
    tokenizer_len = len(tokenizer)
    # 底层 tokenizer 可能是任意特定 backend，不保证实现 is_fast，保留 getattr 兜底
    tokenizer_is_fast = getattr(tokenizer, "is_fast", True)

    # 删除点 1：这里原本会探测 tokenizer.tokenizer.instruct_tokenizer，
    # 为 MistralCommonBackend 构造 mistral_tekkenizer，
    # 供下面被删掉的 convert_* 覆写做 byte-fallback 感知的解码。
    # hf 路径不再产出 MistralCommonBackend，这段探测整体删除。

    max_token_id = max(tokenizer_vocab.values())
    max_chars_per_token = max(len(tok) for tok in tokenizer_vocab)

    class CachedTokenizer(tokenizer.__class__):  # type: ignore
        @property
        def all_special_ids(self) -> list[int]:
            return tokenizer_all_special_ids

        @property
        def all_special_tokens(self) -> list[str]:
            return tokenizer_all_special_tokens

        @property
        def max_token_id(self) -> int:
            return max_token_id

        @property
        def max_chars_per_token(self) -> int:
            return max_chars_per_token

        @property
        def is_fast(self) -> bool:
            return tokenizer_is_fast

        # 删除点 2：原来的 convert_ids_to_tokens / convert_tokens_to_string
        # 覆写（含 NotImplementedError -> "".join(tokens) 兜底）被移除，
        # CachedTokenizer 退回“纯缓存”职责，解码统一走父类实现。

        def get_vocab(self) -> dict[str, int]:
            return tokenizer_vocab

        def __len__(self) -> int:
            return tokenizer_len

        def __reduce__(self):
            return get_cached_tokenizer, (tokenizer,)

    CachedTokenizer.__name__ = f"Cached{tokenizer.__class__.__name__}"
    cached_tokenizer.__class__ = CachedTokenizer
    return cached_tokenizer

```

### `vllm/v1/structured_output/backend_guidance.py`

`GuidanceBackend.__post_init__` 删除 `MistralCommonBackend` + `mistral_common.guidance.tokenizer.from_mistral_tokenizer` 分支，语法后端只保留 mistral 原生与 llguidance HF 两条路径。

```python
# vllm/v1/structured_output/backend_guidance.py

@dataclass
class GuidanceBackend(StructuredOutputBackend):
    def __post_init__(self):
        self.disable_any_whitespace = (
            self.vllm_config.structured_outputs_config.disable_any_whitespace
        )
        self.disable_additional_properties = (
            self.vllm_config.structured_outputs_config.disable_additional_properties
        )

        if is_mistral_tokenizer(self.tokenizer):
            # --tokenizer-mode mistral：直接用 MistralTokenizer 自带的 llguidance 分词器
            self.ll_tokenizer = self.tokenizer.llg_tokenizer
        else:
            # 删除点：这里原本还有一条分支
            # elif isinstance(self.tokenizer, MistralCommonBackend):
            # from mistral_common.guidance.tokenizer import from_mistral_tokenizer
            # self.ll_tokenizer = from_mistral_tokenizer(self.tokenizer.tokenizer)
            # hf 模式强制 mistral_format=False 之后，MistralCommonBackend 不会再流入
            # 语法后端，这条兜底分支连同 mistral-common 的导入一起删除。
            self.ll_tokenizer = llguidance_hf.from_tokenizer(
                self.tokenizer, max(self.vocab_size, len(self.tokenizer))
            )

```

# 评论区精华

1. **版本闸门由 review 直接催生**：hmellor 在 `vllm/tokenizers/registry.py` 上评论 “We need another error for the situation where the user wants `hf` tokenizer mode on a Mistral model when transformers version is <5.15. 5.15 is the earliest version with `mistral_format`”。这直接产生了 commit `ca8b7aa48676` “Guard HF Mistral tokenizers on old Transformers” 与 `_MIN_TRANSFORMERS_VERSION_FOR_HF_MISTRAL` 常量及对应参数化测试，是本次 review 中最实质的设计补充。
2. **“卸载 mistral-common”这条替代方案被否决**：hmellor 提出 “There is a third solution, uninstalling `mistral-common`”；juliendenize 反驳称 mistral-common 是不加守卫的依赖，“mixing tool call parser mistral and hf tokenizer (which is valid combination) would not be possible by uninstalling mistral-common as it would crash”，hmellor 回 “Good point”。结论是保留显式报错 + 升级提示，而不是让用户卸载依赖。
3. **路径归属的共识**：issue #50706 讨论中作者主张 hf 路径不应触达 MCommon，hmellor “I agree that this path should never be reached by vLLM”，随后才允许推进合并；这是本 PR 以“删除兼容层”而非“继续打补丁”的决策依据。
4. **未解决的疑虑**：没有任何 reviewer 追问 `kwargs["mistral_format"] = False` 会被写到所有 hf 模式加载上（包括非 Mistral 模型）这一副作用，也未讨论 `vllm/tokenizers/mistral.py` 中 `mistral_common_tekkenizer` 等辅助函数是否会变成死代码。

- transformers < 5.15 时需要额外的显式报错 (correctness): 采纳，新增版本闸门与可操作错误信息，并留 TODO 待 transformers 版本依赖提升到 >= 5.15 后删除。
- 是否可以让用户卸载 mistral-common 作为第三种解法 (design): 不采用卸载方案，保留显式 ValueError 与升级 / 删除文件的提示。
- hf 路径是否应该完全不再触达 MistralCommonBackend (design): 达成共识：在注册层从源头阻断该后端，删除三处兼容层；显式 tokenizer_mode="mistral" 路径不动。

# 风险与影响

- 风险：
 1. **兼容性 / 破坏性变更**：在 transformers < 5.15 且仓库含 tekken 文件时，`tokenizer_mode="hf"` 从“能启动”变为启动即 `ValueError`（`vllm/tokenizers/registry.py::get_tokenizer`）。这是有意的显式失败，但对钉住旧版 transformers 的部署是升级阻断，需要用户改依赖或删除 Mistral 分词器文件。
 2. **参数外溢风险**：`resolve_tokenizer_args` 对 **所有 **hf 模式加载都注入 `mistral_format=False`，非 Mistral 模型（如 GPT-2、Qwen）也会带着这个 kwarg 进入 `AutoTokenizer.from_pretrained`，其可靠性依赖 transformers 对未知 kwargs 的容错；相关测试把 `CachedHfTokenizer.from_pretrained` mock 掉了，未覆盖真实 AutoTokenizer 路径。
 3. **结构化输出回归面**：删除 `maybe_wrap_mistral_common_tokenizer`（`vllm/v1/structured_output/utils.py`）与 `GuidanceBackend.__post_init__` 的 MCommon 分支后，若仍有其它入口把 `MistralCommonBackend` 送进语法后端（例如未来 transformers 行为变化或用户自行指定 `tokenizer_cls`），错误会退化为 xgrammar 的 “Unsupported tokenizer type”，可诊断性低于原来的自动包装。
 4. **解码兜底被移除**：`vllm/tokenizers/hf.py` 删除 `convert_tokens_to_string` 的 `NotImplementedError` → `"".join(tokens)` 兜底，而该函数被 `_reduced_vocabulary` 依赖；若仍有不实现该方法的底层 tokenizer 走 `CachedTokenizer`，会由静默降级变为抛异常。
 5. **测试覆盖结构变化**：被删的 102 行测试是唯一直接验证“Mistral tokenizer + xgrammar/outlines grammar 接受度”的用例，新测试只覆盖参数解析与版本闸门（且大量依赖 mock），对语法后端行为的端到端覆盖实际是下降的。
- 影响：
 - **用户侧**：Mistral/Mistral3/Mixtral 双格式仓库在默认（auto）模式下仍优先走 mistral 模式，行为不变；但显式 `--tokenizer-mode hf` 的用户必须以 transformers >= 5.15 为前提，否则启动即报错。修复后默认参数初始化不再崩溃，多模态 profiling 可正常完成。
 - **系统侧**：分词器与结构化输出之间少了一层隐式类型转换，`CachedTokenizer`、`GuidanceBackend`、`StructuredOutputManager` 的输入约束更窄、更可预测；`token` 透传补齐了私有仓库在注册阶段鉴权的缺口。
 - **维护侧**：净删 28 行代码 + 102 行测试，去掉“在 HF processor 层给 MCommon 打补丁”这条路（#50752 的方案），团队不再需要维护 processor 级兼容包装；代价是新增一处带 TODO 的版本依赖检查，需要 transformers 版本升级后回收。
 - 风险标记：破坏性版本门槛 , 所有 hf 加载都注入 mistral_format=False, 结构化输出兼容层删除 , 真实 AutoTokenizer 路径测试盲区 , 可能遗留 tekken 辅助函数死代码

# 关联脉络

- PR #50752 [Bugfix] Encode special tokens in HF processors for transformers-native mistral-common tokenizers: 被本 PR 显式 supersede 的同因修复：它在 processor 层把 MistralCommonBackend 替换为会编码文本内 special token 的子类，本 PR 改为在 tokenizer 注册层强制 mistral_format=False，从源头避免该后端出现，因此删除了那类包装需求。
- PR #45525 修复 vLLM MistralTokenizer 路径的 special-token 编码（材料仅给出编号，未提供标题）: PR body 说明其以 is_mistral_tokenizer() 守卫修复同一崩溃的另一条路径（--tokenizer-mode mistral），与本 PR 互补；#50752 中提及的 _text_to_ids special-token 编码思路也源自它，本 PR 使两条路径在语义上收敛。