[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:
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user