main
py 274 lines 10.5 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 # ruff: noqa: E501
4 """Tests for the HYV3 tool call parser."""
5
6 import json
7 from unittest.mock import Mock
8
9 import pytest
10
11 from vllm.entrypoints.generate.base.protocol import DeltaMessage
12 from vllm.entrypoints.openai.chat_completion.protocol import (
13 ChatCompletionRequest,
14 ChatCompletionToolsParam,
15 FunctionDefinition,
16 )
17 from vllm.tokenizers import get_tokenizer
18 from vllm.tool_parsers.hy_v3_tool_parser import HYV3ToolParser
19
20 parser_name = "hy_v3"
21 MODEL = "tencent/Hy3-preview"
22
23
24 @pytest.fixture(scope="module")
25 def hy_v3_tokenizer():
26 return get_tokenizer(tokenizer_name=MODEL)
27
28
29 @pytest.fixture
30 def hy_v3_tool_parser(hy_v3_tokenizer):
31 return HYV3ToolParser(hy_v3_tokenizer)
32
33
34 @pytest.fixture
35 def mock_request() -> ChatCompletionRequest:
36 request = Mock(spec=ChatCompletionRequest)
37 request.tools = [
38 ChatCompletionToolsParam(
39 function=FunctionDefinition(name="get_current_date", parameters={}),
40 ),
41 ChatCompletionToolsParam(
42 function=FunctionDefinition(
43 name="get_weather",
44 parameters={
45 "type": "object",
46 "properties": {
47 "city": {"type": "string"},
48 "date": {"type": "string"},
49 },
50 },
51 ),
52 ),
53 ]
54 request.tool_choice = "auto"
55 return request
56
57
58 class TestHYV3ExtractToolCalls:
59 def test_no_tool_call(self, hy_v3_tool_parser, mock_request):
60 out = "This is a plain response."
61 r = hy_v3_tool_parser.extract_tool_calls(out, request=mock_request)
62 assert not r.tools_called
63 assert r.content == out
64
65 def test_zero_arg_inline(self, hy_v3_tool_parser, mock_request):
66 out = (
67 "<tool_calls><tool_call>get_current_date<tool_sep></tool_call></tool_calls>"
68 )
69 r = hy_v3_tool_parser.extract_tool_calls(out, request=mock_request)
70 assert r.tools_called
71 assert r.tool_calls[0].function.name == "get_current_date"
72 assert json.loads(r.tool_calls[0].function.arguments) == {}
73 assert r.content is None
74
75 def test_zero_arg_newline(self, hy_v3_tool_parser, mock_request):
76 out = "<tool_calls>\n<tool_call>get_current_date<tool_sep>\n</tool_call>\n</tool_calls>"
77 r = hy_v3_tool_parser.extract_tool_calls(out, request=mock_request)
78 assert r.tools_called
79 assert r.tool_calls[0].function.name == "get_current_date"
80
81 def test_args_same_line(self, hy_v3_tool_parser, mock_request):
82 out = (
83 "<tool_calls><tool_call>get_weather<tool_sep><arg_key>city</arg_key><arg_value>Beijing"
84 "</arg_value><arg_key>date</arg_key><arg_value>2026-03-30</arg_value></tool_call></tool_calls>"
85 )
86 r = hy_v3_tool_parser.extract_tool_calls(out, request=mock_request)
87 assert r.tools_called
88 assert json.loads(r.tool_calls[0].function.arguments) == {
89 "city": "Beijing",
90 "date": "2026-03-30",
91 }
92
93 def test_args_with_newlines(self, hy_v3_tool_parser, mock_request):
94 out = (
95 "<tool_calls>\n<tool_call>get_weather<tool_sep>\n<arg_key>city</arg_key>\n<arg_value>Beijing"
96 "</arg_value>\n<arg_key>date</arg_key>\n<arg_value>2026-03-30</arg_value>\n</tool_call>\n</tool_calls>"
97 )
98 r = hy_v3_tool_parser.extract_tool_calls(out, request=mock_request)
99 assert r.tools_called
100 assert json.loads(r.tool_calls[0].function.arguments) == {
101 "city": "Beijing",
102 "date": "2026-03-30",
103 }
104
105 def test_content_before(self, hy_v3_tool_parser, mock_request):
106 out = "Checking.<tool_calls>\n<tool_call>get_current_date<tool_sep>\n</tool_call>\n</tool_calls>"
107 r = hy_v3_tool_parser.extract_tool_calls(out, request=mock_request)
108 assert r.tools_called
109 assert r.content == "Checking."
110
111 def test_multiple(self, hy_v3_tool_parser, mock_request):
112 out = (
113 "<tool_calls>\n<tool_call>get_weather<tool_sep>\n<arg_key>city</arg_key>\n<arg_value>Beijing"
114 "</arg_value>\n<arg_key>date</arg_key>\n<arg_value>2026-03-30</arg_value>\n</tool_call>\n"
115 "<tool_call>get_weather<tool_sep>\n<arg_key>city</arg_key>\n<arg_value>Hangzhou</arg_value>\n"
116 "<arg_key>date</arg_key>\n<arg_value>2026-03-30</arg_value>\n</tool_call>\n</tool_calls>"
117 )
118 r = hy_v3_tool_parser.extract_tool_calls(out, request=mock_request)
119 assert len(r.tool_calls) == 2
120
121 def test_empty_content_none(self, hy_v3_tool_parser, mock_request):
122 out = "<tool_calls>\n<tool_call>get_current_date<tool_sep>\n</tool_call>\n</tool_calls>"
123 r = hy_v3_tool_parser.extract_tool_calls(out, request=mock_request)
124 assert r.content is None
125
126
127 def _simulate_streaming(
128 parser: HYV3ToolParser,
129 deltas: list[str],
130 request: ChatCompletionRequest,
131 ) -> list[DeltaMessage | None]:
132 results: list[DeltaMessage | None] = []
133 previous_text = ""
134 previous_token_ids: list[int] = []
135 vocab = parser.vocab
136 for delta_text in deltas:
137 current_text = previous_text + delta_text
138 delta_token_ids = [tid for tok, tid in vocab.items() if tok in delta_text]
139 current_token_ids = previous_token_ids + delta_token_ids
140 result = parser.extract_tool_calls_streaming(
141 previous_text=previous_text,
142 current_text=current_text,
143 delta_text=delta_text,
144 previous_token_ids=previous_token_ids,
145 current_token_ids=current_token_ids,
146 delta_token_ids=delta_token_ids,
147 request=request,
148 )
149 results.append(result)
150 previous_text = current_text
151 previous_token_ids = current_token_ids
152 return results
153
154
155 def _collect_streaming_tool_calls(results: list[DeltaMessage | None]) -> list[dict]:
156 tool_calls: dict[int, dict] = {}
157 for result in results:
158 if result is None or not result.tool_calls:
159 continue
160 for tc in result.tool_calls:
161 idx = tc.index
162 if idx not in tool_calls:
163 tool_calls[idx] = {
164 "name": tc.function.name or "",
165 "arguments": tc.function.arguments or "",
166 }
167 else:
168 if tc.function.name:
169 tool_calls[idx]["name"] += tc.function.name
170 if tc.function.arguments:
171 tool_calls[idx]["arguments"] += tc.function.arguments
172 return [tool_calls[i] for i in sorted(tool_calls.keys())]
173
174
175 def _collect_streaming_content(results: list[DeltaMessage | None]) -> str:
176 parts = []
177 for result in results:
178 if result is not None and result.content:
179 parts.append(result.content)
180 return "".join(parts)
181
182
183 class TestHYV3ExtractToolCallsStreaming:
184 def test_no_tool_call_streaming(self, hy_v3_tool_parser, mock_request):
185 deltas = ["This is ", "a plain ", "response."]
186 results = _simulate_streaming(hy_v3_tool_parser, deltas, mock_request)
187 content = _collect_streaming_content(results)
188 assert content == "This is a plain response."
189 assert len(_collect_streaming_tool_calls(results)) == 0
190
191 def test_zero_arg_streaming(self, hy_v3_tool_parser, mock_request):
192 deltas = [
193 "<tool_calls>",
194 "\n<tool_call>",
195 "get_current_date",
196 "<tool_sep>",
197 "\n</tool_call>",
198 "\n</tool_calls>",
199 ]
200 results = _simulate_streaming(hy_v3_tool_parser, deltas, mock_request)
201 tc = _collect_streaming_tool_calls(results)
202 assert len(tc) == 1
203 assert tc[0]["name"] == "get_current_date"
204 assert json.loads(tc[0]["arguments"]) == {}
205
206 def test_args_streaming(self, hy_v3_tool_parser, mock_request):
207 deltas = [
208 "<tool_calls>",
209 "\n<tool_call>",
210 "get_weather",
211 "<tool_sep>",
212 "\n<arg_key>city</arg_key>",
213 "\n<arg_value>Beijing</arg_value>",
214 "\n<arg_key>date</arg_key>",
215 "\n<arg_value>2026-03-30</arg_value>",
216 "\n</tool_call>",
217 "\n</tool_calls>",
218 ]
219 results = _simulate_streaming(hy_v3_tool_parser, deltas, mock_request)
220 tc = _collect_streaming_tool_calls(results)
221 assert len(tc) == 1 and tc[0]["name"] == "get_weather"
222 assert json.loads(tc[0]["arguments"]) == {
223 "city": "Beijing",
224 "date": "2026-03-30",
225 }
226
227 def test_content_before_streaming(self, hy_v3_tool_parser, mock_request):
228 deltas = [
229 "Checking.",
230 "<tool_calls>",
231 "\n<tool_call>",
232 "get_current_date",
233 "<tool_sep>",
234 "\n</tool_call>",
235 "\n</tool_calls>",
236 ]
237 results = _simulate_streaming(hy_v3_tool_parser, deltas, mock_request)
238 assert "Checking." in _collect_streaming_content(results)
239 tc = _collect_streaming_tool_calls(results)
240 assert len(tc) == 1 and tc[0]["name"] == "get_current_date"
241
242 def test_multiple_streaming(self, hy_v3_tool_parser, mock_request):
243 deltas = [
244 "<tool_calls>",
245 "\n<tool_call>",
246 "get_weather",
247 "<tool_sep>",
248 "\n<arg_key>city</arg_key>",
249 "\n<arg_value>Beijing</arg_value>",
250 "\n<arg_key>date</arg_key>",
251 "\n<arg_value>2026-03-30</arg_value>",
252 "\n</tool_call>",
253 "\n<tool_call>",
254 "get_weather",
255 "<tool_sep>",
256 "\n<arg_key>city</arg_key>",
257 "\n<arg_value>Hangzhou</arg_value>",
258 "\n<arg_key>date</arg_key>",
259 "\n<arg_value>2026-03-30</arg_value>",
260 "\n</tool_call>",
261 "\n</tool_calls>",
262 ]
263 results = _simulate_streaming(hy_v3_tool_parser, deltas, mock_request)
264 tc = _collect_streaming_tool_calls(results)
265 assert len(tc) == 2
266 assert json.loads(tc[0]["arguments"])["city"] == "Beijing"
267 assert json.loads(tc[1]["arguments"])["city"] == "Hangzhou"
268
269 def test_all_in_one_delta_streaming(self, hy_v3_tool_parser, mock_request):
270 out = "<tool_calls>\n<tool_call>get_current_date<tool_sep>\n</tool_call>\n</tool_calls>"
271 results = _simulate_streaming(hy_v3_tool_parser, [out], mock_request)
272 tc = _collect_streaming_tool_calls(results)
273 assert len(tc) == 1 and tc[0]["name"] == "get_current_date"
274 assert json.loads(tc[0]["arguments"]) == {}