Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 137 additions & 0 deletions tests/tool_parsers/test_deepseekv31_tool_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project

import json
from unittest.mock import MagicMock, patch

import pytest

import vllm.envs as envs
from tests.tool_parsers.utils import run_tool_extraction
from vllm.entrypoints.openai.chat_completion.protocol import (
ChatCompletionRequest,
)
from vllm.tokenizers import get_tokenizer
from vllm.tool_parsers.deepseekv31_tool_parser import (
DeepSeekV31ToolParser,
Expand Down Expand Up @@ -81,3 +86,135 @@ def test_extract_tool_calls_with_multiline_arguments(parser, streaming):
assert [call.function.name for call in tool_calls] == ["foo", "bar"]
assert json.loads(tool_calls[0].function.arguments) == args
assert json.loads(tool_calls[1].function.arguments) == {"y": 2}


_TOOL_CALLS_BEGIN = "<|tool▁calls▁begin|>"
_TOOL_CALLS_END = "<|tool▁calls▁end|>"
_TOOL_CALL_BEGIN = "<|tool▁call▁begin|>"
_TOOL_CALL_END = "<|tool▁call▁end|>"
_TOOL_SEP = "<|tool▁sep|>"


def _fake_deepseek_tokenizer() -> MagicMock:
tokenizer = MagicMock()
tokenizer.get_vocab.return_value = {
_TOOL_CALLS_BEGIN: 1,
_TOOL_CALLS_END: 2,
_TOOL_CALL_BEGIN: 3,
_TOOL_CALL_END: 4,
}
return tokenizer


def _timeout_request() -> ChatCompletionRequest:
return ChatCompletionRequest(messages=[], model="test-model")


def test_regex_timeout_treated_as_no_tool_call():
parser = DeepSeekV31ToolParser(_fake_deepseek_tokenizer())
model_output = f"{_TOOL_CALLS_BEGIN}{_TOOL_CALL_BEGIN}incomplete"
mock_regex = MagicMock()
mock_regex.findall.side_effect = TimeoutError("Regex timeout")

with patch.object(parser, "tool_call_regex", mock_regex):
result = parser.extract_tool_calls(model_output, _timeout_request())

assert result.tools_called is False
assert result.tool_calls == []
assert result.content == model_output
mock_regex.findall.assert_called_once()
assert (
mock_regex.findall.call_args.kwargs["timeout"]
== envs.VLLM_TOOL_PARSE_REGEX_TIMEOUT_SECONDS
)


def test_streaming_portion_regex_timeout_skips_delta():
parser = DeepSeekV31ToolParser(_fake_deepseek_tokenizer())
mock_regex = MagicMock()
mock_regex.match.side_effect = TimeoutError("Regex timeout")
current_text = f"{_TOOL_CALLS_BEGIN}{_TOOL_CALL_BEGIN}foo{_TOOL_SEP}" + '{"x":1}'
token_ids = [1, 3]

with patch.object(parser, "stream_tool_call_portion_regex", mock_regex):
result = parser.extract_tool_calls_streaming(
current_text,
current_text,
"",
token_ids,
token_ids,
[],
_timeout_request(),
)

assert result is None
mock_regex.match.assert_called_once()
assert (
mock_regex.match.call_args.kwargs["timeout"]
== envs.VLLM_TOOL_PARSE_REGEX_TIMEOUT_SECONDS
)


def test_streaming_name_regex_timeout_skips_delta():
parser = DeepSeekV31ToolParser(_fake_deepseek_tokenizer())
portion_regex = MagicMock()
portion_regex.match.return_value = None
name_regex = MagicMock()
name_regex.match.side_effect = TimeoutError("Regex timeout")
current_text = f"{_TOOL_CALLS_BEGIN}{_TOOL_CALL_BEGIN}foo{_TOOL_SEP}"
token_ids = [1, 3]

with (
patch.object(parser, "stream_tool_call_portion_regex", portion_regex),
patch.object(parser, "stream_tool_call_name_regex", name_regex),
):
result = parser.extract_tool_calls_streaming(
current_text,
current_text,
"",
token_ids,
token_ids,
[],
_timeout_request(),
)

assert result is None
name_regex.match.assert_called_once()
assert (
name_regex.match.call_args.kwargs["timeout"]
== envs.VLLM_TOOL_PARSE_REGEX_TIMEOUT_SECONDS
)


def test_streaming_regex_timeout_is_not_retried_on_later_delta():
parser = DeepSeekV31ToolParser(_fake_deepseek_tokenizer())
mock_regex = MagicMock()
mock_regex.match.side_effect = TimeoutError("Regex timeout")
current_text = f"{_TOOL_CALLS_BEGIN}{_TOOL_CALL_BEGIN}foo{_TOOL_SEP}" + '{"x":1}'
token_ids = [1, 3]
later_text = current_text + "0"
later_token_ids = [1, 3, 3]

with patch.object(parser, "stream_tool_call_portion_regex", mock_regex):
first = parser.extract_tool_calls_streaming(
current_text,
current_text,
"",
token_ids,
token_ids,
[],
_timeout_request(),
)
second = parser.extract_tool_calls_streaming(
later_text,
later_text,
"0",
token_ids,
later_token_ids,
[3],
_timeout_request(),
)

assert first is None
assert second is None
mock_regex.match.assert_called_once()
142 changes: 142 additions & 0 deletions tests/tool_parsers/test_deepseekv3_tool_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,21 @@
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project

import json
from unittest.mock import MagicMock, patch

import pytest

import vllm.envs as envs
from tests.tool_parsers.common_tests import (
ToolParserTestConfig,
ToolParserTests,
)
from tests.tool_parsers.utils import run_tool_extraction
from vllm.entrypoints.openai.chat_completion.protocol import (
ChatCompletionRequest,
)
from vllm.tokenizers import TokenizerLike, get_tokenizer
from vllm.tool_parsers.deepseekv3_tool_parser import DeepSeekV3ToolParser


class TestDeepSeekV3ToolParser(ToolParserTests):
Expand Down Expand Up @@ -146,3 +152,139 @@ def test_preserves_text_after_tool_call(
assert len(tool_calls) == 1
assert tool_calls[0].function.name == "get_weather"
assert tool_calls[0].function.arguments == '{"city": "Paris"}'


_TOOL_CALLS_BEGIN = "<|tool▁calls▁begin|>"
_TOOL_CALLS_END = "<|tool▁calls▁end|>"
_TOOL_CALL_BEGIN = "<|tool▁call▁begin|>"
_TOOL_CALL_END = "<|tool▁call▁end|>"
_TOOL_SEP = "<|tool▁sep|>"


def _fake_deepseek_tokenizer() -> MagicMock:
tokenizer = MagicMock()
tokenizer.get_vocab.return_value = {
_TOOL_CALLS_BEGIN: 1,
_TOOL_CALLS_END: 2,
_TOOL_CALL_BEGIN: 3,
_TOOL_CALL_END: 4,
}
return tokenizer


def _timeout_request() -> ChatCompletionRequest:
return ChatCompletionRequest(messages=[], model="test-model")


def test_regex_timeout_treated_as_no_tool_call():
parser = DeepSeekV3ToolParser(_fake_deepseek_tokenizer())
model_output = f"{_TOOL_CALLS_BEGIN}{_TOOL_CALL_BEGIN}incomplete"
mock_regex = MagicMock()
mock_regex.findall.side_effect = TimeoutError("Regex timeout")

with patch.object(parser, "tool_call_regex", mock_regex):
result = parser.extract_tool_calls(model_output, _timeout_request())

assert result.tools_called is False
assert result.tool_calls == []
assert result.content == model_output
mock_regex.findall.assert_called_once()
assert (
mock_regex.findall.call_args.kwargs["timeout"]
== envs.VLLM_TOOL_PARSE_REGEX_TIMEOUT_SECONDS
)


def test_streaming_portion_regex_timeout_skips_delta():
parser = DeepSeekV3ToolParser(_fake_deepseek_tokenizer())
mock_regex = MagicMock()
mock_regex.match.side_effect = TimeoutError("Regex timeout")
current_text = (
f"{_TOOL_CALLS_BEGIN}{_TOOL_CALL_BEGIN}function{_TOOL_SEP}get_weather"
)
token_ids = [1, 3]

with patch.object(parser, "stream_tool_call_portion_regex", mock_regex):
result = parser.extract_tool_calls_streaming(
current_text,
current_text,
"",
token_ids,
token_ids,
[],
_timeout_request(),
)

assert result is None
mock_regex.match.assert_called_once()
assert (
mock_regex.match.call_args.kwargs["timeout"]
== envs.VLLM_TOOL_PARSE_REGEX_TIMEOUT_SECONDS
)


def test_streaming_name_regex_timeout_skips_delta():
parser = DeepSeekV3ToolParser(_fake_deepseek_tokenizer())
portion_regex = MagicMock()
portion_regex.match.return_value = None
name_regex = MagicMock()
name_regex.match.side_effect = TimeoutError("Regex timeout")
current_text = f"{_TOOL_CALLS_BEGIN}{_TOOL_CALL_BEGIN}function{_TOOL_SEP}"
token_ids = [1, 3]

with (
patch.object(parser, "stream_tool_call_portion_regex", portion_regex),
patch.object(parser, "stream_tool_call_name_regex", name_regex),
):
result = parser.extract_tool_calls_streaming(
current_text,
current_text,
"",
token_ids,
token_ids,
[],
_timeout_request(),
)

assert result is None
name_regex.match.assert_called_once()
assert (
name_regex.match.call_args.kwargs["timeout"]
== envs.VLLM_TOOL_PARSE_REGEX_TIMEOUT_SECONDS
)


def test_streaming_regex_timeout_is_not_retried_on_later_delta():
parser = DeepSeekV3ToolParser(_fake_deepseek_tokenizer())
mock_regex = MagicMock()
mock_regex.match.side_effect = TimeoutError("Regex timeout")
current_text = (
f"{_TOOL_CALLS_BEGIN}{_TOOL_CALL_BEGIN}function{_TOOL_SEP}get_weather"
)
token_ids = [1, 3]
later_text = current_text + "x"
later_token_ids = [1, 3, 3]

with patch.object(parser, "stream_tool_call_portion_regex", mock_regex):
first = parser.extract_tool_calls_streaming(
current_text,
current_text,
"",
token_ids,
token_ids,
[],
_timeout_request(),
)
second = parser.extract_tool_calls_streaming(
later_text,
later_text,
"x",
token_ids,
later_token_ids,
[3],
_timeout_request(),
)

assert first is None
assert second is None
mock_regex.match.assert_called_once()
Loading
Loading