@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / d53cb9cb8e
Fork 0

A high-throughput and memory-efficient inference and serving engine for LLMs

Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git

Synced Nov 5, 18:30 UTC

[Tool Parser][2/3] Use self.tools instead of request.tools in tool parsers (#38189)

Signed-off-by: sfeng33 <4florafeng@gmail.com>

Flora Feng committed Mar 31, 2026 at 01:41 UTC d53cb9cb8e9eac9d38a3b7bf027df2a6ef15def4
16 files changed +113 -105
tests/tool_parsers/test_deepseekv32_tool_parser.py
+19 -13
@@ -11,6 +11,10 @@ from unittest.mock import MagicMock
11
12 import pytest
13
14 +from vllm.entrypoints.openai.chat_completion.protocol import (
15 + ChatCompletionToolsParam,
16 + FunctionDefinition,
17 +)
18 from vllm.tokenizers import get_tokenizer
19 from vllm.tool_parsers.deepseekv32_tool_parser import DeepSeekV32ToolParser
20
@@ -24,8 +28,8 @@ MOCK_TOKENIZER = MagicMock()
28 MOCK_TOKENIZER.get_vocab.return_value = {}
29
30
27 -def make_parser() -> DeepSeekV32ToolParser:
28 - return DeepSeekV32ToolParser(MOCK_TOKENIZER)
31 +def make_parser(tools=None) -> DeepSeekV32ToolParser:
32 + return DeepSeekV32ToolParser(MOCK_TOKENIZER, tools=tools)
33
34
35 def make_tool_param(name: str, params: dict) -> MagicMock:
@@ -275,20 +279,22 @@ class TestExtractToolCallsStreaming:
279 content = "".join(d.content for d in deltas if d.content is not None)
280 assert "Thinking" in content
281
278 - def test_type_conversion_in_streaming(self, parser):
279 - tool = make_tool_param(
280 - "add",
281 - {
282 - "type": "object",
283 - "properties": {
284 - "x": {"type": "integer"},
285 - "y": {"type": "integer"},
282 + def test_type_conversion_in_streaming(self):
283 + tool = ChatCompletionToolsParam(
284 + function=FunctionDefinition(
285 + name="add",
286 + parameters={
287 + "type": "object",
288 + "properties": {
289 + "x": {"type": "integer"},
290 + "y": {"type": "integer"},
291 + },
292 },
287 - },
293 + ),
294 )
289 - request = make_request(tools=[tool])
295 + parser = make_parser(tools=[tool])
296 full_text = build_tool_call("add", {"x": "3", "y": "4"})
291 - deltas = self._stream(parser, full_text, request=request)
297 + deltas = self._stream(parser, full_text)
298 args_str = self._reconstruct_args(deltas)
299 assert json.loads(args_str) == {"x": 3, "y": 4}
300
tests/tool_parsers/test_glm47_moe_tool_parser.py
+13 -8
@@ -25,14 +25,8 @@ def glm47_tokenizer():
25
26
27 @pytest.fixture
28 -def glm47_tool_parser(glm47_tokenizer):
29 - return Glm47MoeModelToolParser(glm47_tokenizer)
30 -
31 -
32 -@pytest.fixture
33 -def mock_request() -> ChatCompletionRequest:
34 - request = Mock(spec=ChatCompletionRequest)
35 - request.tools = [
28 +def sample_tools():
29 + return [
30 ChatCompletionToolsParam(
31 function=FunctionDefinition(name="get_current_date", parameters={}),
32 ),
@@ -49,6 +43,17 @@ def mock_request() -> ChatCompletionRequest:
43 ),
44 ),
45 ]
46 +
47 +
48 +@pytest.fixture
49 +def glm47_tool_parser(glm47_tokenizer, sample_tools):
50 + return Glm47MoeModelToolParser(glm47_tokenizer, tools=sample_tools)
51 +
52 +
53 +@pytest.fixture
54 +def mock_request(sample_tools) -> ChatCompletionRequest:
55 + request = Mock(spec=ChatCompletionRequest)
56 + request.tools = sample_tools
57 request.tool_choice = "auto"
58 return request
59
tests/tool_parsers/test_glm4_moe_tool_parser.py
+32 -27
@@ -27,14 +27,8 @@ def glm4_moe_tokenizer():
27
28
29 @pytest.fixture
30 -def glm4_moe_tool_parser(glm4_moe_tokenizer):
31 - return Glm4MoeModelToolParser(glm4_moe_tokenizer)
32 -
33 -
34 -@pytest.fixture
35 -def mock_request() -> ChatCompletionRequest:
36 - request = Mock(spec=ChatCompletionRequest)
37 - request.tools = [ # GLM45 parser needs this attribute to enable tool parsing.
30 +def sample_tools():
31 + return [
32 ChatCompletionToolsParam(
33 function=FunctionDefinition(
34 name="get_weather",
@@ -42,6 +36,17 @@ def mock_request() -> ChatCompletionRequest:
36 ),
37 ),
38 ]
39 +
40 +
41 +@pytest.fixture
42 +def glm4_moe_tool_parser(glm4_moe_tokenizer, sample_tools):
43 + return Glm4MoeModelToolParser(glm4_moe_tokenizer, tools=sample_tools)
44 +
45 +
46 +@pytest.fixture
47 +def mock_request(sample_tools) -> ChatCompletionRequest:
48 + request = Mock(spec=ChatCompletionRequest)
49 + request.tools = sample_tools
50 return request
51
52
@@ -671,14 +676,13 @@ def test_streaming_json_escape_in_string(glm4_moe_tool_parser, mock_request):
676 assert '"' in parsed["message"] or "world" in parsed["message"]
677
678
674 -def test_streaming_long_content_incremental(glm4_moe_tool_parser):
679 +def test_streaming_long_content_incremental(glm4_moe_tokenizer):
680 """Test incremental streaming of long content (Issue #32829).
681
682 This is the core fix: for long string values like code (4000+ chars),
683 the parser should stream incrementally rather than buffering until
684 complete. This test verifies we get many fragments, not just 1-3.
685 """
681 - _reset_streaming_state(glm4_moe_tool_parser)
686
687 # Bubble sort example from Issue #32829 - realistic long content
688 bubble_sort_code = '''#!/usr/bin/env python3
@@ -705,27 +709,28 @@ if __name__ == "__main__":
709 sorted_arr = bubble_sort(test_arr.copy())
710 print(f"Sorted: {sorted_arr}")'''
711
708 - # Create a request with tool schema to enable string type detection
712 + # Create tools with schema to enable string type detection
713 # This is required for incremental streaming of string values
714 + tools = [
715 + ChatCompletionToolsParam(
716 + function=FunctionDefinition(
717 + name="write_to_file",
718 + parameters={
719 + "type": "object",
720 + "properties": {
721 + "file_path": {"type": "string"},
722 + "content": {"type": "string"},
723 + },
724 + },
725 + ),
726 + ),
727 + ]
728 + glm4_moe_tool_parser = Glm4MoeModelToolParser(glm4_moe_tokenizer, tools=tools)
729 request = ChatCompletionRequest(
730 model=MODEL,
731 messages=[],
713 - tools=[
714 - {
715 - "type": "function",
716 - "function": {
717 - "name": "write_to_file",
718 - "parameters": {
719 - "type": "object",
720 - "properties": {
721 - "file_path": {"type": "string"},
722 - "content": {"type": "string"},
723 - },
724 - },
725 - },
726 - }
727 - ],
728 - ) # type: ignore
732 + tools=tools,
733 + )
734
735 # Simulate token-based streaming (special tags as single tokens)
736 chunks = [
tests/tool_parsers/test_qwen3coder_tool_parser.py
+10 -12
@@ -31,13 +31,13 @@ def qwen3_tokenizer():
31
32
33 @pytest.fixture
34 -def qwen3_tool_parser(qwen3_tokenizer):
35 - return Qwen3CoderToolParser(qwen3_tokenizer)
34 +def qwen3_tool_parser(qwen3_tokenizer, sample_tools):
35 + return Qwen3CoderToolParser(qwen3_tokenizer, tools=sample_tools)
36
37
38 @pytest.fixture
39 -def qwen3_xml_tool_parser(qwen3_tokenizer):
40 - return Qwen3XMLToolParser(qwen3_tokenizer)
39 +def qwen3_xml_tool_parser(qwen3_tokenizer, sample_tools):
40 + return Qwen3XMLToolParser(qwen3_tokenizer, tools=sample_tools)
41
42
43 @pytest.fixture(params=["xml"])
@@ -376,7 +376,7 @@ TX
376 assert extracted_tool_calls.tool_calls[0].function.name == "get_current_weather"
377
378
379 -def test_extract_tool_calls_type_conversion(qwen3_tool_parser_parametrized):
379 +def test_extract_tool_calls_type_conversion(qwen3_tokenizer):
380 """Test parameter type conversion based on tool schema"""
381 tools = [
382 ChatCompletionToolsParam(
@@ -417,10 +417,9 @@ hello world
417 </function>
418 </tool_call>"""
419
420 + parser = Qwen3XMLToolParser(qwen3_tokenizer, tools=tools)
421 request = ChatCompletionRequest(model=MODEL, messages=[], tools=tools)
421 - extracted_tool_calls = qwen3_tool_parser_parametrized.extract_tool_calls(
422 - model_output, request=request
423 - )
422 + extracted_tool_calls = parser.extract_tool_calls(model_output, request=request)
423
424 args = json.loads(extracted_tool_calls.tool_calls[0].function.arguments)
425 assert args["int_param"] == 42
@@ -859,7 +858,7 @@ TX
858
859
860 def test_extract_tool_calls_complex_type_with_single_quote(
862 - qwen3_tool_parser_parametrized,
861 + qwen3_tokenizer,
862 ):
863 """Test parameter type conversion based on tool schema"""
864 tools = [
@@ -889,10 +888,9 @@ def test_extract_tool_calls_complex_type_with_single_quote(
888 </function>
889 </tool_call>"""
890
891 + parser = Qwen3XMLToolParser(qwen3_tokenizer, tools=tools)
892 request = ChatCompletionRequest(model=MODEL, messages=[], tools=tools)
893 - extracted_tool_calls = qwen3_tool_parser_parametrized.extract_tool_calls(
894 - model_output, request=request
895 - )
893 + extracted_tool_calls = parser.extract_tool_calls(model_output, request=request)
894
895 args = json.loads(extracted_tool_calls.tool_calls[0].function.arguments)
896 assert args["obj_param"] == {"key": "value"}
tests/tool_parsers/test_seed_oss_tool_parser.py
+2 -2
@@ -30,8 +30,8 @@ def seed_oss_tokenizer():
30
31
32 @pytest.fixture
33 -def seed_oss_tool_parser(seed_oss_tokenizer):
34 - return SeedOssToolParser(seed_oss_tokenizer)
33 +def seed_oss_tool_parser(seed_oss_tokenizer, sample_tools):
34 + return SeedOssToolParser(seed_oss_tokenizer, tools=sample_tools)
35
36
37 @pytest.fixture
tests/tool_parsers/test_step3p5_tool_parser.py
+8 -10
@@ -28,8 +28,8 @@ def step3p5_tokenizer():
28
29
30 @pytest.fixture
31 -def step3p5_tool_parser(step3p5_tokenizer):
32 - return Step3p5ToolParser(step3p5_tokenizer)
31 +def step3p5_tool_parser(step3p5_tokenizer, sample_tools):
32 + return Step3p5ToolParser(step3p5_tokenizer, tools=sample_tools)
33
34
35 @pytest.fixture
@@ -386,7 +386,7 @@ TX
386 assert extracted_tool_calls.tool_calls[0].function.name == "get_current_weather"
387
388
389 -def test_extract_tool_calls_type_conversion(step3p5_tool_parser):
389 +def test_extract_tool_calls_type_conversion(step3p5_tokenizer):
390 """Test parameter type conversion based on tool schema"""
391 tools = [
392 ChatCompletionToolsParam(
@@ -427,10 +427,9 @@ hello world
427 </function>
428 </tool_call>"""
429
430 + parser = Step3p5ToolParser(step3p5_tokenizer, tools=tools)
431 request = ChatCompletionRequest(model=MODEL, messages=[], tools=tools)
431 - extracted_tool_calls = step3p5_tool_parser.extract_tool_calls(
432 - model_output, request=request
433 - )
432 + extracted_tool_calls = parser.extract_tool_calls(model_output, request=request)
433
434 args = json.loads(extracted_tool_calls.tool_calls[0].function.arguments)
435 assert args["int_param"] == 42
@@ -864,7 +863,7 @@ TX
863 assert parsed_args["state"] == "TX"
864
865
867 -def test_extract_tool_calls_complex_type_with_single_quote(step3p5_tool_parser):
866 +def test_extract_tool_calls_complex_type_with_single_quote(step3p5_tokenizer):
867 """Test parameter type conversion based on tool schema"""
868 tools = [
869 ChatCompletionToolsParam(
@@ -893,10 +892,9 @@ def test_extract_tool_calls_complex_type_with_single_quote(step3p5_tool_parser):
892 </function>
893 </tool_call>"""
894
895 + parser = Step3p5ToolParser(step3p5_tokenizer, tools=tools)
896 request = ChatCompletionRequest(model=MODEL, messages=[], tools=tools)
897 - extracted_tool_calls = step3p5_tool_parser.extract_tool_calls(
898 - model_output, request=request
899 - )
897 + extracted_tool_calls = parser.extract_tool_calls(model_output, request=request)
898
899 args = json.loads(extracted_tool_calls.tool_calls[0].function.arguments)
900 assert args["obj_param"] == {"key": "value"}
vllm/tool_parsers/abstract_tool_parser.py
+10 -1
@@ -10,9 +10,11 @@ from openai.types.responses import (
10 ResponseFormatTextJSONSchemaConfig,
11 ResponseTextConfig,
12 )
13 +from openai.types.responses.function_tool import FunctionTool
14
15 from vllm.entrypoints.openai.chat_completion.protocol import (
16 ChatCompletionRequest,
17 + ChatCompletionToolsParam,
18 )
19 from vllm.entrypoints.openai.engine.protocol import (
20 DeltaMessage,
@@ -54,7 +56,14 @@ class ToolParser:
56 self.streamed_args_for_tool: list[str] = []
57
58 self.model_tokenizer = tokenizer
57 - self.tools = tools
59 + if tools:
60 + self.tools: list[ChatCompletionToolsParam | FunctionTool] = [
61 + tool
62 + for tool in tools
63 + if isinstance(tool, (ChatCompletionToolsParam, FunctionTool))
64 + ]
65 + else:
66 + self.tools = []
67
68 @cached_property
69 def vocab(self) -> dict[str, int]:
vllm/tool_parsers/deepseekv32_tool_parser.py
+3 -6
@@ -142,12 +142,11 @@ class DeepSeekV32ToolParser(ToolParser):
142 self,
143 function_name: str,
144 param_dict: dict[str, str],
145 - request: ChatCompletionRequest | None,
145 ) -> dict[str, Any]:
146 """Convert raw string param values using the tool schema types."""
147 param_config: dict = {}
149 - if request and request.tools:
150 - for tool in request.tools:
148 + if self.tools:
149 + for tool in self.tools:
150 if (
151 hasattr(tool, "function")
152 and tool.function.name == function_name
@@ -241,9 +240,7 @@ class DeepSeekV32ToolParser(ToolParser):
240 invoke_name, invoke_body = complete_invokes[self.current_tool_index]
241 param_dict = self._parse_invoke_params(invoke_body)
242
244 - converted = self._convert_params_with_schema(
245 - invoke_name, param_dict, request
246 - )
243 + converted = self._convert_params_with_schema(invoke_name, param_dict)
244 args_json = json.dumps(converted, ensure_ascii=False)
245 idx = self.current_tool_index
246 self.current_tool_index += 1
vllm/tool_parsers/glm4_moe_tool_parser.py
+2 -2
@@ -189,7 +189,7 @@ class Glm4MoeModelToolParser(ToolParser):
189 for key, value in pairs:
190 arg_key = key.strip()
191 arg_val = value.strip()
192 - if not self._is_string_type(tc_name, arg_key, request.tools):
192 + if not self._is_string_type(tc_name, arg_key, self.tools):
193 arg_val = self._deserialize(arg_val)
194 logger.debug("arg_key = %s, arg_val = %s", arg_key, arg_val)
195 arg_dct[arg_key] = arg_val
@@ -330,7 +330,7 @@ class Glm4MoeModelToolParser(ToolParser):
330 key = (self._pending_key or "").strip()
331
332 is_string = self._is_string_type(
333 - self._current_tool_name, key, request.tools
333 + self._current_tool_name, key, self.tools
334 )
335
336 if is_string:
vllm/tool_parsers/internlm2_tool_parser.py
+1 -1
@@ -200,7 +200,7 @@ class Internlm2ToolParser(ToolParser):
200 request: ChatCompletionRequest,
201 ) -> ExtractedToolCallInformation:
202 text = model_output
203 - tools = request.tools
203 + tools = self.tools
204 if "<|action_start|><|plugin|>" in text:
205 text, action = text.split("<|action_start|><|plugin|>")
206 action = action.split("<|action_end|>".strip())[0]
vllm/tool_parsers/minimax_m2_tool_parser.py
+2 -4
@@ -308,7 +308,7 @@ class MinimaxM2ToolParser(ToolParser):
308 invoke_str = complete_invokes[self.current_tool_index]
309 tool_call = self._parse_single_invoke(
310 invoke_str,
311 - request.tools if request else None,
311 + self.tools,
312 )
313 if not tool_call:
314 self.current_tool_index += 1
@@ -358,9 +358,7 @@ class MinimaxM2ToolParser(ToolParser):
358 for tool_call_match in self.tool_call_complete_regex.findall(model_output):
359 # Find all invokes within this tool_call
360 for invoke_match in self.invoke_complete_regex.findall(tool_call_match):
361 - tool_call = self._parse_single_invoke(
362 - invoke_match, request.tools if request else None
363 - )
361 + tool_call = self._parse_single_invoke(invoke_match, self.tools)
362 if tool_call:
363 tool_calls.append(tool_call)
364
vllm/tool_parsers/qwen3coder_tool_parser.py
+3 -5
@@ -314,7 +314,7 @@ class Qwen3CoderToolParser(ToolParser):
314 )
315
316 tool_calls = [
317 - self._parse_xml_function_call(function_call_str, request.tools)
317 + self._parse_xml_function_call(function_call_str, self.tools)
318 for function_call_str in function_calls
319 ]
320 # Populate prev_tool_call_arr for serving layer to set finish_reason
@@ -607,7 +607,7 @@ class Qwen3CoderToolParser(ToolParser):
607
608 param_config = self._get_arguments_config(
609 self.current_function_name or "",
610 - self.streaming_request.tools if self.streaming_request else None,
610 + self.tools,
611 )
612
613 converted_value = self._convert_param_value(
@@ -666,9 +666,7 @@ class Qwen3CoderToolParser(ToolParser):
666 try:
667 parsed_tool = self._parse_xml_function_call(
668 func_content,
669 - self.streaming_request.tools
670 - if self.streaming_request
671 - else None,
669 + self.tools,
670 )
671 if parsed_tool and self.current_tool_index < len(
672 self.prev_tool_call_arr
vllm/tool_parsers/qwen3xml_tool_parser.py
+2 -4
@@ -1188,8 +1188,7 @@ class Qwen3XMLToolParser(ToolParser):
1188 # Reset tool call tracking arrays for new extraction
1189 self.prev_tool_call_arr = []
1190 self.streamed_args_for_tool = []
1191 - if request:
1192 - self.parser.set_tools(request.tools)
1191 + self.parser.set_tools(self.tools)
1192 result = self.parser.parse_single_streaming_chunks(model_output)
1193 if not result.tool_calls:
1194 return ExtractedToolCallInformation(
@@ -1260,8 +1259,7 @@ class Qwen3XMLToolParser(ToolParser):
1259 # Reset tool call tracking arrays for new streaming session
1260 self.prev_tool_call_arr = []
1261 self.streamed_args_for_tool = []
1263 - if request:
1264 - self.parser.set_tools(request.tools)
1262 + self.parser.set_tools(self.tools)
1263
1264 # Model sometimes outputs separately causing delta_text to be empty.
1265 # If there were tool_calls before and all current tool_calls have ended,
vllm/tool_parsers/seed_oss_tool_parser.py
+2 -2
@@ -312,7 +312,7 @@ class SeedOssToolParser(ToolParser):
312 )
313
314 tool_calls = [
315 - self._parse_xml_function_call(function_call_str, request.tools)
315 + self._parse_xml_function_call(function_call_str, self.tools)
316 for function_call_str in function_calls
317 ]
318
@@ -566,7 +566,7 @@ class SeedOssToolParser(ToolParser):
566 # Parse to get the complete arguments
567 try:
568 parsed_tool = self._parse_xml_function_call(
569 - func_content, request.tools if request else None
569 + func_content, self.tools
570 )
571 if parsed_tool:
572 # Update existing entry in prev_tool_call_arr with complete arguments
vllm/tool_parsers/step3_tool_parser.py
+2 -4
@@ -82,9 +82,8 @@ class Step3ToolParser(ToolParser):
82 self,
83 func_name: str,
84 params: dict[str, Any],
85 - request: ChatCompletionRequest,
85 ) -> dict[str, Any]:
87 - for tool in request.tools or []:
86 + for tool in self.tools or []:
87 if tool.function.name == func_name:
88 schema = tool.function.parameters or {}
89 properties = schema.get("properties", {})
@@ -234,7 +233,6 @@ class Step3ToolParser(ToolParser):
233 final_args = self._cast_arguments(
234 function_name,
235 tool_call_arr.get("parameters", {}), # type: ignore
237 - request,
236 )
237 if final_args:
238 final_args_json = json.dumps(final_args, ensure_ascii=False)
@@ -291,7 +289,7 @@ class Step3ToolParser(ToolParser):
289 function_name, params_dict = self._parse_steptml_invoke(invoke_part)
290
291 if function_name and params_dict is not None:
294 - params_dict = self._cast_arguments(function_name, params_dict, request)
292 + params_dict = self._cast_arguments(function_name, params_dict)
293 params_str = json.dumps(params_dict, ensure_ascii=False)
294 tool_calls.append(
295 ToolCall(
vllm/tool_parsers/step3p5_tool_parser.py
+2 -4
@@ -1385,8 +1385,7 @@ class Step3p5ToolParser(ToolParser):
1385 # Reset tool call tracking arrays for new extraction
1386 self.prev_tool_call_arr = []
1387 self.streamed_args_for_tool = []
1388 - if request:
1389 - self.parser.set_tools(request.tools)
1388 + self.parser.set_tools(self.tools)
1389 result = self.parser.parse_single_streaming_chunks(model_output)
1390 if not result.tool_calls:
1391 return ExtractedToolCallInformation(
@@ -1457,8 +1456,7 @@ class Step3p5ToolParser(ToolParser):
1456 # Reset tool call tracking arrays for new streaming session
1457 self.prev_tool_call_arr = []
1458 self.streamed_args_for_tool = []
1460 - if request:
1461 - self.parser.set_tools(request.tools)
1459 + self.parser.set_tools(self.tools)
1460
1461 # Model sometimes outputs separately causing delta_text to be empty.
1462 # If there were tool_calls before and all current tool_calls have ended,