@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 7a49742b88
Fork 0

A high-throughput and memory-efficient inference and serving engine for LLMs

Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git

Synced Nov 6, 14:30 UTC

[CI/Build] Add common tool call parser test suite (#27599)

Signed-off-by: Ben Browning <bbrownin@redhat.com>

Ben Browning committed Mar 16, 2026 at 19:46 UTC 7a49742b8867e7d310abfd85c944e54d090e9301
16 files changed +1201 -5
tests/entrypoints/openai/tool_parsers/test_gigachat3_tool_parser.py
+1 -1
@@ -5,7 +5,7 @@ import json
5
6 import pytest
7
8 -from tests.entrypoints.openai.tool_parsers.utils import (
8 +from tests.tool_parsers.utils import (
9 run_tool_extraction,
10 run_tool_extraction_streaming,
11 )
tests/entrypoints/openai/tool_parsers/test_hunyuan_a13b_tool_parser.py
+1 -1
@@ -7,7 +7,7 @@ from unittest.mock import MagicMock
7
8 import pytest
9
10 -from tests.entrypoints.openai.tool_parsers.utils import (
10 +from tests.tool_parsers.utils import (
11 run_tool_extraction,
12 run_tool_extraction_streaming,
13 )
tests/entrypoints/openai/tool_parsers/test_llama4_pythonic_tool_parser.py
+1 -1
@@ -5,7 +5,7 @@ from unittest.mock import MagicMock, patch
5
6 import pytest
7
8 -from tests.entrypoints.openai.tool_parsers.utils import (
8 +from tests.tool_parsers.utils import (
9 run_tool_extraction,
10 run_tool_extraction_streaming,
11 )
tests/entrypoints/openai/tool_parsers/test_olmo3_tool_parser.py
+1 -1
@@ -5,7 +5,7 @@ from unittest.mock import MagicMock, patch
5
6 import pytest
7
8 -from tests.entrypoints.openai.tool_parsers.utils import (
8 +from tests.tool_parsers.utils import (
9 run_tool_extraction,
10 run_tool_extraction_streaming,
11 )
tests/entrypoints/openai/tool_parsers/test_pythonic_tool_parser.py
+1 -1
@@ -5,7 +5,7 @@ from unittest.mock import MagicMock, patch
5
6 import pytest
7
8 -from tests.entrypoints.openai.tool_parsers.utils import (
8 +from tests.tool_parsers.utils import (
9 run_tool_extraction,
10 run_tool_extraction_streaming,
11 )
tests/tool_parsers/common_tests.py new
+378
@@ -0,0 +1,378 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +
4 +import json
5 +from dataclasses import dataclass, field
6 +from types import NoneType
7 +from typing import Any
8 +
9 +import pytest
10 +
11 +from tests.tool_parsers.utils import run_tool_extraction
12 +from vllm.tokenizers import TokenizerLike
13 +from vllm.tool_parsers import ToolParserManager
14 +
15 +
16 +@dataclass
17 +class ToolParserTestConfig:
18 + """Configuration for a tool parser's common tests.
19 +
20 + This dataclass contains all the test data and expected results needed
21 + to run the common test suite for a parser. Each parser test file
22 + creates one instance of this config with parser-specific values.
23 +
24 + Attributes:
25 + parser_name: Name used with ToolParserManager (e.g., "mistral")
26 +
27 + Test data (model outputs):
28 + no_tool_calls_output: Plain text without any tool syntax
29 + single_tool_call_output: One tool call with simple arguments
30 + parallel_tool_calls_output: Multiple tool calls in one response
31 + various_data_types_output: Tool with various data types
32 + empty_arguments_output: Tool call with no parameters
33 + surrounding_text_output: Tool call mixed with regular text
34 + escaped_strings_output: Tool call with escaped chars
35 + malformed_input_outputs: List of invalid inputs
36 +
37 + Expected results:
38 + single_tool_call_expected_name: Expected function name
39 + single_tool_call_expected_args: Expected arguments dict
40 + parallel_tool_calls_count: Number of tools in parallel test
41 + parallel_tool_calls_names: Function names in order
42 + single_tool_call_expected_content: Content field when tool called
43 + parallel_tool_calls_expected_content: Content for parallel test
44 +
45 + xfail markers:
46 + xfail_streaming: Mapping test name to xfail reason (streaming only)
47 + xfail_nonstreaming: Mapping test name to xfail reason (non-streaming)
48 +
49 + Special flags:
50 + allow_empty_or_json_empty_args: True if "" or "{}" both valid for empty args
51 + supports_typed_arguments: True if the parser supports typed function arguments
52 + """
53 +
54 + # Parser identification
55 + parser_name: str
56 +
57 + # Test data - model outputs for each common test
58 + no_tool_calls_output: str
59 + single_tool_call_output: str
60 + parallel_tool_calls_output: str
61 + various_data_types_output: str
62 + empty_arguments_output: str
63 + surrounding_text_output: str
64 + escaped_strings_output: str
65 + malformed_input_outputs: list[str]
66 +
67 + # Expected results for specific tests (optional overrides)
68 + single_tool_call_expected_name: str = "get_weather"
69 + single_tool_call_expected_args: dict[str, Any] = field(
70 + default_factory=lambda: {"city": "Tokyo"}
71 + )
72 + parallel_tool_calls_count: int = 2
73 + parallel_tool_calls_names: list[str] = field(
74 + default_factory=lambda: ["get_weather", "get_time"]
75 + )
76 +
77 + # xfail configuration - maps test name to xfail reason
78 + xfail_streaming: dict[str, str] = field(default_factory=dict)
79 + xfail_nonstreaming: dict[str, str] = field(default_factory=dict)
80 +
81 + # Content expectations (some parsers strip content, others don't)
82 + single_tool_call_expected_content: str | None = None
83 + parallel_tool_calls_expected_content: str | None = None
84 +
85 + # Special assertions for edge cases
86 + allow_empty_or_json_empty_args: bool = True # "{}" or "" for empty args
87 + supports_typed_arguments: bool = True
88 +
89 +
90 +class ToolParserTests:
91 + """Mixin class providing common test suite for tool parsers.
92 +
93 + To use this mixin in a parser test file:
94 +
95 + 1. Create a test_config fixture that returns a ToolParserTestConfig instance
96 + 2. Inherit from this class
97 + 3. Add parser-specific tests as additional methods
98 +
99 + Example:
100 + class TestMistralToolParser(ToolParserTests):
101 + @pytest.fixture
102 + def test_config(self) -> ToolParserTestConfig:
103 + return ToolParserTestConfig(
104 + parser_name="mistral",
105 + no_tool_calls_output="Plain text...",
106 + # ... other config ...
107 + )
108 +
109 + # Parser-specific tests
110 + def test_mistral_specific_feature(self, tool_parser):
111 + # Custom test logic
112 + pass
113 + """
114 +
115 + @pytest.fixture
116 + def test_config(self) -> ToolParserTestConfig:
117 + """Override this to provide parser-specific configuration."""
118 + raise NotImplementedError(
119 + "Subclass must provide test_config fixture returning ToolParserTestConfig"
120 + )
121 +
122 + @pytest.fixture
123 + def tokenizer(self, default_tokenizer: TokenizerLike) -> TokenizerLike:
124 + """Override this to provide parser-specific tokenizer."""
125 + return default_tokenizer
126 +
127 + @pytest.fixture
128 + def tool_parser(self, test_config: ToolParserTestConfig, tokenizer: TokenizerLike):
129 + return ToolParserManager.get_tool_parser(test_config.parser_name)(tokenizer)
130 +
131 + @pytest.fixture(params=[True, False])
132 + def streaming(self, request: pytest.FixtureRequest) -> bool:
133 + return request.param
134 +
135 + def test_no_tool_calls(
136 + self,
137 + request: pytest.FixtureRequest,
138 + tool_parser: Any,
139 + test_config: ToolParserTestConfig,
140 + streaming: bool,
141 + ):
142 + """Verify parser handles plain text without tool syntax."""
143 + # Apply xfail markers if configured
144 + test_name = "test_no_tool_calls"
145 + self.apply_xfail_mark(request, test_config, test_name, streaming)
146 +
147 + content, tool_calls = run_tool_extraction(
148 + tool_parser, test_config.no_tool_calls_output, streaming=streaming
149 + )
150 + assert content == test_config.no_tool_calls_output, (
151 + f"Expected content to match input, got {content}"
152 + )
153 + assert len(tool_calls) == 0, f"Expected no tool calls, got {len(tool_calls)}"
154 +
155 + def test_single_tool_call_simple_args(
156 + self,
157 + request: pytest.FixtureRequest,
158 + tool_parser: Any,
159 + test_config: ToolParserTestConfig,
160 + streaming: bool,
161 + ):
162 + """Verify parser extracts one tool with simple arguments."""
163 + # Apply xfail markers if configured
164 + test_name = "test_single_tool_call_simple_args"
165 + self.apply_xfail_mark(request, test_config, test_name, streaming)
166 +
167 + content, tool_calls = run_tool_extraction(
168 + tool_parser, test_config.single_tool_call_output, streaming=streaming
169 + )
170 +
171 + # Content check (some parsers strip it)
172 + if test_config.single_tool_call_expected_content is not None:
173 + assert content == test_config.single_tool_call_expected_content
174 +
175 + assert len(tool_calls) == 1, f"Expected 1 tool call, got {len(tool_calls)}"
176 + assert tool_calls[0].type == "function"
177 + assert tool_calls[0].function.name == test_config.single_tool_call_expected_name
178 +
179 + args = json.loads(tool_calls[0].function.arguments)
180 + for key, value in test_config.single_tool_call_expected_args.items():
181 + assert args.get(key) == value, (
182 + f"Expected {key}={value}, got {args.get(key)}"
183 + )
184 +
185 + def test_parallel_tool_calls(
186 + self,
187 + request: pytest.FixtureRequest,
188 + tool_parser: Any,
189 + test_config: ToolParserTestConfig,
190 + streaming: bool,
191 + ):
192 + """Verify parser handles multiple tools in one response."""
193 + # Apply xfail markers if configured
194 + test_name = "test_parallel_tool_calls"
195 + self.apply_xfail_mark(request, test_config, test_name, streaming)
196 +
197 + content, tool_calls = run_tool_extraction(
198 + tool_parser,
199 + test_config.parallel_tool_calls_output,
200 + streaming=streaming,
201 + )
202 +
203 + assert len(tool_calls) == test_config.parallel_tool_calls_count, (
204 + f"Expected {test_config.parallel_tool_calls_count} "
205 + f"tool calls, got {len(tool_calls)}"
206 + )
207 +
208 + # Verify tool names match expected
209 + for i, expected_name in enumerate(test_config.parallel_tool_calls_names):
210 + assert tool_calls[i].type == "function"
211 + assert tool_calls[i].function.name == expected_name
212 +
213 + # Verify unique IDs
214 + ids = [tc.id for tc in tool_calls]
215 + assert len(ids) == len(set(ids)), "Tool call IDs should be unique"
216 +
217 + def test_various_data_types(
218 + self,
219 + request: pytest.FixtureRequest,
220 + tool_parser: Any,
221 + test_config: ToolParserTestConfig,
222 + streaming: bool,
223 + ):
224 + """Verify parser handles all JSON types in arguments."""
225 + # Apply xfail markers if configured
226 + test_name = "test_various_data_types"
227 + self.apply_xfail_mark(request, test_config, test_name, streaming)
228 +
229 + content, tool_calls = run_tool_extraction(
230 + tool_parser,
231 + test_config.various_data_types_output,
232 + streaming=streaming,
233 + )
234 + assert len(tool_calls) == 1, f"Expected 1 tool call, got {len(tool_calls)}"
235 +
236 + args = json.loads(tool_calls[0].function.arguments)
237 + # Verify all expected fields present
238 + required_fields_types = {
239 + "string_field": str,
240 + "int_field": int,
241 + "float_field": float,
242 + "bool_field": bool,
243 + "null_field": NoneType,
244 + "array_field": list,
245 + "object_field": dict,
246 + }
247 + for required_field, expected_type in required_fields_types.items():
248 + assert required_field in args, (
249 + f"Expected field '{required_field}' in arguments"
250 + )
251 + if test_config.supports_typed_arguments:
252 + found_type = type(args[required_field])
253 + assert found_type is expected_type, (
254 + f"Expected field '{required_field}' to have type {expected_type}, "
255 + f"got {found_type}"
256 + )
257 +
258 + def test_empty_arguments(
259 + self,
260 + request: pytest.FixtureRequest,
261 + tool_parser: Any,
262 + test_config: ToolParserTestConfig,
263 + streaming: bool,
264 + ):
265 + """Verify parser handles parameterless tool calls."""
266 + # Apply xfail markers if configured
267 + test_name = "test_empty_arguments"
268 + self.apply_xfail_mark(request, test_config, test_name, streaming)
269 +
270 + content, tool_calls = run_tool_extraction(
271 + tool_parser, test_config.empty_arguments_output, streaming=streaming
272 + )
273 + assert len(tool_calls) == 1, f"Expected 1 tool call, got {len(tool_calls)}"
274 +
275 + args = tool_calls[0].function.arguments
276 + if test_config.allow_empty_or_json_empty_args:
277 + assert args in ["{}", ""], f"Expected empty args, got {args}"
278 + else:
279 + assert args == "{}", f"Expected {{}}, got {args}"
280 +
281 + def test_surrounding_text(
282 + self,
283 + request: pytest.FixtureRequest,
284 + tool_parser: Any,
285 + test_config: ToolParserTestConfig,
286 + streaming: bool,
287 + ):
288 + """Verify parser extracts tools from mixed content."""
289 + # Apply xfail markers if configured
290 + test_name = "test_surrounding_text"
291 + self.apply_xfail_mark(request, test_config, test_name, streaming)
292 +
293 + content, tool_calls = run_tool_extraction(
294 + tool_parser, test_config.surrounding_text_output, streaming=streaming
295 + )
296 + assert len(tool_calls) >= 1, (
297 + f"Expected at least 1 tool call, got {len(tool_calls)}"
298 + )
299 +
300 + def test_escaped_strings(
301 + self,
302 + request: pytest.FixtureRequest,
303 + tool_parser: Any,
304 + test_config: ToolParserTestConfig,
305 + streaming: bool,
306 + ):
307 + """Verify parser handles escaped characters in arguments."""
308 + # Apply xfail markers if configured
309 + test_name = "test_escaped_strings"
310 + self.apply_xfail_mark(request, test_config, test_name, streaming)
311 +
312 + content, tool_calls = run_tool_extraction(
313 + tool_parser, test_config.escaped_strings_output, streaming=streaming
314 + )
315 + assert len(tool_calls) == 1, f"Expected 1 tool call, got {len(tool_calls)}"
316 +
317 + args = json.loads(tool_calls[0].function.arguments)
318 + # At minimum, verify we can parse and have expected fields
319 + # Exact escaping behavior varies by parser
320 + assert len(args) > 0, "Expected some arguments with escaped strings"
321 +
322 + def test_malformed_input(
323 + self,
324 + request: pytest.FixtureRequest,
325 + tool_parser: Any,
326 + test_config: ToolParserTestConfig,
327 + streaming: bool,
328 + ):
329 + """Verify parser gracefully handles invalid syntax."""
330 + # Apply xfail markers if configured
331 + test_name = "test_malformed_input"
332 + self.apply_xfail_mark(request, test_config, test_name, streaming)
333 +
334 + for malformed_input in test_config.malformed_input_outputs:
335 + # Should not raise exception
336 + content, tool_calls = run_tool_extraction(
337 + tool_parser, malformed_input, streaming=streaming
338 + )
339 + # Parser should handle gracefully (exact behavior varies)
340 +
341 + def test_streaming_reconstruction(
342 + self,
343 + request: pytest.FixtureRequest,
344 + tool_parser: Any,
345 + test_config: ToolParserTestConfig,
346 + ):
347 + """Verify streaming produces same result as non-streaming."""
348 + test_name = "test_streaming_reconstruction"
349 + self.apply_xfail_mark(request, test_config, test_name, True)
350 +
351 + test_output = test_config.single_tool_call_output
352 +
353 + # Non-streaming result
354 + content_non, tools_non = run_tool_extraction(
355 + tool_parser, test_output, streaming=False
356 + )
357 +
358 + # Streaming result
359 + content_stream, tools_stream = run_tool_extraction(
360 + tool_parser, test_output, streaming=True
361 + )
362 +
363 + # Compare results
364 + assert content_non == content_stream, "Content should match between modes"
365 + assert len(tools_non) == len(tools_stream), "Tool count should match"
366 + if len(tools_non) > 0:
367 + assert tools_non[0].function.name == tools_stream[0].function.name
368 + assert tools_non[0].function.arguments == tools_stream[0].function.arguments
369 +
370 + def apply_xfail_mark(self, request, test_config, test_name, streaming):
371 + reason = None
372 + if streaming and test_name in test_config.xfail_streaming:
373 + reason = test_config.xfail_streaming[test_name]
374 + elif not streaming and test_name in test_config.xfail_nonstreaming:
375 + reason = test_config.xfail_nonstreaming[test_name]
376 + if reason is not None:
377 + mark = pytest.mark.xfail(reason=reason, strict=True)
378 + request.node.add_marker(mark)
tests/tool_parsers/conftest.py new
+12
@@ -0,0 +1,12 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +
4 +import pytest
5 +from transformers import AutoTokenizer
6 +
7 +from vllm.tokenizers import TokenizerLike
8 +
9 +
10 +@pytest.fixture(scope="module")
11 +def default_tokenizer() -> TokenizerLike:
12 + return AutoTokenizer.from_pretrained("gpt2")
tests/tool_parsers/test_deepseekv3_tool_parser.py new
+92
@@ -0,0 +1,92 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +
4 +
5 +import pytest
6 +
7 +from tests.tool_parsers.common_tests import (
8 + ToolParserTestConfig,
9 + ToolParserTests,
10 +)
11 +from vllm.tokenizers import TokenizerLike, get_tokenizer
12 +
13 +
14 +class TestDeepSeekV3ToolParser(ToolParserTests):
15 + @pytest.fixture(scope="class")
16 + def tokenizer(self) -> TokenizerLike:
17 + return get_tokenizer("deepseek-ai/DeepSeek-V3")
18 +
19 + @pytest.fixture
20 + def test_config(self) -> ToolParserTestConfig:
21 + return ToolParserTestConfig(
22 + parser_name="deepseek_v3",
23 + # Test data
24 + no_tool_calls_output=(
25 + "How can I help you today? I can check weather for you."
26 + ),
27 + single_tool_call_output="""<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>get_weather
28 +```json
29 +{"city": "Tokyo", "unit": "celsius"}
30 +```<|tool▁call▁end|><|tool▁calls▁end|>""",
31 + parallel_tool_calls_output="""<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>get_weather
32 +```json
33 +{"city": "Tokyo", "unit": "celsius"}
34 +```<|tool▁call▁end|><|tool▁call▁begin|>function<|tool▁sep|>search_hotels
35 +```json
36 +{"location": "Tokyo", "check_in": "2025-01-15"}
37 +```<|tool▁call▁end|><|tool▁calls▁end|>""",
38 + various_data_types_output=(
39 + """<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>test_function
40 +```json
41 +"""
42 + """{"string_field": "hello", "int_field": 42, "float_field": 3.14, """
43 + """"bool_field": true, "null_field": null, """
44 + """"array_field": ["a", "b", "c"], """
45 + """"object_field": {"nested": "value"}, """
46 + """"empty_array": [], "empty_object": {}}
47 +```<|tool▁call▁end|><|tool▁calls▁end|>"""
48 + ),
49 + empty_arguments_output="""<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>get_current_time
50 +```json
51 +{}
52 +```<|tool▁call▁end|><|tool▁calls▁end|>""",
53 + surrounding_text_output=(
54 + """Let me check the weather for you."""
55 + """<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>get_weather
56 +```json
57 +{"city": "Paris"}
58 +```<|tool▁call▁end|><|tool▁calls▁end|>"""
59 + ),
60 + escaped_strings_output=(
61 + """<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>send_message
62 +```json
63 +"""
64 + """{"text": "He said \\"hello\\"", "path": "C:\\\\Users\\\\file", """
65 + """"newline": "line1\\nline2"}
66 +```<|tool▁call▁end|><|tool▁calls▁end|>"""
67 + ),
68 + malformed_input_outputs=[
69 + """<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>get_weather
70 +```json
71 +{"city": "Tokyo"
72 +```<|tool▁call▁end|><|tool▁calls▁end|>""",
73 + """<|tool▁calls▁begin|>function<|tool▁sep|>get_weather
74 +```json
75 +{"city": "Tokyo"}
76 +```<|tool▁calls▁end|>""",
77 + ],
78 + # Expected results
79 + single_tool_call_expected_name="get_weather",
80 + single_tool_call_expected_args={"city": "Tokyo", "unit": "celsius"},
81 + single_tool_call_expected_content=None,
82 + parallel_tool_calls_count=2,
83 + parallel_tool_calls_names=["get_weather", "search_hotels"],
84 + # xfail markers
85 + xfail_streaming={},
86 + xfail_nonstreaming={
87 + "test_malformed_input": (
88 + "Parser sets tools_called=True even when tool_calls is "
89 + "empty (detects start token but fails to parse)"
90 + ),
91 + },
92 + )
tests/tool_parsers/test_granite_20b_fc_tool_parser.py new
+76
@@ -0,0 +1,76 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +
4 +import pytest
5 +
6 +from tests.tool_parsers.common_tests import (
7 + ToolParserTestConfig,
8 + ToolParserTests,
9 +)
10 +
11 +
12 +class TestGranite20bFcToolParser(ToolParserTests):
13 + @pytest.fixture
14 + def test_config(self) -> ToolParserTestConfig:
15 + return ToolParserTestConfig(
16 + parser_name="granite-20b-fc",
17 + # Test data
18 + no_tool_calls_output="This is a regular response without any tool calls.",
19 + single_tool_call_output=(
20 + '<function_call> {"name": "get_weather", '
21 + '"arguments": {"city": "Tokyo"}}'
22 + ),
23 + parallel_tool_calls_output=(
24 + '<function_call> {"name": "get_weather", '
25 + '"arguments": {"city": "Tokyo"}}\n'
26 + '<function_call> {"name": "get_time", '
27 + '"arguments": {"timezone": "Asia/Tokyo"}}'
28 + ),
29 + various_data_types_output="""<function_call> {
30 + "name": "test_function",
31 + "arguments": {
32 + "string_field": "hello",
33 + "int_field": 42,
34 + "float_field": 3.14,
35 + "bool_field": true,
36 + "null_field": null,
37 + "array_field": ["a", "b", "c"],
38 + "object_field": {"nested": "value"},
39 + "empty_array": [],
40 + "empty_object": {}
41 + }
42 +}""",
43 + empty_arguments_output=(
44 + '<function_call> {"name": "refresh", "arguments": {}}'
45 + ),
46 + surrounding_text_output="""Let me check the weather for you.
47 +<function_call> {"name": "get_weather", "arguments": {"city": "Tokyo"}}""",
48 + escaped_strings_output="""<function_call> {
49 + "name": "test_function",
50 + "arguments": {
51 + "quoted": "He said \\"hello\\"",
52 + "path": "C:\\\\Users\\\\file.txt",
53 + "newline": "line1\\nline2",
54 + "unicode": "emoji: 🎉"
55 + }
56 +}""",
57 + malformed_input_outputs=[
58 + '<function_call> {"name": "func", "arguments": {',
59 + '<function_call> [{"name": "func", "arguments": {}}]',
60 + '{"name": "func", "arguments": {}}',
61 + '<function_call> {"name": 123}',
62 + ],
63 + # Expected results
64 + single_tool_call_expected_name="get_weather",
65 + single_tool_call_expected_args={"city": "Tokyo"},
66 + single_tool_call_expected_content=None,
67 + parallel_tool_calls_count=2,
68 + parallel_tool_calls_names=["get_weather", "get_time"],
69 + # xfail markers
70 + xfail_streaming={
71 + "test_surrounding_text": (
72 + "Granite 20B FC streaming requires <function_call> at start"
73 + ),
74 + },
75 + xfail_nonstreaming={},
76 + )
tests/tool_parsers/test_granite_tool_parser.py new
+118
@@ -0,0 +1,118 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +
4 +
5 +import pytest
6 +
7 +from tests.tool_parsers.common_tests import (
8 + ToolParserTestConfig,
9 + ToolParserTests,
10 +)
11 +from tests.tool_parsers.utils import run_tool_extraction
12 +
13 +
14 +class TestGraniteToolParser(ToolParserTests):
15 + @pytest.fixture
16 + def test_config(self) -> ToolParserTestConfig:
17 + return ToolParserTestConfig(
18 + parser_name="granite",
19 + # Test data
20 + no_tool_calls_output="This is a regular response without any tool calls.",
21 + single_tool_call_output=(
22 + '<|tool_call|> [{"name": "get_weather", '
23 + '"arguments": {"city": "Tokyo"}}]'
24 + ),
25 + parallel_tool_calls_output="""<|tool_call|> [
26 + {"name": "get_weather", "arguments": {"city": "Tokyo"}},
27 + {"name": "get_time", "arguments": {"timezone": "Asia/Tokyo"}}
28 +]""",
29 + various_data_types_output="""<tool_call> [{
30 + "name": "test_function",
31 + "arguments": {
32 + "string_field": "hello",
33 + "int_field": 42,
34 + "float_field": 3.14,
35 + "bool_field": true,
36 + "null_field": null,
37 + "array_field": ["a", "b", "c"],
38 + "object_field": {"nested": "value"},
39 + "empty_array": [],
40 + "empty_object": {}
41 + }
42 +}]""",
43 + empty_arguments_output=(
44 + '<|tool_call|> [{"name": "refresh", "arguments": {}}]'
45 + ),
46 + surrounding_text_output="""Let me check the weather for you.
47 +<|tool_call|> [{"name": "get_weather", "arguments": {"city": "Tokyo"}}]
48 +I'll get that information.""",
49 + escaped_strings_output="""<tool_call> [{
50 + "name": "test_function",
51 + "arguments": {
52 + "quoted": "He said \\"hello\\"",
53 + "path": "C:\\\\Users\\\\file.txt",
54 + "newline": "line1\\nline2",
55 + "unicode": "emoji: 🎉"
56 + }
57 +}]""",
58 + malformed_input_outputs=[
59 + '<|tool_call|> [{"name": "func", "arguments": {',
60 + '<|tool_call|> {"name": "func", "arguments": {}}', # Not an array
61 + '[{"name": "func", "arguments": "not a dict"}]',
62 + 'Some text [{"name": "func"}]', # JSON but not tool call format
63 + ],
64 + # Expected results
65 + single_tool_call_expected_name="get_weather",
66 + single_tool_call_expected_args={"city": "Tokyo"},
67 + # Granite strips content when tool calls present
68 + single_tool_call_expected_content=None,
69 + parallel_tool_calls_count=2,
70 + parallel_tool_calls_names=["get_weather", "get_time"],
71 + # xfail markers
72 + xfail_streaming={
73 + "test_malformed_input": (
74 + "Streaming mode incorrectly creates tool call from malformed JSON"
75 + ),
76 + "test_surrounding_text": (
77 + "Parser doesn't handle surrounding text correctly in streaming"
78 + ),
79 + "test_streaming_reconstruction": (
80 + "Streaming mode doesn't strip <|tool_call|> marker from content"
81 + ),
82 + },
83 + xfail_nonstreaming={
84 + "test_surrounding_text": (
85 + "Parser doesn't handle surrounding text correctly in non-streaming"
86 + ),
87 + },
88 + )
89 +
90 + # Granite-Specific Tests
91 +
92 + @pytest.mark.parametrize("streaming", [True, False])
93 + def test_granite_token_prefix_format(self, tool_parser, streaming):
94 + """Verify parser handles Granite 3.0 <|tool_call|> token format."""
95 + single_tool_call_token = (
96 + '<|tool_call|> [{"name": "get_weather", "arguments": {"city": "Tokyo"}}]'
97 + )
98 + content, tool_calls = run_tool_extraction(
99 + tool_parser, single_tool_call_token, streaming=streaming
100 + )
101 + assert len(tool_calls) == 1, (
102 + f"Expected 1 tool call from token format, got {len(tool_calls)}"
103 + )
104 + assert tool_calls[0].function.name == "get_weather"
105 +
106 + @pytest.mark.parametrize("streaming", [True, False])
107 + def test_granite_string_prefix_format(self, tool_parser, streaming):
108 + """Verify parser handles Granite 3.1 <tool_call> string format."""
109 + single_tool_call_string = (
110 + '<tool_call> [{"name": "get_weather", "arguments": {"city": "Tokyo"}}]'
111 + )
112 + content, tool_calls = run_tool_extraction(
113 + tool_parser, single_tool_call_string, streaming=streaming
114 + )
115 + assert len(tool_calls) == 1, (
116 + f"Expected 1 tool call from string format, got {len(tool_calls)}"
117 + )
118 + assert tool_calls[0].function.name == "get_weather"
tests/tool_parsers/test_internlm2_tool_parser.py new
+122
@@ -0,0 +1,122 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +
4 +from unittest.mock import MagicMock
5 +
6 +import pytest
7 +
8 +from tests.tool_parsers.common_tests import (
9 + ToolParserTestConfig,
10 + ToolParserTests,
11 +)
12 +from vllm.tokenizers import TokenizerLike
13 +
14 +
15 +class TestInternLM2ToolParser(ToolParserTests):
16 + @pytest.fixture
17 + def tokenizer(self, default_tokenizer: TokenizerLike) -> TokenizerLike:
18 + """Add some internlm2 specific tokens to the default vocab."""
19 +
20 + tokenizer_vocab = default_tokenizer.get_vocab()
21 + default_tokenizer.get_vocab = MagicMock()
22 + tokenizer_vocab.update(
23 + {
24 + "<|action_start|>": 92540,
25 + "<|plugin|>": 92541,
26 + "<|action_end|>": 92542,
27 + }
28 + )
29 + default_tokenizer.get_vocab.return_value = tokenizer_vocab
30 + return default_tokenizer
31 +
32 + @pytest.fixture
33 + def test_config(self) -> ToolParserTestConfig:
34 + return ToolParserTestConfig(
35 + parser_name="internlm",
36 + # Test data
37 + no_tool_calls_output="This is a regular response without any tool calls.",
38 + single_tool_call_output=(
39 + '<|action_start|><|plugin|>{"name": "get_weather", '
40 + '"parameters": {"city": "Tokyo"}}<|action_end|>'
41 + ),
42 + # InternLM2 doesn't support parallel calls
43 + parallel_tool_calls_output=(
44 + '<|action_start|><|plugin|>{"name": "get_weather", '
45 + '"parameters": {"city": "Tokyo"}}<|action_end|>'
46 + ),
47 + various_data_types_output="""<|action_start|><|plugin|>{
48 + "name": "test_function",
49 + "parameters": {
50 + "string_field": "hello",
51 + "int_field": 42,
52 + "float_field": 3.14,
53 + "bool_field": true,
54 + "null_field": null,
55 + "array_field": ["a", "b", "c"],
56 + "object_field": {"nested": "value"},
57 + "empty_array": [],
58 + "empty_object": {}
59 + }
60 +}<|action_end|>""",
61 + empty_arguments_output=(
62 + '<|action_start|><|plugin|>{"name": "refresh", '
63 + '"parameters": {}}<|action_end|>'
64 + ),
65 + surrounding_text_output=(
66 + "Let me check the weather for you. "
67 + '<|action_start|><|plugin|>{"name": "get_weather", '
68 + '"parameters": {"city": "Tokyo"}}<|action_end|>'
69 + ),
70 + escaped_strings_output="""<|action_start|><|plugin|>{
71 + "name": "test_function",
72 + "parameters": {
73 + "quoted": "He said \\"hello\\"",
74 + "path": "C:\\\\Users\\\\file.txt",
75 + "newline": "line1\\nline2",
76 + "unicode": "emoji: 🎉"
77 + }
78 +}<|action_end|>""",
79 + malformed_input_outputs=[
80 + '<|action_start|><|plugin|>{"name": "func", "parameters": {',
81 + (
82 + '<|action_start|><|plugin|>{"name": "func", '
83 + '"parameters": "not a dict"}<|action_end|>'
84 + ),
85 + "<|action_start|><|plugin|>not json<|action_end|>",
86 + "<|action_start|><|plugin|>",
87 + '<|action_start|>{"name": "func"}',
88 + ],
89 + # Expected results
90 + single_tool_call_expected_name="get_weather",
91 + single_tool_call_expected_args={"city": "Tokyo"},
92 + single_tool_call_expected_content=None,
93 + parallel_tool_calls_count=1, # InternLM2 only supports single tool calls
94 + parallel_tool_calls_names=["get_weather"],
95 + # Parser-specific settings
96 + allow_empty_or_json_empty_args=True,
97 + # xfail markers
98 + xfail_streaming={
99 + "test_single_tool_call_simple_args": (
100 + "InternLM2 streaming not fully implemented"
101 + ),
102 + "test_parallel_tool_calls": (
103 + "InternLM2 streaming not fully implemented"
104 + ),
105 + "test_various_data_types": (
106 + "InternLM2 streaming not fully implemented"
107 + ),
108 + "test_empty_arguments": ("InternLM2 streaming not fully implemented"),
109 + "test_surrounding_text": ("InternLM2 streaming not fully implemented"),
110 + "test_escaped_strings": ("InternLM2 streaming not fully implemented"),
111 + "test_streaming_reconstruction": (
112 + "InternLM2 streaming parser returns '<|action_start|' as "
113 + "content instead of None - streaming/non-streaming inconsistency"
114 + ),
115 + },
116 + xfail_nonstreaming={
117 + "test_malformed_input": (
118 + "InternLM2 parser raises JSONDecodeError on malformed JSON "
119 + "instead of gracefully handling it"
120 + ),
121 + },
122 + )
tests/tool_parsers/test_longcat_tool_parser.py new
+101
@@ -0,0 +1,101 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +
4 +from unittest.mock import MagicMock
5 +
6 +import pytest
7 +
8 +from tests.tool_parsers.common_tests import (
9 + ToolParserTestConfig,
10 + ToolParserTests,
11 +)
12 +from vllm.tokenizers import TokenizerLike
13 +
14 +
15 +class TestLongCatToolParser(ToolParserTests):
16 + @pytest.fixture
17 + def tokenizer(self, default_tokenizer: TokenizerLike) -> TokenizerLike:
18 + """Add some longcat specific tokens to the default vocab."""
19 + tokenizer = default_tokenizer
20 + tokenizer_vocab = tokenizer.get_vocab()
21 + tokenizer.get_vocab = MagicMock()
22 + tokenizer_vocab.update(
23 + {
24 + "<longcat_tool_call>": 32000,
25 + "</longcat_tool_call>": 32001,
26 + }
27 + )
28 + tokenizer.get_vocab.return_value = tokenizer_vocab
29 + return tokenizer
30 +
31 + @pytest.fixture
32 + def test_config(self) -> ToolParserTestConfig:
33 + return ToolParserTestConfig(
34 + parser_name="longcat",
35 + # Test data
36 + no_tool_calls_output="This is a regular response without any tool calls.",
37 + single_tool_call_output=(
38 + '<longcat_tool_call>{"name": "get_weather", '
39 + '"arguments": {"city": "Tokyo"}}</longcat_tool_call>'
40 + ),
41 + parallel_tool_calls_output=(
42 + '<longcat_tool_call>{"name": "get_weather", '
43 + '"arguments": {"city": "Tokyo"}}</longcat_tool_call>\n'
44 + '<longcat_tool_call>{"name": "get_time", '
45 + '"arguments": {"timezone": "Asia/Tokyo"}}</longcat_tool_call>'
46 + ),
47 + various_data_types_output="""<longcat_tool_call>{
48 + "name": "test_function",
49 + "arguments": {
50 + "string_field": "hello",
51 + "int_field": 42,
52 + "float_field": 3.14,
53 + "bool_field": true,
54 + "null_field": null,
55 + "array_field": ["a", "b", "c"],
56 + "object_field": {"nested": "value"},
57 + "empty_array": [],
58 + "empty_object": {}
59 + }
60 +}</longcat_tool_call>""",
61 + empty_arguments_output=(
62 + '<longcat_tool_call>{"name": "refresh", "arguments": {}}'
63 + "</longcat_tool_call>"
64 + ),
65 + surrounding_text_output=(
66 + "Let me check the weather for you.\n"
67 + '<longcat_tool_call>{"name": "get_weather", '
68 + '"arguments": {"city": "Tokyo"}}</longcat_tool_call>\n'
69 + "Here is the result."
70 + ),
71 + escaped_strings_output="""<longcat_tool_call>{
72 + "name": "test_function",
73 + "arguments": {
74 + "quoted": "He said \\"hello\\"",
75 + "path": "C:\\\\Users\\\\file.txt",
76 + "newline": "line1\\nline2",
77 + "unicode": "emoji: 🎉"
78 + }
79 +}</longcat_tool_call>""",
80 + malformed_input_outputs=[
81 + '<longcat_tool_call>{"name": "func", "arguments": {',
82 + (
83 + '<longcat_tool_call>{"name": "func", '
84 + '"arguments": "not a dict"}</longcat_tool_call>'
85 + ),
86 + "Some text with <longcat_tool_call>invalid json",
87 + ],
88 + # Expected results
89 + single_tool_call_expected_name="get_weather",
90 + single_tool_call_expected_args={"city": "Tokyo"},
91 + single_tool_call_expected_content=None,
92 + parallel_tool_calls_count=2,
93 + parallel_tool_calls_names=["get_weather", "get_time"],
94 + # xfail markers
95 + xfail_streaming={
96 + "test_malformed_input": "Streaming has complex buffering behavior",
97 + },
98 + xfail_nonstreaming={},
99 + # Configuration
100 + allow_empty_or_json_empty_args=True,
101 + )
tests/tool_parsers/test_phi4mini_tool_parser.py new
+110
@@ -0,0 +1,110 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +
4 +from unittest.mock import MagicMock
5 +
6 +import pytest
7 +
8 +from tests.tool_parsers.common_tests import (
9 + ToolParserTestConfig,
10 + ToolParserTests,
11 +)
12 +from vllm.tokenizers import TokenizerLike
13 +
14 +
15 +class TestPhi4MiniToolParser(ToolParserTests):
16 + @pytest.fixture
17 + def tokenizer(self, default_tokenizer: TokenizerLike) -> TokenizerLike:
18 + """Add some phi4mini specific tokens to the default vocab."""
19 +
20 + tokenizer = default_tokenizer
21 + tokenizer_vocab = tokenizer.get_vocab()
22 + tokenizer.get_vocab = MagicMock()
23 + tokenizer_vocab.update(
24 + {
25 + "functools": 32000,
26 + }
27 + )
28 + tokenizer.get_vocab.return_value = tokenizer_vocab
29 + return tokenizer
30 +
31 + @pytest.fixture
32 + def test_config(self) -> ToolParserTestConfig:
33 + return ToolParserTestConfig(
34 + parser_name="phi4_mini_json",
35 + # Test data
36 + no_tool_calls_output="This is a regular response without any tool calls.",
37 + single_tool_call_output=(
38 + 'functools[{"name": "get_weather", "arguments": {"city": "Tokyo"}}]'
39 + ),
40 + parallel_tool_calls_output="""functools[
41 + {"name": "get_weather", "arguments": {"city": "Tokyo"}},
42 + {"name": "get_time", "arguments": {"timezone": "Asia/Tokyo"}}
43 +]""",
44 + various_data_types_output="""functools[{
45 + "name": "test_function",
46 + "arguments": {
47 + "string_field": "hello",
48 + "int_field": 42,
49 + "float_field": 3.14,
50 + "bool_field": true,
51 + "null_field": null,
52 + "array_field": ["a", "b", "c"],
53 + "object_field": {"nested": "value"},
54 + "empty_array": [],
55 + "empty_object": {}
56 + }
57 +}]""",
58 + empty_arguments_output='functools[{"name": "refresh", "arguments": {}}]',
59 + surrounding_text_output="""Let me check the weather for you.
60 +functools[{"name": "get_weather", "arguments": {"city": "Tokyo"}}]
61 +Would you like to know more?""",
62 + escaped_strings_output="""functools[{
63 + "name": "test_function",
64 + "arguments": {
65 + "quoted": "He said \\"hello\\"",
66 + "path": "C:\\\\Users\\\\file.txt",
67 + "newline": "line1\\nline2",
68 + "unicode": "emoji: 🎉"
69 + }
70 +}]""",
71 + malformed_input_outputs=[
72 + 'functools[{"name": "func", "arguments": {',
73 + 'functools[{"name": "func", "arguments": "not a dict"}]',
74 + 'functools{"name": "func"}', # Missing brackets
75 + 'functools[{"name": "func"}]', # Missing arguments/parameters
76 + "functools[] This is just text", # Empty functools
77 + "functools[ This is just text ]", # functools with invalid JSON
78 + ],
79 + # Expected results
80 + single_tool_call_expected_name="get_weather",
81 + single_tool_call_expected_args={"city": "Tokyo"},
82 + # Phi-4 Mini strips content when tool calls present
83 + single_tool_call_expected_content=None,
84 + parallel_tool_calls_count=2,
85 + parallel_tool_calls_names=["get_weather", "get_time"],
86 + parallel_tool_calls_expected_content=None,
87 + # xfail markers
88 + xfail_streaming={
89 + "test_no_tool_calls": "Phi4 Mini streaming not implemented",
90 + "test_single_tool_call_simple_args": (
91 + "Phi4 Mini streaming not implemented"
92 + ),
93 + "test_parallel_tool_calls": "Phi4 Mini streaming not implemented",
94 + "test_various_data_types": "Phi4 Mini streaming not implemented",
95 + "test_empty_arguments": "Phi4 Mini streaming not implemented",
96 + "test_surrounding_text": "Phi4 Mini streaming not implemented",
97 + "test_escaped_strings": "Phi4 Mini streaming not implemented",
98 + "test_streaming_reconstruction": "Phi4 Mini streaming not implemented",
99 + },
100 + xfail_nonstreaming={
101 + "test_various_data_types": (
102 + "Phi4MiniJsonToolParser regex has nesting limitations "
103 + "with nested objects"
104 + ),
105 + "test_malformed_input": (
106 + "Phi4MiniJsonToolParser incorrectly sets "
107 + "tools_called=True on empty array"
108 + ),
109 + },
110 + )
tests/tool_parsers/test_qwen3xml_tool_parser.py new
+75
@@ -0,0 +1,75 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +
4 +
5 +import pytest
6 +
7 +from tests.tool_parsers.common_tests import (
8 + ToolParserTestConfig,
9 + ToolParserTests,
10 +)
11 +
12 +
13 +class TestQwen3xmlToolParser(ToolParserTests):
14 + @pytest.fixture
15 + def test_config(self) -> ToolParserTestConfig:
16 + return ToolParserTestConfig(
17 + parser_name="qwen3_xml",
18 + # Test data
19 + no_tool_calls_output="This is a regular response without any tool calls.",
20 + single_tool_call_output="<tool_call>\n<function=get_weather>\n<parameter=city>Tokyo</parameter>\n</function>\n</tool_call>",
21 + parallel_tool_calls_output="<tool_call>\n<function=get_weather>\n<parameter=city>Tokyo</parameter>\n</function>\n</tool_call><tool_call>\n<function=get_time>\n<parameter=timezone>Asia/Tokyo</parameter>\n</function>\n</tool_call>",
22 + various_data_types_output=(
23 + "<tool_call>\n<function=test_function>\n"
24 + "<parameter=string_field>hello</parameter>\n"
25 + "<parameter=int_field>42</parameter>\n"
26 + "<parameter=float_field>3.14</parameter>\n"
27 + "<parameter=bool_field>true</parameter>\n"
28 + "<parameter=null_field>null</parameter>\n"
29 + '<parameter=array_field>["a", "b", "c"]</parameter>\n'
30 + '<parameter=object_field>{"nested": "value"}</parameter>\n'
31 + "</function>\n</tool_call>"
32 + ),
33 + empty_arguments_output="<tool_call>\n<function=refresh>\n</function>\n</tool_call>",
34 + surrounding_text_output=(
35 + "Let me check the weather for you.\n\n"
36 + "<tool_call>\n<function=get_weather>\n"
37 + "<parameter=city>Tokyo</parameter>\n"
38 + "</function>\n</tool_call>\n\n"
39 + "I will get that information."
40 + ),
41 + escaped_strings_output=(
42 + "<tool_call>\n<function=test_function>\n"
43 + '<parameter=quoted>He said "hello"</parameter>\n'
44 + "<parameter=path>C:\\Users\\file.txt</parameter>\n"
45 + "<parameter=newline>line1\nline2</parameter>\n"
46 + "</function>\n</tool_call>"
47 + ),
48 + malformed_input_outputs=[
49 + "<tool_call><function=func>",
50 + "<tool_call><function=></function></tool_call>",
51 + ],
52 + # Expected results
53 + single_tool_call_expected_name="get_weather",
54 + single_tool_call_expected_args={"city": "Tokyo"},
55 + parallel_tool_calls_count=2,
56 + parallel_tool_calls_names=["get_weather", "get_time"],
57 + # xfail markers - Qwen3XML has systematic streaming issues
58 + xfail_streaming={
59 + "test_single_tool_call_simple_args": (
60 + "Qwen3XML streaming has systematic issues"
61 + ),
62 + "test_parallel_tool_calls": "Qwen3XML streaming has systematic issues",
63 + "test_various_data_types": "Qwen3XML streaming has systematic issues",
64 + "test_empty_arguments": "Qwen3XML streaming has systematic issues",
65 + "test_surrounding_text": "Qwen3XML streaming has systematic issues",
66 + "test_escaped_strings": "Qwen3XML streaming has systematic issues",
67 + "test_malformed_input": (
68 + "Qwen3XML parser is lenient with malformed input"
69 + ),
70 + "test_streaming_reconstruction": (
71 + "Qwen3XML streaming reconstruction has known issues"
72 + ),
73 + },
74 + supports_typed_arguments=False,
75 + )
tests/tool_parsers/test_step3_tool_parser.py new
+112
@@ -0,0 +1,112 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +
4 +
5 +import pytest
6 +
7 +from tests.tool_parsers.common_tests import (
8 + ToolParserTestConfig,
9 + ToolParserTests,
10 +)
11 +from vllm.tokenizers import TokenizerLike, get_tokenizer
12 +
13 +
14 +class TestStep3ToolParser(ToolParserTests):
15 + @pytest.fixture(scope="class")
16 + def tokenizer(self) -> TokenizerLike:
17 + return get_tokenizer("stepfun-ai/step3")
18 +
19 + @pytest.fixture
20 + def test_config(self) -> ToolParserTestConfig:
21 + return ToolParserTestConfig(
22 + parser_name="step3",
23 + # Test data
24 + no_tool_calls_output="This is a regular response without any tool calls.",
25 + single_tool_call_output=(
26 + "<|tool_calls_begin|><|tool_call_begin|>"
27 + '<steptml:invoke name="get_weather">'
28 + '<steptml:parameter name="city">Tokyo</steptml:parameter>'
29 + "</steptml:invoke><|tool_call_end|><|tool_calls_end|>"
30 + ),
31 + parallel_tool_calls_output=(
32 + "<|tool_calls_begin|><|tool_call_begin|>"
33 + '<steptml:invoke name="get_weather">'
34 + '<steptml:parameter name="city">Tokyo</steptml:parameter>'
35 + "</steptml:invoke><|tool_call_end|><|tool_sep|>"
36 + '<|tool_call_begin|><steptml:invoke name="get_time">'
37 + '<steptml:parameter name="timezone">Asia/Tokyo</steptml:parameter>'
38 + "</steptml:invoke><|tool_call_end|><|tool_calls_end|>"
39 + ),
40 + various_data_types_output=(
41 + "<|tool_calls_begin|><|tool_call_begin|>"
42 + '<steptml:invoke name="test_function">'
43 + '<steptml:parameter name="string_field">hello</steptml:parameter>'
44 + '<steptml:parameter name="int_field">42</steptml:parameter>'
45 + '<steptml:parameter name="float_field">3.14</steptml:parameter>'
46 + '<steptml:parameter name="bool_field">true</steptml:parameter>'
47 + '<steptml:parameter name="null_field">null</steptml:parameter>'
48 + '<steptml:parameter name="array_field">'
49 + '["a", "b", "c"]</steptml:parameter>'
50 + '<steptml:parameter name="object_field">'
51 + '{"nested": "value"}</steptml:parameter>'
52 + "</steptml:invoke><|tool_call_end|><|tool_calls_end|>"
53 + ),
54 + empty_arguments_output=(
55 + "<|tool_calls_begin|><|tool_call_begin|>"
56 + '<steptml:invoke name="refresh"></steptml:invoke>'
57 + "<|tool_call_end|><|tool_calls_end|>"
58 + ),
59 + surrounding_text_output=(
60 + "Let me check the weather for you.\n\n"
61 + "<|tool_calls_begin|><|tool_call_begin|>"
62 + '<steptml:invoke name="get_weather">'
63 + '<steptml:parameter name="city">Tokyo</steptml:parameter>'
64 + "</steptml:invoke><|tool_call_end|><|tool_calls_end|>\n\n"
65 + "I'll get that information."
66 + ),
67 + escaped_strings_output=(
68 + "<|tool_calls_begin|><|tool_call_begin|>"
69 + '<steptml:invoke name="test_function">'
70 + '<steptml:parameter name="quoted">He said "hello"</steptml:parameter>'
71 + '<steptml:parameter name="path">C:\\Users\\file.txt</steptml:parameter>'
72 + '<steptml:parameter name="newline">line1\nline2</steptml:parameter>'
73 + "</steptml:invoke><|tool_call_end|><|tool_calls_end|>"
74 + ),
75 + malformed_input_outputs=[
76 + (
77 + "<|tool_calls_begin|><|tool_call_begin|>"
78 + '<steptml:invoke name="func">'
79 + ),
80 + (
81 + '<|tool_call_begin|><steptml:invoke name="func">'
82 + "</steptml:invoke><|tool_call_end|>"
83 + ),
84 + ],
85 + # Expected results
86 + single_tool_call_expected_name="get_weather",
87 + single_tool_call_expected_args={"city": "Tokyo"},
88 + parallel_tool_calls_count=2,
89 + parallel_tool_calls_names=["get_weather", "get_time"],
90 + # xfail markers
91 + xfail_nonstreaming={
92 + "test_single_tool_call_simple_args": (
93 + "Step3 parser non-streaming has bugs"
94 + ),
95 + "test_parallel_tool_calls": ("Step3 parser non-streaming has bugs"),
96 + "test_various_data_types": "Step3 parser non-streaming has bugs",
97 + "test_empty_arguments": "Step3 parser non-streaming has bugs",
98 + "test_surrounding_text": "Step3 parser non-streaming has bugs",
99 + "test_escaped_strings": "Step3 parser non-streaming has bugs",
100 + },
101 + xfail_streaming={
102 + "test_parallel_tool_calls": (
103 + "Step3 parser has significant bugs in both streaming "
104 + "and non-streaming"
105 + ),
106 + "test_streaming_reconstruction": (
107 + "Step3 parser non-streaming has bugs, so streaming "
108 + "doesn't match non-streaming"
109 + ),
110 + },
111 + supports_typed_arguments=False,
112 + )
tests/tool_parsers/utils.py renamed