# SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project import dataclasses import json import pytest from tests.parser.engine.conftest import make_mock_tokenizer from tests.parser.engine.streaming_helpers import ( collect_function_name, collect_tool_arguments, simulate_tool_streaming, ) from vllm.entrypoints.openai.chat_completion.protocol import ( ChatCompletionToolsParam, FunctionDefinition, ) from vllm.parser.engine.parser_engine import ParserEngine from vllm.parser.minimax_m2 import ( THINK_END, TOOL_CALL_END, TOOL_CALL_START, MinimaxM2Parser, _minimax_m2_arg_value_extender, minimax_m2_config, ) @pytest.fixture def mock_tokenizer(): return make_mock_tokenizer( { THINK_END: 99, TOOL_CALL_START: 100, TOOL_CALL_END: 101, } ) @pytest.fixture def parser(mock_tokenizer): return MinimaxM2Parser(mock_tokenizer) def make_tools(*names: str): return [ ChatCompletionToolsParam( function=FunctionDefinition( name=name, parameters={ "type": "object", "properties": {}, }, ), ) for name in names ] class TestNonStreaming: def test_no_tool_calls(self, parser, mock_request): result = parser.extract_tool_calls( "This is a regular response without tool calls.", mock_request, ) assert result.tools_called is False assert result.tool_calls == [] assert result.content == "This is a regular response without tool calls." def test_single_tool_call(self, parser, mock_request): result = parser.extract_tool_calls( '' 'Seattle' "", mock_request, ) assert result.tools_called is True assert len(result.tool_calls) == 1 assert result.tool_calls[0].function.name == "get_weather" assert json.loads(result.tool_calls[0].function.arguments) == { "city": "Seattle", } def test_multiple_invokes(self, parser, mock_request): result = parser.extract_tool_calls( "" 'OpenAI' 'vLLM' "", mock_request, ) assert result.tools_called is True assert [tc.function.name for tc in result.tool_calls] == ["search", "search"] assert json.loads(result.tool_calls[0].function.arguments) == {"q": "OpenAI"} assert json.loads(result.tool_calls[1].function.arguments) == {"q": "vLLM"} def test_schema_type_coercion(self, mock_tokenizer, mock_request): tools = [ ChatCompletionToolsParam( function=FunctionDefinition( name="forecast", parameters={ "type": "object", "properties": { "days": {"type": "integer"}, "include_hourly": {"type": "boolean"}, }, }, ), ) ] parser = MinimaxM2Parser(mock_tokenizer, tools=tools) mock_request.tools = tools result = parser.extract_tool_calls( '' '5' 'true' "", mock_request, ) assert json.loads(result.tool_calls[0].function.arguments) == { "days": 5, "include_hourly": True, } def test_invalid_tool_name_is_rejected(self, mock_tokenizer, mock_request): tools = make_tools("search") parser = MinimaxM2Parser(mock_tokenizer) mock_request.tools = tools result = parser.extract_tool_calls( '' 'a cat' "", mock_request, ) assert result.tools_called is False assert result.tool_calls == [] def test_mixed_tool_names_only_return_valid(self, mock_tokenizer, mock_request): tools = make_tools("search") parser = MinimaxM2Parser(mock_tokenizer) mock_request.tools = tools result = parser.extract_tool_calls( "" 'cat' 'news' "", mock_request, ) assert result.tools_called is True assert [tc.function.name for tc in result.tool_calls] == ["search"] assert json.loads(result.tool_calls[0].function.arguments) == { "query": "news", } class TestStreaming: def test_streaming_single_tool_call(self, parser, mock_request): results = simulate_tool_streaming( parser, mock_request, [ "", '', 'Seattle', "", ], ) assert collect_function_name(results) == "get_weather" assert json.loads(collect_tool_arguments(results)) == { "city": "Seattle", } def test_streaming_multiple_invokes(self, parser, mock_request): results = simulate_tool_streaming( parser, mock_request, [ "", '1', '2', "", ], ) tool_names = [ tc.function.name for delta, _ in results if delta and delta.tool_calls for tc in delta.tool_calls if tc.function and tc.function.name ] assert tool_names == ["a", "b"] def test_streaming_invoke_prefix_split_before_quote(self, parser, mock_request): results = simulate_tool_streaming( parser, mock_request, [ "", "', 'Seattle', "", ], ) assert collect_function_name(results) == "get_weather" assert json.loads(collect_tool_arguments(results)) == { "city": "Seattle", } def test_streaming_invalid_tool_name_is_rejected( self, mock_tokenizer, mock_request ): tools = make_tools("search") parser = MinimaxM2Parser(mock_tokenizer) mock_request.tools = tools results = simulate_tool_streaming( parser, mock_request, [ "", '', 'cat', "", ], ) assert collect_function_name(results) is None assert collect_tool_arguments(results) == "" WRITE_CALL_HEAD = ( '\n\n' '/repo/app.py\n' '644\n' '' ) WRITE_CALL_TAIL = "\n\n" # Escapes, non-ASCII, blank lines and tag-like text that must stay inside # the value, including prefixes of the closing and next-parameter tags. CONTENT_LINE = 'if a < b > c: print("x\\y") #

int: calls = 0 def counting_converter(raw_args: str, partial: bool) -> str: nonlocal calls calls += partial return converter(raw_args, partial) parser = ParserEngine( mock_tokenizer, parser_engine_config=dataclasses.replace( base, arg_converter=counting_converter ), ) text = WRITE_CALL_HEAD + CONTENT_LINE * lines + WRITE_CALL_TAIL simulate_tool_streaming( parser, mock_request, [text[i : i + 4] for i in range(0, len(text), 4)], ) return calls assert streaming_conversions(lines=20) == streaming_conversions(lines=200) def test_extender_stops_at_split_end_tag(self): extend = _minimax_m2_arg_value_extender('') assert extend is not None deltas = ['a"\n', ""] assert [extend(d) for d in deltas] == ['a\\"\\n', "x") assert extend is not None assert [extend(d) for d in ["< parameter", " name ="]] == ["< parameter", None] @pytest.mark.parametrize( "raw_args", [ 'x', '12' '3' ), 'x', ], ids=[ "closed_value", "open_name", "repeated_key_not_last", "earlier_unmatched_opener", "bare_name_opens_quote", ], ) def test_no_extender_unless_open_value_is_stable(self, raw_args): assert _minimax_m2_arg_value_extender(raw_args) is None class TestReasoning: def test_extract_reasoning_without_start_token(self, parser, mock_request): reasoning, content = parser.extract_reasoning( "This is reasoningThis is content", mock_request, ) assert reasoning == "This is reasoning" assert content == "This is content" def test_extract_reasoning_without_end_token(self, parser, mock_request): reasoning, content = parser.extract_reasoning( "This is still reasoning", mock_request, ) assert reasoning == "This is still reasoning" assert content is None def test_extract_content_ids_without_end_token(self, parser): assert parser.extract_content_ids([1, 2, 3]) == [] def test_extract_content_ids_after_end_token(self, parser): assert parser.extract_content_ids([1, 99, 2, 3]) == [2, 3]