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

Signed-off-by: sfeng33 <4florafeng@gmail.com>
This commit is contained in:
Flora Feng
2026-03-31 01:41:36 -04:00
committed by GitHub
parent 44eef0ca1e
commit d53cb9cb8e
16 changed files with 113 additions and 105 deletions

View File

@@ -11,6 +11,10 @@ from unittest.mock import MagicMock
import pytest
from vllm.entrypoints.openai.chat_completion.protocol import (
ChatCompletionToolsParam,
FunctionDefinition,
)
from vllm.tokenizers import get_tokenizer
from vllm.tool_parsers.deepseekv32_tool_parser import DeepSeekV32ToolParser
@@ -24,8 +28,8 @@ MOCK_TOKENIZER = MagicMock()
MOCK_TOKENIZER.get_vocab.return_value = {}
def make_parser() -> DeepSeekV32ToolParser:
return DeepSeekV32ToolParser(MOCK_TOKENIZER)
def make_parser(tools=None) -> DeepSeekV32ToolParser:
return DeepSeekV32ToolParser(MOCK_TOKENIZER, tools=tools)
def make_tool_param(name: str, params: dict) -> MagicMock:
@@ -275,20 +279,22 @@ class TestExtractToolCallsStreaming:
content = "".join(d.content for d in deltas if d.content is not None)
assert "Thinking" in content
def test_type_conversion_in_streaming(self, parser):
tool = make_tool_param(
"add",
{
"type": "object",
"properties": {
"x": {"type": "integer"},
"y": {"type": "integer"},
def test_type_conversion_in_streaming(self):
tool = ChatCompletionToolsParam(
function=FunctionDefinition(
name="add",
parameters={
"type": "object",
"properties": {
"x": {"type": "integer"},
"y": {"type": "integer"},
},
},
},
),
)
request = make_request(tools=[tool])
parser = make_parser(tools=[tool])
full_text = build_tool_call("add", {"x": "3", "y": "4"})
deltas = self._stream(parser, full_text, request=request)
deltas = self._stream(parser, full_text)
args_str = self._reconstruct_args(deltas)
assert json.loads(args_str) == {"x": 3, "y": 4}

View File

@@ -25,14 +25,8 @@ def glm47_tokenizer():
@pytest.fixture
def glm47_tool_parser(glm47_tokenizer):
return Glm47MoeModelToolParser(glm47_tokenizer)
@pytest.fixture
def mock_request() -> ChatCompletionRequest:
request = Mock(spec=ChatCompletionRequest)
request.tools = [
def sample_tools():
return [
ChatCompletionToolsParam(
function=FunctionDefinition(name="get_current_date", parameters={}),
),
@@ -49,6 +43,17 @@ def mock_request() -> ChatCompletionRequest:
),
),
]
@pytest.fixture
def glm47_tool_parser(glm47_tokenizer, sample_tools):
return Glm47MoeModelToolParser(glm47_tokenizer, tools=sample_tools)
@pytest.fixture
def mock_request(sample_tools) -> ChatCompletionRequest:
request = Mock(spec=ChatCompletionRequest)
request.tools = sample_tools
request.tool_choice = "auto"
return request

View File

@@ -27,14 +27,8 @@ def glm4_moe_tokenizer():
@pytest.fixture
def glm4_moe_tool_parser(glm4_moe_tokenizer):
return Glm4MoeModelToolParser(glm4_moe_tokenizer)
@pytest.fixture
def mock_request() -> ChatCompletionRequest:
request = Mock(spec=ChatCompletionRequest)
request.tools = [ # GLM45 parser needs this attribute to enable tool parsing.
def sample_tools():
return [
ChatCompletionToolsParam(
function=FunctionDefinition(
name="get_weather",
@@ -42,6 +36,17 @@ def mock_request() -> ChatCompletionRequest:
),
),
]
@pytest.fixture
def glm4_moe_tool_parser(glm4_moe_tokenizer, sample_tools):
return Glm4MoeModelToolParser(glm4_moe_tokenizer, tools=sample_tools)
@pytest.fixture
def mock_request(sample_tools) -> ChatCompletionRequest:
request = Mock(spec=ChatCompletionRequest)
request.tools = sample_tools
return request
@@ -671,14 +676,13 @@ def test_streaming_json_escape_in_string(glm4_moe_tool_parser, mock_request):
assert '"' in parsed["message"] or "world" in parsed["message"]
def test_streaming_long_content_incremental(glm4_moe_tool_parser):
def test_streaming_long_content_incremental(glm4_moe_tokenizer):
"""Test incremental streaming of long content (Issue #32829).
This is the core fix: for long string values like code (4000+ chars),
the parser should stream incrementally rather than buffering until
complete. This test verifies we get many fragments, not just 1-3.
"""
_reset_streaming_state(glm4_moe_tool_parser)
# Bubble sort example from Issue #32829 - realistic long content
bubble_sort_code = '''#!/usr/bin/env python3
@@ -705,27 +709,28 @@ if __name__ == "__main__":
sorted_arr = bubble_sort(test_arr.copy())
print(f"Sorted: {sorted_arr}")'''
# Create a request with tool schema to enable string type detection
# Create tools with schema to enable string type detection
# This is required for incremental streaming of string values
tools = [
ChatCompletionToolsParam(
function=FunctionDefinition(
name="write_to_file",
parameters={
"type": "object",
"properties": {
"file_path": {"type": "string"},
"content": {"type": "string"},
},
},
),
),
]
glm4_moe_tool_parser = Glm4MoeModelToolParser(glm4_moe_tokenizer, tools=tools)
request = ChatCompletionRequest(
model=MODEL,
messages=[],
tools=[
{
"type": "function",
"function": {
"name": "write_to_file",
"parameters": {
"type": "object",
"properties": {
"file_path": {"type": "string"},
"content": {"type": "string"},
},
},
},
}
],
) # type: ignore
tools=tools,
)
# Simulate token-based streaming (special tags as single tokens)
chunks = [

View File

@@ -31,13 +31,13 @@ def qwen3_tokenizer():
@pytest.fixture
def qwen3_tool_parser(qwen3_tokenizer):
return Qwen3CoderToolParser(qwen3_tokenizer)
def qwen3_tool_parser(qwen3_tokenizer, sample_tools):
return Qwen3CoderToolParser(qwen3_tokenizer, tools=sample_tools)
@pytest.fixture
def qwen3_xml_tool_parser(qwen3_tokenizer):
return Qwen3XMLToolParser(qwen3_tokenizer)
def qwen3_xml_tool_parser(qwen3_tokenizer, sample_tools):
return Qwen3XMLToolParser(qwen3_tokenizer, tools=sample_tools)
@pytest.fixture(params=["xml"])
@@ -376,7 +376,7 @@ TX
assert extracted_tool_calls.tool_calls[0].function.name == "get_current_weather"
def test_extract_tool_calls_type_conversion(qwen3_tool_parser_parametrized):
def test_extract_tool_calls_type_conversion(qwen3_tokenizer):
"""Test parameter type conversion based on tool schema"""
tools = [
ChatCompletionToolsParam(
@@ -417,10 +417,9 @@ hello world
</function>
</tool_call>"""
parser = Qwen3XMLToolParser(qwen3_tokenizer, tools=tools)
request = ChatCompletionRequest(model=MODEL, messages=[], tools=tools)
extracted_tool_calls = qwen3_tool_parser_parametrized.extract_tool_calls(
model_output, request=request
)
extracted_tool_calls = parser.extract_tool_calls(model_output, request=request)
args = json.loads(extracted_tool_calls.tool_calls[0].function.arguments)
assert args["int_param"] == 42
@@ -859,7 +858,7 @@ TX
def test_extract_tool_calls_complex_type_with_single_quote(
qwen3_tool_parser_parametrized,
qwen3_tokenizer,
):
"""Test parameter type conversion based on tool schema"""
tools = [
@@ -889,10 +888,9 @@ def test_extract_tool_calls_complex_type_with_single_quote(
</function>
</tool_call>"""
parser = Qwen3XMLToolParser(qwen3_tokenizer, tools=tools)
request = ChatCompletionRequest(model=MODEL, messages=[], tools=tools)
extracted_tool_calls = qwen3_tool_parser_parametrized.extract_tool_calls(
model_output, request=request
)
extracted_tool_calls = parser.extract_tool_calls(model_output, request=request)
args = json.loads(extracted_tool_calls.tool_calls[0].function.arguments)
assert args["obj_param"] == {"key": "value"}

View File

@@ -30,8 +30,8 @@ def seed_oss_tokenizer():
@pytest.fixture
def seed_oss_tool_parser(seed_oss_tokenizer):
return SeedOssToolParser(seed_oss_tokenizer)
def seed_oss_tool_parser(seed_oss_tokenizer, sample_tools):
return SeedOssToolParser(seed_oss_tokenizer, tools=sample_tools)
@pytest.fixture

View File

@@ -28,8 +28,8 @@ def step3p5_tokenizer():
@pytest.fixture
def step3p5_tool_parser(step3p5_tokenizer):
return Step3p5ToolParser(step3p5_tokenizer)
def step3p5_tool_parser(step3p5_tokenizer, sample_tools):
return Step3p5ToolParser(step3p5_tokenizer, tools=sample_tools)
@pytest.fixture
@@ -386,7 +386,7 @@ TX
assert extracted_tool_calls.tool_calls[0].function.name == "get_current_weather"
def test_extract_tool_calls_type_conversion(step3p5_tool_parser):
def test_extract_tool_calls_type_conversion(step3p5_tokenizer):
"""Test parameter type conversion based on tool schema"""
tools = [
ChatCompletionToolsParam(
@@ -427,10 +427,9 @@ hello world
</function>
</tool_call>"""
parser = Step3p5ToolParser(step3p5_tokenizer, tools=tools)
request = ChatCompletionRequest(model=MODEL, messages=[], tools=tools)
extracted_tool_calls = step3p5_tool_parser.extract_tool_calls(
model_output, request=request
)
extracted_tool_calls = parser.extract_tool_calls(model_output, request=request)
args = json.loads(extracted_tool_calls.tool_calls[0].function.arguments)
assert args["int_param"] == 42
@@ -864,7 +863,7 @@ TX
assert parsed_args["state"] == "TX"
def test_extract_tool_calls_complex_type_with_single_quote(step3p5_tool_parser):
def test_extract_tool_calls_complex_type_with_single_quote(step3p5_tokenizer):
"""Test parameter type conversion based on tool schema"""
tools = [
ChatCompletionToolsParam(
@@ -893,10 +892,9 @@ def test_extract_tool_calls_complex_type_with_single_quote(step3p5_tool_parser):
</function>
</tool_call>"""
parser = Step3p5ToolParser(step3p5_tokenizer, tools=tools)
request = ChatCompletionRequest(model=MODEL, messages=[], tools=tools)
extracted_tool_calls = step3p5_tool_parser.extract_tool_calls(
model_output, request=request
)
extracted_tool_calls = parser.extract_tool_calls(model_output, request=request)
args = json.loads(extracted_tool_calls.tool_calls[0].function.arguments)
assert args["obj_param"] == {"key": "value"}