A high-throughput and memory-efficient inference and serving engine for LLMs
[Bugfix] Fix tool parsing crash with non-function tool types (e.g. WebSearchTool) (#45147)
Signed-off-by: Ben Browning <bbrownin@redhat.com>
Ben Browning committed
Jun 10, 2026 at 13:17 UTC
d1bcb4b44cc3d7bc625d9a3b490c7766f79f400f
2 files changed
+48
-3
tests/tool_use/test_tool_choice_required.py
+39
-1
@@ -5,13 +5,17 @@ from copy import deepcopy
5
6
import pytest
7
import regex as re
8
+from openai.types.responses import FunctionTool, WebSearchTool
9
from pydantic import TypeAdapter
10
11
from vllm.entrypoints.openai.chat_completion.protocol import (
12
ChatCompletionToolsParam,
13
)
14
from vllm.tool_parsers.streaming import extract_required_tool_call_streaming
14
-from vllm.tool_parsers.utils import get_json_schema_from_tools
15
+from vllm.tool_parsers.utils import (
16
+ find_tool_properties,
17
+ get_json_schema_from_tools,
18
+)
19
20
pytestmark = pytest.mark.cpu_test
21
@@ -354,3 +358,37 @@ def test_streaming_output_valid_with_trailing_extra_data():
358
previous_text = current_text
359
360
assert len(messages) > 0
361
+
362
+
363
+FUNCTION_TOOL = FunctionTool(
364
+ type="function",
365
+ name="get_weather",
366
+ parameters={
367
+ "type": "object",
368
+ "properties": {"city": {"type": "string"}},
369
+ "required": ["city"],
370
+ },
371
+)
372
+WEB_SEARCH_TOOL = WebSearchTool(type="web_search")
373
+
374
+
375
+class TestNonFunctionToolsSkipped:
376
+ """Non-function tools (web_search, etc.) must be silently skipped
377
+ by the tool-schema utilities instead of raising TypeError."""
378
+
379
+ def test_find_tool_properties_skips_web_search(self):
380
+ tools = [WEB_SEARCH_TOOL, FUNCTION_TOOL]
381
+ props = find_tool_properties(tools, "get_weather")
382
+ assert props == {"city": {"type": "string"}}
383
+
384
+ def test_find_tool_properties_only_non_function_tools(self):
385
+ props = find_tool_properties([WEB_SEARCH_TOOL], "get_weather")
386
+ assert props == {}
387
+
388
+ def test_get_json_schema_with_mixed_tools(self):
389
+ tools = [WEB_SEARCH_TOOL, FUNCTION_TOOL]
390
+ schema = get_json_schema_from_tools(tools=tools, tool_choice="required")
391
+ assert isinstance(schema, dict)
392
+ any_of = schema["items"]["anyOf"]
393
+ assert len(any_of) == 1
394
+ assert any_of[0]["properties"]["name"]["enum"] == ["get_weather"]
vllm/tool_parsers/utils.py
+9
-2
@@ -151,6 +151,10 @@ def consume_space(i: int, s: str) -> int:
151
return i
152
153
154
+def _is_function_tool(tool: Tool) -> bool:
155
+ return isinstance(tool, (FunctionTool, ChatCompletionToolsParam))
156
+
157
+
158
def _extract_tool_info(
159
tool: Tool,
160
) -> tuple[str, dict[str, Any] | None]:
@@ -170,6 +174,8 @@ def find_tool_properties(
174
if not tools:
175
return {}
176
for tool in tools:
177
+ if not _is_function_tool(tool):
178
+ continue
179
name, params = _extract_tool_info(tool)
180
if name == tool_name:
181
return (params or {}).get("properties", {})
@@ -210,15 +216,16 @@ def _get_tool_schema_defs(
216
def _get_json_schema_from_tools(
217
tools: list[Tool],
218
) -> dict:
219
+ fn_tools = [t for t in tools if _is_function_tool(t)]
220
json_schema = {
221
"type": "array",
222
"minItems": 1,
223
"items": {
224
"type": "object",
218
- "anyOf": [_get_tool_schema_from_tool(tool) for tool in tools],
225
+ "anyOf": [_get_tool_schema_from_tool(tool) for tool in fn_tools],
226
},
227
}
221
- json_schema_defs = _get_tool_schema_defs(tools)
228
+ json_schema_defs = _get_tool_schema_defs(fn_tools)
229
if json_schema_defs:
230
json_schema["$defs"] = json_schema_defs
231
return json_schema