diff --git a/docs/features/tool_calling.md b/docs/features/tool_calling.md
index fb70c50248..6527eccd09 100644
--- a/docs/features/tool_calling.md
+++ b/docs/features/tool_calling.md
@@ -458,6 +458,17 @@ Supported models:
Flags: `--tool-call-parser qwen3_xml`
+### MiMo-V2.6 Models (`mimo`)
+
+Supported models:
+
+* `XiaomiMiMo/MiMo-V2.6-Pro-RL`
+* `XiaomiMiMo/MiMo-V2.6-Flash-RL`
+
+Use the checkpoint's chat template with `--tool-call-parser mimo --reasoning-parser mimo --enable-auto-tool-choice`.
+For schema-constrained tool arguments, set `strict: true` inside each tool's `function`, or use `--tool-strict-level parameter`.
+The parser supports `auto`, `required`, named tools, and `parallel_tool_calls=False`, preserving leading/trailing whitespace in string arguments.
+
### Olmo 3 Models (`olmo3`)
Olmo 3 models output tool calls in a format that is very similar to the one expected by the `pythonic` parser (see below), with a few differences. Each tool call is a pythonic string, but the parallel tool calls are newline-delimited, and the calls are wrapped within XML tags as `..`. In addition, the parser also allows JSON boolean and null literals (`true`, `false`, and `null`) in addition to the pythonic ones (`True`, `False`, and `None`).
diff --git a/requirements/common.txt b/requirements/common.txt
index ab0060284c..1958173fd7 100644
--- a/requirements/common.txt
+++ b/requirements/common.txt
@@ -26,7 +26,7 @@ lm-format-enforcer == 0.11.3
llguidance >= 1.7.0, < 1.8.0; platform_machine == "x86_64" or platform_machine == "arm64" or platform_machine == "aarch64" or platform_machine == "ppc64le" or platform_machine == "s390x"
outlines_core == 0.2.14
lark == 1.2.2
-xgrammar == 0.2.7; platform_machine == "x86_64" or platform_machine == "aarch64" or platform_machine == "arm64" or platform_machine == "s390x" or platform_machine == "ppc64le"
+xgrammar == 0.2.8; platform_machine == "x86_64" or platform_machine == "aarch64" or platform_machine == "arm64" or platform_machine == "s390x" or platform_machine == "ppc64le"
typing_extensions >= 4.10
filelock >= 3.16.1 # need to contain https://github.com/tox-dev/filelock/pull/317
partial-json-parser # used for parsing partial JSON outputs
diff --git a/requirements/rocm.txt b/requirements/rocm.txt
index 824e2e1201..85b196e401 100644
--- a/requirements/rocm.txt
+++ b/requirements/rocm.txt
@@ -24,7 +24,7 @@ timm>=1.0.17
amd-quark==0.12.post1
tilelang==0.1.10
# Required apache-tvm-ffi matching tilelang version
-apache-tvm-ffi==0.1.10
+apache-tvm-ffi==0.1.11
# Required for faster safetensors model loading
fastsafetensors >= 0.3.3
# Mooncake transfer engine for KV cache offloading/transfer (ROCm build, published on PyPI)
diff --git a/requirements/test/cpu.txt b/requirements/test/cpu.txt
index ab3e42886c..e8dd282121 100644
--- a/requirements/test/cpu.txt
+++ b/requirements/test/cpu.txt
@@ -1319,7 +1319,7 @@ word2number==1.1
# via lm-eval
wrapt==1.17.2
# via smart-open
-xgrammar==0.2.7
+xgrammar==0.2.8
# via -r requirements/test/../common.txt
xxhash==3.5.0
# via
diff --git a/requirements/test/cuda.txt b/requirements/test/cuda.txt
index ffbc639c4c..b7711a10cf 100644
--- a/requirements/test/cuda.txt
+++ b/requirements/test/cuda.txt
@@ -1429,7 +1429,7 @@ word2number==1.1
# via lm-eval
wrapt==1.17.2
# via smart-open
-xgrammar==0.2.7
+xgrammar==0.2.8
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
diff --git a/requirements/test/rocm.txt b/requirements/test/rocm.txt
index 36e1ba182d..3581e5fea9 100644
--- a/requirements/test/rocm.txt
+++ b/requirements/test/rocm.txt
@@ -43,7 +43,7 @@ anyio==4.13.0
# sse-starlette
# starlette
# watchfiles
-apache-tvm-ffi==0.1.10
+apache-tvm-ffi==0.1.11
# via
# -c requirements/rocm.txt
# tilelang
@@ -1380,7 +1380,7 @@ word2number==1.1
# via lm-eval
wrapt==2.1.2
# via smart-open
-xgrammar==0.2.7
+xgrammar==0.2.8
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
diff --git a/requirements/test/xpu.txt b/requirements/test/xpu.txt
index e5f898adcc..2bcd3664c1 100644
--- a/requirements/test/xpu.txt
+++ b/requirements/test/xpu.txt
@@ -1218,7 +1218,7 @@ werkzeug==3.1.8
# via schemathesis
word2number==1.1
# via lm-eval
-xgrammar==0.2.7
+xgrammar==0.2.8
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
diff --git a/tests/parser/engine/test_qwen3.py b/tests/parser/engine/test_qwen3.py
index 07a025876a..7bdddb2faf 100644
--- a/tests/parser/engine/test_qwen3.py
+++ b/tests/parser/engine/test_qwen3.py
@@ -1175,3 +1175,27 @@ class TestNestedSchemaCoercion:
assert questions[0]["question"] == "Pick a color"
assert questions[0]["multiSelect"] is False
assert questions[0]["answer"] is None
+
+
+@pytest.mark.parametrize("value", ["\n北京 &\n", "null", "42", "", "a\nb"])
+@pytest.mark.parametrize("chunk_size", [1, 7, 1000])
+def test_mimo_preserves_verbatim_parameter_values(
+ mock_tokenizer, mock_request, value, chunk_size
+):
+ from vllm.parser.mimo import MiMoParser
+
+ text = (
+ f"{value}"
+ ""
+ )
+ parser = MiMoParser(mock_tokenizer, chat_template_kwargs={"enable_thinking": False})
+ result = parser.extract_tool_calls(text, mock_request)
+ assert json.loads(result.tool_calls[0].function.arguments) == {"text": value}
+ parser = MiMoParser(mock_tokenizer, chat_template_kwargs={"enable_thinking": False})
+ results = simulate_tool_streaming(
+ parser,
+ mock_request,
+ [text[i : i + chunk_size] for i in range(0, len(text), chunk_size)],
+ )
+ assert collect_function_name(results) == "run"
+ assert json.loads(collect_tool_arguments(results)) == {"text": value}
diff --git a/tests/parser/engine/trace_builder.py b/tests/parser/engine/trace_builder.py
index 29b9db3f2b..321ce6b9e0 100644
--- a/tests/parser/engine/trace_builder.py
+++ b/tests/parser/engine/trace_builder.py
@@ -15,6 +15,7 @@ from __future__ import annotations
import functools
import json
+from collections.abc import Callable
from dataclasses import dataclass, replace
from typing import Any
@@ -39,6 +40,7 @@ from vllm.parser.engine.registered_adapters import (
GraniteThinkingParser,
InklingParser,
KimiK2Parser,
+ MiMoParser,
MinimaxM2Parser,
NemotronV3Parser,
Plamo3Parser,
@@ -356,7 +358,12 @@ def _qwen3_tool_segments(tc: ToolCallSpec) -> list[tuple[str, bool]]:
]
-def _qwen3_segments(scenario: Scenario) -> list[tuple[str, bool]]:
+def _qwen3_segments(
+ scenario: Scenario,
+ tool_segments: Callable[[ToolCallSpec], list[tuple[str, bool]]] = (
+ _qwen3_tool_segments
+ ),
+) -> list[tuple[str, bool]]:
segs: list[tuple[str, bool]] = []
if scenario.reasoning is not None:
segs.append((scenario.reasoning, False))
@@ -369,7 +376,7 @@ def _qwen3_segments(scenario: Scenario) -> list[tuple[str, bool]]:
segs.append((scenario.content, False))
if scenario.tool_calls:
for tc in scenario.tool_calls:
- segs.extend(_qwen3_tool_segments(tc))
+ segs.extend(tool_segments(tc))
return segs
@@ -389,6 +396,9 @@ def _build_qwen3(
parser_cls: type = Qwen3Parser,
strip_trailing_ws: bool = False,
validate: bool = True,
+ tool_segments: Callable[[ToolCallSpec], list[tuple[str, bool]]] = (
+ _qwen3_tool_segments
+ ),
) -> Sample:
expected_reasoning: str | None
if scenario.reasoning is not None:
@@ -403,7 +413,7 @@ def _build_qwen3(
sample_id=f"{name}-{scenario.id}",
description=scenario.description,
vocab=_QWEN3_VOCAB,
- segments=_qwen3_segments(scenario),
+ segments=_qwen3_segments(scenario, tool_segments),
expected_reasoning=expected_reasoning,
expected_content=_qwen3_expected_content(scenario),
expected_tool_calls=_expected_tc(scenario),
@@ -414,6 +424,31 @@ def _build_qwen3(
return sample
+# ── MiMo (compact Qwen3 XML, no newlines between tags) ───────────────
+
+
+def _mimo_tool_segments(tc: ToolCallSpec) -> list[tuple[str, bool]]:
+ parts = [f""]
+ for key, value in tc.arguments.items():
+ parts.append(f"{_qwen3_arg_value(value)}")
+ parts.append("")
+ return [
+ ("", True),
+ ("".join(parts), False),
+ ("", True),
+ ]
+
+
+def _build_mimo(scenario: Scenario, validate: bool = True) -> Sample:
+ return _build_qwen3(
+ scenario,
+ name="mimo",
+ parser_cls=MiMoParser,
+ validate=validate,
+ tool_segments=_mimo_tool_segments,
+ )
+
+
# ── MiniMax M2 (XML invoke format, starts in REASONING) ──────────────
_MINIMAX_M2_VOCAB: dict[str, int] = {
@@ -1232,6 +1267,7 @@ _BUILDERS: dict[str, Any] = {
"glm47_moe": _build_glm47_moe,
"kimi_k2": _build_kimi_k2,
"qwen3": _build_qwen3,
+ "mimo": _build_mimo,
"inkling": _build_inkling,
"plamo3": _build_plamo3,
}
diff --git a/tests/tool_parsers/test_structural_tag_registry.py b/tests/tool_parsers/test_structural_tag_registry.py
index 99b18c6736..01bc53968e 100644
--- a/tests/tool_parsers/test_structural_tag_registry.py
+++ b/tests/tool_parsers/test_structural_tag_registry.py
@@ -40,6 +40,7 @@ from vllm.tool_parsers.hermes_tool_parser import Hermes2ProToolParser
from vllm.tool_parsers.kimi_k2_tool_parser import KimiK2ToolParser
from vllm.tool_parsers.kimi_k3_tool_parser import KimiK3ToolParser
from vllm.tool_parsers.llama_tool_parser import Llama3JsonToolParser
+from vllm.tool_parsers.mimo_tool_parser import MiMoToolParser
from vllm.tool_parsers.minimax_m2_tool_parser import MinimaxM2ToolParser
from vllm.tool_parsers.plamo3_engine_tool_parser import Plamo3EngineToolParser
from vllm.tool_parsers.qwen3_engine_tool_parser import Qwen3EngineToolParser
@@ -665,6 +666,7 @@ def test_get_model_structural_tag_supports_named_tool_choice(
(Llama3JsonToolParser, "llama"),
(MinimaxM2ToolParser, "minimax"),
(Qwen3EngineToolParser, "qwen_3_coder"),
+ (MiMoToolParser, "mimo"),
],
)
def test_tool_parsers_declare_matching_xgrammar_builtin_model(parser_cls, model):
@@ -727,7 +729,7 @@ def test_get_structural_tag_disables_reasoning(
@pytest.mark.parametrize(
- "parser_cls", [Qwen3EngineToolParser, DeepSeekV41EngineToolParser]
+ "parser_cls", [Qwen3EngineToolParser, DeepSeekV41EngineToolParser, MiMoToolParser]
)
def test_unified_parser_get_structural_tag_disables_reasoning(
parser_cls,
@@ -1167,3 +1169,78 @@ def test_tool_strict_level_from_name():
ToolStrictLevel.from_name("strict")
with pytest.raises(ValueError, match="expected one of auto, function, parameter"):
ToolStrictLevel.from_name("off")
+
+
+@pytest.mark.parametrize("policy", ["auto", "required", "named"])
+def test_mimo_strict_compact_xml(policy):
+ tools = [
+ ChatCompletionToolsParam(
+ type="function",
+ function={
+ "name": "run",
+ "strict": True,
+ "parameters": {
+ "type": "object",
+ "properties": {"n": {"type": "integer", "minimum": 1}},
+ "required": ["n"],
+ "additionalProperties": False,
+ },
+ },
+ )
+ ]
+ choice = (
+ ChatCompletionNamedToolChoiceParam(
+ function=ChatCompletionNamedFunction(name="run")
+ )
+ if policy == "named"
+ else policy
+ )
+ tag = get_model_structural_tag("mimo", tools, choice, reasoning=False)
+ grammar = Grammar.from_structural_tag(tag)
+ call = "2"
+ assert _is_grammar_accept_string(grammar, call)
+ assert _is_grammar_accept_string(grammar, call * 2) == (policy != "named")
+ assert _is_grammar_accept_string(grammar, "answer") == (policy == "auto")
+ for invalid in [
+ call.replace("function=run", "function=unknown"),
+ call.replace("2", ""),
+ call.replace(">2", ">bad"),
+ call.replace(">2", ">0"),
+ call.replace("", ""),
+ call.replace("", "3"),
+ "plan" + call,
+ ]:
+ assert not _is_grammar_accept_string(grammar, invalid)
+ tools[0].function.strict = False
+ relaxed = Grammar.from_structural_tag(
+ get_model_structural_tag("mimo", tools, "required", reasoning=False)
+ )
+ assert _is_grammar_accept_string(
+ relaxed, call.replace("", "")
+ )
+
+
+@pytest.mark.parametrize("policy", ["auto", "required"])
+def test_mimo_request_limits_parallel_calls(sample_tools_strict, policy):
+ from vllm.parser.parser_manager import ParserManager
+
+ request = ChatCompletionRequest(
+ model="mimo",
+ messages=[],
+ tools=[t.model_dump() for t in sample_tools_strict],
+ tool_choice=policy,
+ parallel_tool_calls=False,
+ )
+ parser_cls = ParserManager.get_parser(
+ tool_parser_name="mimo", enable_auto_tools=True
+ )
+ parser = parser_cls(MagicMock(), tools=sample_tools_strict)
+ adjusted = parser.adjust_request(request)
+ grammar = Grammar.from_structural_tag(adjusted.structured_outputs.structural_tag)
+ call = (
+ "北京"
+ ""
+ )
+ assert _is_grammar_accept_string(grammar, call)
+ assert not _is_grammar_accept_string(grammar, call * 2)
+ assert not _is_grammar_accept_string(grammar, call + "extra text")
diff --git a/vllm/parser/engine/registered_adapters.py b/vllm/parser/engine/registered_adapters.py
index 7ce123c787..3c445c44c9 100644
--- a/vllm/parser/engine/registered_adapters.py
+++ b/vllm/parser/engine/registered_adapters.py
@@ -17,6 +17,7 @@ from vllm.parser.granite import GraniteParser
from vllm.parser.granite_thinking import GraniteThinkingParser
from vllm.parser.inkling import InklingParser
from vllm.parser.kimi_k2 import KimiK2Parser
+from vllm.parser.mimo import MiMoParser
from vllm.parser.minimax_m2 import MinimaxM2Parser
from vllm.parser.mistral import MistralParser
from vllm.parser.nemotron_v3 import NemotronV3Parser
@@ -69,6 +70,11 @@ from vllm.parser.seed_oss import SeedOssParser
Qwen3ParserToolAdapter,
) = make_adapters(Qwen3Parser)
+(
+ MiMoParserReasoningAdapter,
+ MiMoParserToolAdapter,
+) = make_adapters(MiMoParser)
+
(
SeedOssParserReasoningAdapter,
SeedOssParserToolAdapter,
diff --git a/vllm/parser/mimo.py b/vllm/parser/mimo.py
new file mode 100644
index 0000000000..b1c37dd746
--- /dev/null
+++ b/vllm/parser/mimo.py
@@ -0,0 +1,42 @@
+# SPDX-License-Identifier: Apache-2.0
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
+"""MiMo's compact XML parameters contain verbatim string values."""
+
+import json
+from dataclasses import replace
+
+from vllm.parser.qwen3 import (
+ _PARAM_RE,
+ _PARTIAL_PARAM_RE,
+ CHATML_TURN_BOUNDARIES,
+ Qwen3Parser,
+ qwen3_config,
+)
+
+
+def _mimo_arg_converter(raw_args: str, partial: bool) -> str:
+ params = {m.group(1): m.group(2) for m in _PARAM_RE.finditer(raw_args)}
+ if partial:
+ match = _PARTIAL_PARAM_RE.search(_PARAM_RE.sub("", raw_args))
+ if match and match.group(1):
+ params[match.group(1)] = match.group(2)
+ return json.dumps(params, ensure_ascii=False)
+
+
+class MiMoParser(Qwen3Parser):
+ def __init__(self, tokenizer, tools=None, **kwargs):
+ thinking = (kwargs.get("chat_template_kwargs") or {}).get(
+ "enable_thinking", True
+ )
+ kwargs.setdefault(
+ "parser_engine_config",
+ replace(
+ qwen3_config(
+ thinking=thinking,
+ name="mimo",
+ turn_boundary_tokens=CHATML_TURN_BOUNDARIES,
+ ),
+ arg_converter=_mimo_arg_converter,
+ ),
+ )
+ super().__init__(tokenizer, tools, **kwargs)
diff --git a/vllm/reasoning/__init__.py b/vllm/reasoning/__init__.py
index f1ab60b6b7..736a4fcdeb 100644
--- a/vllm/reasoning/__init__.py
+++ b/vllm/reasoning/__init__.py
@@ -109,8 +109,8 @@ _REASONING_PARSERS_TO_REGISTER = {
"K2HorizonReasoningParser",
),
"mimo": (
- "qwen3_engine_reasoning_parser",
- "Qwen3ParserReasoningAdapter",
+ "mimo_engine_reasoning_parser",
+ "MiMoParserReasoningAdapter",
),
"minimax_m2": (
"minimax_m2_reasoning_parser",
diff --git a/vllm/reasoning/mimo_engine_reasoning_parser.py b/vllm/reasoning/mimo_engine_reasoning_parser.py
new file mode 100644
index 0000000000..d07cde9278
--- /dev/null
+++ b/vllm/reasoning/mimo_engine_reasoning_parser.py
@@ -0,0 +1,6 @@
+# SPDX-License-Identifier: Apache-2.0
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
+
+from vllm.parser.engine.registered_adapters import MiMoParserReasoningAdapter
+
+__all__ = ["MiMoParserReasoningAdapter"]
diff --git a/vllm/tool_parsers/__init__.py b/vllm/tool_parsers/__init__.py
index 90a724858a..5647a75291 100644
--- a/vllm/tool_parsers/__init__.py
+++ b/vllm/tool_parsers/__init__.py
@@ -143,8 +143,8 @@ _TOOL_PARSERS_TO_REGISTER = {
"LongcatFlashToolParser",
),
"mimo": (
- "qwen3_engine_tool_parser",
- "Qwen3EngineToolParser",
+ "mimo_tool_parser",
+ "MiMoToolParser",
),
"minimax_m2": (
"minimax_m2_tool_parser",
diff --git a/vllm/tool_parsers/mimo_tool_parser.py b/vllm/tool_parsers/mimo_tool_parser.py
new file mode 100644
index 0000000000..cca77eb56b
--- /dev/null
+++ b/vllm/tool_parsers/mimo_tool_parser.py
@@ -0,0 +1,25 @@
+# SPDX-License-Identifier: Apache-2.0
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
+
+from xgrammar.structural_tag import TriggeredTagsFormat
+
+from vllm.parser.engine.registered_adapters import MiMoParserToolAdapter
+from vllm.tool_parsers.tool_strict_level import ToolStrictLevel
+
+
+class MiMoToolParser(MiMoParserToolAdapter): # type: ignore[valid-type, misc]
+ structural_tag_model = "mimo"
+
+ def get_structural_tag(
+ self, request, *, reasoning=False, strict_level=ToolStrictLevel.AUTO
+ ):
+ tag = super().get_structural_tag(
+ request, reasoning=reasoning, strict_level=strict_level
+ )
+ if (
+ tag is not None
+ and request.parallel_tool_calls is False
+ and isinstance(tag.format, TriggeredTagsFormat)
+ ):
+ tag.format.stop_after_first = True
+ return tag
diff --git a/vllm/tool_parsers/structural_tag_registry.py b/vllm/tool_parsers/structural_tag_registry.py
index fdb4e4f92a..bc8fab8fe6 100644
--- a/vllm/tool_parsers/structural_tag_registry.py
+++ b/vllm/tool_parsers/structural_tag_registry.py
@@ -66,6 +66,7 @@ XGRAMMAR_BUILTIN_STRUCTURAL_TAG_MODELS = frozenset(
"deepseek_v3_1",
"qwen_3_5",
"qwen_3_coder",
+ "mimo",
"qwen_3",
"deepseek_v3_2",
"glm_4_7",