[Misc] Add 20 regression tests for 11 tool parser bug fixes (#38172)

Signed-off-by: Ben Browning <bbrownin@redhat.com>
Co-authored-by: Chauncey <chaunceyjiang@gmail.com>
This commit is contained in:
Ben Browning
2026-03-31 23:00:31 -04:00
committed by GitHub
parent 40bb175027
commit cb0b443274
7 changed files with 700 additions and 0 deletions

View File

@@ -11,6 +11,7 @@ from unittest.mock import MagicMock
import pytest
from tests.tool_parsers.utils import run_tool_extraction_streaming
from vllm.entrypoints.openai.chat_completion.protocol import (
ChatCompletionToolsParam,
FunctionDefinition,
@@ -26,6 +27,7 @@ from vllm.tool_parsers.deepseekv32_tool_parser import DeepSeekV32ToolParser
# tokenizer object to be truthy (the parser checks `if not self.model_tokenizer`).
MOCK_TOKENIZER = MagicMock()
MOCK_TOKENIZER.get_vocab.return_value = {}
MOCK_TOKENIZER.tokenize.return_value = []
def make_parser(tools=None) -> DeepSeekV32ToolParser:
@@ -483,6 +485,88 @@ class TestExtractToolCallsStreaming:
assert all(not d.tool_calls for d in deltas)
class TestDelimiterPreservation:
"""Regression: fast detokenization skipping DSML delimiters (PR #33964)."""
@pytest.fixture
def parser(self):
return make_parser()
def test_delimiter_preserved_fast_detokenization(self, parser):
"""DSML delimiters as literal text must still be detected."""
# Delimiters appear as regular text (fast detokenization scenario).
model_output = (
f"{FC_START}\n"
f'{INV_START}get_weather">\n'
f'{PARAM_START}location" string="true">Tokyo{PARAM_END}\n'
f"{INV_END}\n"
f"{FC_END}"
)
# Non-streaming: parser must detect the tool call
result = parser.extract_tool_calls(model_output, None)
assert result.tools_called
assert len(result.tool_calls) == 1
assert result.tool_calls[0].function.name == "get_weather"
assert json.loads(result.tool_calls[0].function.arguments) == {
"location": "Tokyo"
}
assert result.content is None
# With content prefix
prefixed_output = "Here is the weather: " + model_output
result2 = parser.extract_tool_calls(prefixed_output, None)
assert result2.tools_called
assert result2.content == "Here is the weather: "
def test_tool_detection_skip_special_tokens_false(self, parser):
"""Regression: skip_special_tokens must be False when tools are enabled."""
# adjust_request must set skip_special_tokens=False
tool = make_tool_param(
"search",
{
"type": "object",
"properties": {
"query": {"type": "string"},
},
},
)
request = make_request(tools=[tool])
request.tool_choice = "auto"
adjusted = parser.adjust_request(request)
assert adjusted.skip_special_tokens is False
full_text = build_tool_call("search", {"query": "vllm documentation"})
# Non-streaming extraction
non_stream_result = parser.extract_tool_calls(full_text, request)
assert non_stream_result.tools_called
assert len(non_stream_result.tool_calls) == 1
assert non_stream_result.tool_calls[0].function.name == "search"
ns_args = json.loads(non_stream_result.tool_calls[0].function.arguments)
assert ns_args == {"query": "vllm documentation"}
# Streaming extraction: drive the parser line-by-line
chunks: list[str] = []
remaining = full_text
while remaining:
nl = remaining.find("\n")
if nl == -1:
chunks.append(remaining)
break
chunks.append(remaining[: nl + 1])
remaining = remaining[nl + 1 :]
reconstructor = run_tool_extraction_streaming(
parser, chunks, request, assert_one_tool_per_delta=False
)
assert len(reconstructor.tool_calls) == 1
assert reconstructor.tool_calls[0].function.name == "search"
streamed_args = json.loads(reconstructor.tool_calls[0].function.arguments)
assert streamed_args == ns_args
@pytest.fixture(scope="module")
def deepseekv32_tokenizer():
return get_tokenizer(tokenizer_name="deepseek-ai/DeepSeek-V3.2")