Files
vllm-project--vllm/tests/reasoning/test_minimax_m3_reasoning_parser.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

462 lines
15 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import string
from collections.abc import Sequence
import pytest
from vllm.entrypoints.openai.chat_completion.protocol import ChatCompletionRequest
from vllm.reasoning import ReasoningParserManager
from vllm.reasoning.minimax_m3_reasoning_parser import MiniMaxM3ReasoningParser
pytestmark = pytest.mark.skip_global_cleanup
class MiniMaxM3Tokenizer:
"""Small tokenizer with MiniMax M3 reasoning tags as special tokens."""
special_tokens = ("<mm:think>", "</mm:think>")
def __init__(self):
self._token_to_id: dict[str, int] = {}
self._id_to_token: dict[int, str] = {}
for token in self.special_tokens:
self._add_token(token)
for char in string.printable:
self._add_token(char)
def _add_token(self, token: str) -> int:
token_id = self._token_to_id.get(token)
if token_id is None:
token_id = len(self._token_to_id) + 1
self._token_to_id[token] = token_id
self._id_to_token[token_id] = token
return token_id
def get_vocab(self) -> dict[str, int]:
return dict(self._token_to_id)
def encode(
self,
text: str,
truncation: bool | None = None,
max_length: int | None = None,
add_special_tokens: bool = True,
) -> list[int]:
return [self._add_token(token) for token in self.tokenize(text)]
def decode(
self, ids: Sequence[int] | int, skip_special_tokens: bool = False
) -> str:
if isinstance(ids, int):
ids = [ids]
return "".join(self._id_to_token[token_id] for token_id in ids)
def tokenize(self, text: str) -> list[str]:
tokens: list[str] = []
pos = 0
while pos < len(text):
for special_token in self.special_tokens:
if text.startswith(special_token, pos):
tokens.append(special_token)
pos += len(special_token)
break
else:
tokens.append(text[pos])
pos += 1
return tokens
def convert_ids_to_tokens(
self,
ids: Sequence[int],
skip_special_tokens: bool = False,
) -> list[str]:
return [self._id_to_token[token_id] for token_id in ids]
def convert_tokens_to_ids(self, tokens: str | list[str]) -> int | list[int]:
if isinstance(tokens, str):
return self._add_token(tokens)
return [self._add_token(token) for token in tokens]
def convert_tokens_to_string(self, tokens: list[str]) -> str:
return "".join(tokens)
class SplitMiniMaxM3Tokenizer(MiniMaxM3Tokenizer):
"""Tokenizer that exposes marker vocab entries but encodes them as text."""
def tokenize(self, text: str) -> list[str]:
return list(text)
class RuntimeSplitMiniMaxM3Tokenizer(MiniMaxM3Tokenizer):
"""Tokenizer whose runtime output splits markers despite atomic encodes."""
def encode_runtime(self, text: str) -> list[int]:
return [self._add_token(token) for token in list(text)]
def make_parser(
chat_template_kwargs: dict[str, str] | None = None,
) -> tuple[MiniMaxM3ReasoningParser, MiniMaxM3Tokenizer]:
tokenizer = MiniMaxM3Tokenizer()
return (
MiniMaxM3ReasoningParser(tokenizer, chat_template_kwargs=chat_template_kwargs),
tokenizer,
)
def run_streaming(
parser: MiniMaxM3ReasoningParser,
tokenizer: MiniMaxM3Tokenizer,
chunks: list[str],
) -> tuple[str | None, str | None, list[bool]]:
previous_text = ""
previous_token_ids: list[int] = []
reasoning_parts: list[str] = []
content_parts: list[str] = []
reasoning_end_states: list[bool] = []
for chunk in chunks:
encode_runtime = getattr(tokenizer, "encode_runtime", tokenizer.encode)
delta_token_ids = encode_runtime(chunk)
current_text = previous_text + chunk
current_token_ids = previous_token_ids + delta_token_ids
delta = parser.extract_reasoning_streaming(
previous_text=previous_text,
current_text=current_text,
delta_text=chunk,
previous_token_ids=previous_token_ids,
current_token_ids=current_token_ids,
delta_token_ids=delta_token_ids,
)
reasoning_end_states.append(
parser.is_reasoning_end_streaming(current_token_ids, delta_token_ids)
)
if delta is not None:
if delta.reasoning is not None:
reasoning_parts.append(delta.reasoning)
if delta.content is not None:
content_parts.append(delta.content)
previous_text = current_text
previous_token_ids = current_token_ids
return (
"".join(reasoning_parts) or None,
"".join(content_parts) or None,
reasoning_end_states,
)
def test_parser_registration():
parser_cls = ReasoningParserManager.get_reasoning_parser("minimax_m3")
assert parser_cls is MiniMaxM3ReasoningParser
def test_nonstreaming_extracts_explicit_reasoning_block():
parser, _ = make_parser()
request = ChatCompletionRequest(messages=[], model="test-model")
reasoning, content = parser.extract_reasoning(
"<mm:think>plan</mm:think>answer", request
)
assert reasoning == "plan"
assert content == "answer"
def test_nonstreaming_without_start_tag_is_content():
parser, _ = make_parser()
request = ChatCompletionRequest(messages=[], model="test-model")
reasoning, content = parser.extract_reasoning("plain answer", request)
assert reasoning is None
assert content == "plain answer"
def test_nonstreaming_drops_leading_end_tag():
parser, _ = make_parser()
request = ChatCompletionRequest(messages=[], model="test-model")
reasoning, content = parser.extract_reasoning("</mm:think>answer", request)
assert reasoning is None
assert content == "answer"
def test_nonstreaming_non_leading_end_tag_is_content():
parser, _ = make_parser()
request = ChatCompletionRequest(messages=[], model="test-model")
reasoning, content = parser.extract_reasoning("XXX</mm:think>YYY", request)
assert reasoning is None
assert content == "XXX</mm:think>YYY"
def test_nonstreaming_enabled_mode_starts_in_reasoning():
parser, _ = make_parser(chat_template_kwargs={"thinking_mode": "enabled"})
request = ChatCompletionRequest(messages=[], model="test-model")
reasoning, content = parser.extract_reasoning("plan</mm:think>answer", request)
assert reasoning == "plan"
assert content == "answer"
def test_nonstreaming_open_reasoning_block():
parser, _ = make_parser()
request = ChatCompletionRequest(messages=[], model="test-model")
reasoning, content = parser.extract_reasoning("<mm:think>still thinking", request)
assert reasoning == "still thinking"
assert content is None
def test_streaming_reasoning_tags_are_not_returned():
parser, tokenizer = make_parser()
reasoning, content, end_states = run_streaming(
parser,
tokenizer,
["<mm:think>", "plan", "</mm:think>", "answer"],
)
assert reasoning == "plan"
assert content == "answer"
assert end_states == [False, False, True, True]
def test_streaming_boundary_can_emit_reasoning_and_content():
parser, tokenizer = make_parser()
reasoning, content, end_states = run_streaming(
parser,
tokenizer,
["<mm:think>plan</mm:think>answer"],
)
assert reasoning == "plan"
assert content == "answer"
assert end_states == [True]
def test_streaming_drops_leading_end_tag():
parser, tokenizer = make_parser()
reasoning, content, end_states = run_streaming(
parser,
tokenizer,
["</mm:think>", "answer"],
)
assert reasoning is None
assert content == "answer"
assert end_states == [True, True]
def test_streaming_non_leading_end_tag_is_content():
parser, tokenizer = make_parser()
reasoning, content, end_states = run_streaming(
parser,
tokenizer,
["XXX</mm:think>YYY"],
)
assert reasoning is None
assert content == "XXX</mm:think>YYY"
assert end_states == [True]
def test_streaming_enabled_mode_starts_in_reasoning():
parser, tokenizer = make_parser(chat_template_kwargs={"thinking_mode": "enabled"})
reasoning, content, end_states = run_streaming(
parser,
tokenizer,
["plan", "</mm:think>", "answer"],
)
assert reasoning == "plan"
assert content == "answer"
assert end_states == [False, True, True]
def test_streaming_plain_content_ends_reasoning_phase():
parser, tokenizer = make_parser()
reasoning, content, end_states = run_streaming(
parser,
tokenizer,
["plain ", "answer"],
)
assert reasoning is None
assert content == "plain answer"
assert end_states == [True, True]
def test_streaming_split_marker_tokens_are_not_returned():
tokenizer = RuntimeSplitMiniMaxM3Tokenizer()
parser = MiniMaxM3ReasoningParser(tokenizer)
reasoning, content, end_states = run_streaming(
parser,
tokenizer,
["<mm:think>", "Reasoning", " content", "</mm:think>", "content"],
)
assert reasoning == "Reasoning content"
assert content == "content"
assert end_states == [False, False, False, True, True]
def test_streaming_split_marker_text_drives_end_state():
tokenizer = RuntimeSplitMiniMaxM3Tokenizer()
parser = MiniMaxM3ReasoningParser(tokenizer)
previous_text = ""
previous_token_ids: list[int] = []
for chunk in ["<mm:think>", "Reasoning", " content", "</mm:think>"]:
delta_token_ids = tokenizer.encode_runtime(chunk)
current_text = previous_text + chunk
current_token_ids = previous_token_ids + delta_token_ids
parser.extract_reasoning_streaming(
previous_text=previous_text,
current_text=current_text,
delta_text=chunk,
previous_token_ids=previous_token_ids,
current_token_ids=current_token_ids,
delta_token_ids=delta_token_ids,
)
previous_text = current_text
previous_token_ids = current_token_ids
assert parser.is_reasoning_end_streaming(previous_token_ids, []) is True
def test_streaming_split_end_marker_content_ids_are_stripped():
tokenizer = RuntimeSplitMiniMaxM3Tokenizer()
parser = MiniMaxM3ReasoningParser(tokenizer)
previous_text = "<mm:think>Reasoning"
previous_token_ids = tokenizer.encode_runtime(previous_text)
delta_text = "</mm:think>content"
delta_token_ids = tokenizer.encode_runtime(delta_text)
current_token_ids = previous_token_ids + delta_token_ids
parser.extract_reasoning_streaming(
previous_text=previous_text,
current_text=previous_text + delta_text,
delta_text=delta_text,
previous_token_ids=previous_token_ids,
current_token_ids=current_token_ids,
delta_token_ids=delta_token_ids,
)
assert parser.is_reasoning_end_streaming(current_token_ids, delta_token_ids)
assert tokenizer.decode(parser.extract_content_ids(delta_token_ids)) == "content"
def test_streaming_split_marker_tokens_enabled_mode():
tokenizer = RuntimeSplitMiniMaxM3Tokenizer()
parser = MiniMaxM3ReasoningParser(
tokenizer, chat_template_kwargs={"thinking_mode": "enabled"}
)
reasoning, content, end_states = run_streaming(
parser,
tokenizer,
["Reasoning", " content", "</mm:think>", "content"],
)
assert reasoning == "Reasoning content"
assert content == "content"
assert end_states == [False, False, True, True]
def test_streaming_split_marker_text_across_deltas():
tokenizer = RuntimeSplitMiniMaxM3Tokenizer()
parser = MiniMaxM3ReasoningParser(tokenizer)
reasoning, content, end_states = run_streaming(
parser,
tokenizer,
["<mm:", "think>", "Reasoning", " content", "</mm:", "think>", "content"],
)
assert reasoning == "Reasoning content"
assert content == "content"
assert end_states == [False, False, False, False, False, True, True]
def test_streaming_split_leading_end_marker_text_across_deltas():
tokenizer = RuntimeSplitMiniMaxM3Tokenizer()
parser = MiniMaxM3ReasoningParser(tokenizer)
reasoning, content, end_states = run_streaming(
parser,
tokenizer,
["</mm:", "think>", "content"],
)
assert reasoning is None
assert content == "content"
assert end_states == [False, True, True]
def test_token_id_helpers_with_split_marker_tokens():
tokenizer = SplitMiniMaxM3Tokenizer()
parser = MiniMaxM3ReasoningParser(tokenizer)
output_ids = tokenizer.encode(
"<mm:think>abc</mm:think>def", add_special_tokens=False
)
open_reasoning_ids = tokenizer.encode("<mm:think>abc", add_special_tokens=False)
content_ids = tokenizer.encode("plain", add_special_tokens=False)
assert parser.is_reasoning_end(output_ids)
assert not parser.is_reasoning_end(open_reasoning_ids)
assert not parser.is_reasoning_end(content_ids)
assert tokenizer.decode(parser.extract_content_ids(output_ids)) == "def"
assert parser.extract_content_ids(open_reasoning_ids) == []
assert parser.extract_content_ids(content_ids) == content_ids
assert parser.count_reasoning_tokens(output_ids) == len(tokenizer.encode("abc"))
def test_token_id_helpers():
parser, tokenizer = make_parser()
output_ids = tokenizer.encode(
"<mm:think>abc</mm:think>def", add_special_tokens=False
)
open_reasoning_ids = tokenizer.encode("<mm:think>abc", add_special_tokens=False)
content_ids = tokenizer.encode("plain", add_special_tokens=False)
assert parser.is_reasoning_end(output_ids)
assert not parser.is_reasoning_end(open_reasoning_ids)
assert not parser.is_reasoning_end(content_ids)
assert tokenizer.decode(parser.extract_content_ids(output_ids)) == "def"
assert parser.extract_content_ids(open_reasoning_ids) == []
assert parser.extract_content_ids(content_ids) == content_ids
assert parser.count_reasoning_tokens(output_ids) == len(tokenizer.encode("abc"))
def test_token_id_helpers_enabled_mode():
parser, tokenizer = make_parser(chat_template_kwargs={"thinking_mode": "enabled"})
output_ids = tokenizer.encode("abc</mm:think>def", add_special_tokens=False)
open_reasoning_ids = tokenizer.encode("abc", add_special_tokens=False)
assert parser.is_reasoning_end(output_ids)
assert not parser.is_reasoning_end(open_reasoning_ids)
assert tokenizer.decode(parser.extract_content_ids(output_ids)) == "def"
assert parser.extract_content_ids(open_reasoning_ids) == []
assert parser.count_reasoning_tokens(output_ids) == len(tokenizer.encode("abc"))
assert parser.count_reasoning_tokens(open_reasoning_ids) == len(
tokenizer.encode("abc")
)