Files
vllm-project--vllm/vllm/parser/minimax_m2.py
T
wehub-resource-sync 7ce4c8e27e
pre-commit / pre-run-check (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:55:37 +08:00

212 lines
6.6 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""MiniMax M2 parser for XML-style tool calls.
MiniMax M2 tool call format::
<minimax:tool_call><invoke name="get_weather">
<parameter name="city">Seattle</parameter>
</invoke></minimax:tool_call>
Each ``<invoke>`` block becomes one tool call. The argument body consists
of ``<parameter name="...">...</parameter>`` tags.
"""
from __future__ import annotations
import functools
import json
import regex as re
from vllm.parser.engine.events import EventType
from vllm.parser.engine.parser_engine import ParserEngine
from vllm.parser.engine.parser_engine_config import (
ParserEngineConfig,
ParserState,
Transition,
)
TOOL_CALL_START = "<minimax:tool_call>"
TOOL_CALL_END = "</minimax:tool_call>"
THINK_START = "<think>"
THINK_END = "</think>"
INVOKE_PREFIX_DQ = '<invoke name="'
INVOKE_PREFIX_SQ = "<invoke name='"
INVOKE_PREFIX_UNQUOTED = "<invoke name="
INVOKE_END = "</invoke>"
NAME_END_DQ = '">'
NAME_END_SQ = "'>"
NAME_END_UNQUOTED = ">"
PARAM_START = "<parameter name="
PARAM_END = "</parameter>"
_PARAM_RE = re.compile(
r"<\s*parameter\s+name\s*=\s*"
r"(?:\"(?P<dq_name>[^\"]*)\"|'(?P<sq_name>[^']*)'|(?P<bare_name>[^>\s]+))"
r"\s*>"
r"(?P<value>.*?)"
r"(?:<\s*/\s*parameter\s*>|(?=<\s*parameter\s+name\s*=))",
re.DOTALL,
)
_PARTIAL_PARAM_RE = re.compile(
r"<\s*parameter\s+name\s*=\s*"
r"(?:\"(?P<dq_name>[^\"]*)\"|'(?P<sq_name>[^']*)'|(?P<bare_name>[^>\s]+))"
r"\s*>"
r"(?P<value>.*)$",
re.DOTALL,
)
def _minimax_m2_arg_converter(raw_args: str, partial: bool) -> str:
params: dict[str, object] = {}
for match in _PARAM_RE.finditer(raw_args):
name = (
match.group("dq_name")
or match.group("sq_name")
or match.group("bare_name")
or ""
).strip()
if not name:
continue
params[name] = match.group("value").strip()
if partial:
remaining = _PARAM_RE.sub("", raw_args)
match = _PARTIAL_PARAM_RE.search(remaining)
if match:
name = (
match.group("dq_name")
or match.group("sq_name")
or match.group("bare_name")
or ""
).strip()
if name:
params[name] = match.group("value").strip()
return json.dumps(params, ensure_ascii=False)
@functools.cache
def minimax_m2_config() -> ParserEngineConfig:
return ParserEngineConfig(
name="minimax_m2",
initial_state=ParserState.REASONING,
terminals={
"THINK_START": THINK_START,
"THINK_END": THINK_END,
"TOOL_START": TOOL_CALL_START,
"PARAM_START": PARAM_START,
"PARAM_END": PARAM_END,
"TOOL_END": TOOL_CALL_END,
"INVOKE_PREFIX_DQ": INVOKE_PREFIX_DQ,
"INVOKE_PREFIX_SQ": INVOKE_PREFIX_SQ,
"INVOKE_PREFIX_UNQUOTED": INVOKE_PREFIX_UNQUOTED,
"INVOKE_END": INVOKE_END,
"NAME_END_DQ": NAME_END_DQ,
"NAME_END_SQ": NAME_END_SQ,
"NAME_END_UNQUOTED": NAME_END_UNQUOTED,
},
token_id_terminals={
"THINK_START": THINK_START,
"THINK_END": THINK_END,
"TOOL_START": TOOL_CALL_START,
"TOOL_END": TOOL_CALL_END,
},
transitions={
(ParserState.REASONING, "THINK_START"): Transition(
ParserState.REASONING,
(),
),
(ParserState.REASONING, "THINK_END"): Transition(
ParserState.CONTENT,
(EventType.REASONING_END,),
),
(ParserState.CONTENT, "THINK_END"): Transition(
ParserState.CONTENT,
(),
),
(ParserState.REASONING, "TOOL_START"): Transition(
ParserState.TOOL_PREAMBLE,
(EventType.REASONING_END,),
),
(ParserState.CONTENT, "TOOL_START"): Transition(
ParserState.TOOL_PREAMBLE,
(),
),
(ParserState.TOOL_ARGS, "PARAM_START"): Transition(
ParserState.TOOL_ARGS,
(EventType.ARG_VALUE_CHUNK,),
),
(ParserState.TOOL_ARGS, "PARAM_END"): Transition(
ParserState.TOOL_ARGS,
(EventType.ARG_VALUE_CHUNK,),
),
(ParserState.TOOL_PREAMBLE, "TOOL_END"): Transition(
ParserState.CONTENT,
(),
),
(ParserState.TOOL_BETWEEN, "TOOL_END"): Transition(
ParserState.CONTENT,
(),
),
(ParserState.CONTENT, "TOOL_END"): Transition(
ParserState.CONTENT,
(),
),
(ParserState.TOOL_ARGS, "INVOKE_END"): Transition(
ParserState.TOOL_BETWEEN,
(EventType.TOOL_CALL_END,),
),
**{
(state, terminal): Transition(
ParserState.TOOL_NAME,
(EventType.TOOL_CALL_START,),
)
for state in (
ParserState.CONTENT,
ParserState.TOOL_PREAMBLE,
ParserState.TOOL_BETWEEN,
)
for terminal in (
"INVOKE_PREFIX_DQ",
"INVOKE_PREFIX_SQ",
"INVOKE_PREFIX_UNQUOTED",
)
},
**{
(ParserState.TOOL_NAME, terminal): Transition(
ParserState.TOOL_ARGS,
(),
)
for terminal in (
"NAME_END_DQ",
"NAME_END_SQ",
"NAME_END_UNQUOTED",
)
},
},
arg_converter=_minimax_m2_arg_converter,
stream_arg_deltas=True,
tool_args_json=False,
validate_tool_names=True,
)
class MinimaxM2Parser(ParserEngine):
"""MiniMax M2 parser backed by the declarative parser engine."""
def __init__(self, tokenizer, tools=None, **kwargs) -> None:
kwargs.setdefault("parser_engine_config", minimax_m2_config())
super().__init__(tokenizer, tools, **kwargs)
self._think_end_token_id = self.vocab.get(THINK_END)
def extract_content_ids(self, input_ids: list[int]) -> list[int]:
end_id = self._think_end_token_id
if end_id is None:
return []
for i in range(len(input_ids) - 1, -1, -1):
if input_ids[i] == end_id:
return input_ids[i + 1 :]
return []