From d1bcb4b44cc3d7bc625d9a3b490c7766f79f400f Mon Sep 17 00:00:00 2001 From: Ben Browning Date: Wed, 10 Jun 2026 13:17:16 -0400 Subject: [PATCH] [Bugfix] Fix tool parsing crash with non-function tool types (e.g. WebSearchTool) (#45147) Signed-off-by: Ben Browning --- tests/tool_use/test_tool_choice_required.py | 40 ++++++++++++++++++++- vllm/tool_parsers/utils.py | 11 ++++-- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/tests/tool_use/test_tool_choice_required.py b/tests/tool_use/test_tool_choice_required.py index e99165f356..929bb33da0 100644 --- a/tests/tool_use/test_tool_choice_required.py +++ b/tests/tool_use/test_tool_choice_required.py @@ -5,13 +5,17 @@ from copy import deepcopy import pytest import regex as re +from openai.types.responses import FunctionTool, WebSearchTool from pydantic import TypeAdapter from vllm.entrypoints.openai.chat_completion.protocol import ( ChatCompletionToolsParam, ) from vllm.tool_parsers.streaming import extract_required_tool_call_streaming -from vllm.tool_parsers.utils import get_json_schema_from_tools +from vllm.tool_parsers.utils import ( + find_tool_properties, + get_json_schema_from_tools, +) pytestmark = pytest.mark.cpu_test @@ -354,3 +358,37 @@ def test_streaming_output_valid_with_trailing_extra_data(): previous_text = current_text assert len(messages) > 0 + + +FUNCTION_TOOL = FunctionTool( + type="function", + name="get_weather", + parameters={ + "type": "object", + "properties": {"city": {"type": "string"}}, + "required": ["city"], + }, +) +WEB_SEARCH_TOOL = WebSearchTool(type="web_search") + + +class TestNonFunctionToolsSkipped: + """Non-function tools (web_search, etc.) must be silently skipped + by the tool-schema utilities instead of raising TypeError.""" + + def test_find_tool_properties_skips_web_search(self): + tools = [WEB_SEARCH_TOOL, FUNCTION_TOOL] + props = find_tool_properties(tools, "get_weather") + assert props == {"city": {"type": "string"}} + + def test_find_tool_properties_only_non_function_tools(self): + props = find_tool_properties([WEB_SEARCH_TOOL], "get_weather") + assert props == {} + + def test_get_json_schema_with_mixed_tools(self): + tools = [WEB_SEARCH_TOOL, FUNCTION_TOOL] + schema = get_json_schema_from_tools(tools=tools, tool_choice="required") + assert isinstance(schema, dict) + any_of = schema["items"]["anyOf"] + assert len(any_of) == 1 + assert any_of[0]["properties"]["name"]["enum"] == ["get_weather"] diff --git a/vllm/tool_parsers/utils.py b/vllm/tool_parsers/utils.py index 6ee107433c..fb3ff6447a 100644 --- a/vllm/tool_parsers/utils.py +++ b/vllm/tool_parsers/utils.py @@ -151,6 +151,10 @@ def consume_space(i: int, s: str) -> int: return i +def _is_function_tool(tool: Tool) -> bool: + return isinstance(tool, (FunctionTool, ChatCompletionToolsParam)) + + def _extract_tool_info( tool: Tool, ) -> tuple[str, dict[str, Any] | None]: @@ -170,6 +174,8 @@ def find_tool_properties( if not tools: return {} for tool in tools: + if not _is_function_tool(tool): + continue name, params = _extract_tool_info(tool) if name == tool_name: return (params or {}).get("properties", {}) @@ -210,15 +216,16 @@ def _get_tool_schema_defs( def _get_json_schema_from_tools( tools: list[Tool], ) -> dict: + fn_tools = [t for t in tools if _is_function_tool(t)] json_schema = { "type": "array", "minItems": 1, "items": { "type": "object", - "anyOf": [_get_tool_schema_from_tool(tool) for tool in tools], + "anyOf": [_get_tool_schema_from_tool(tool) for tool in fn_tools], }, } - json_schema_defs = _get_tool_schema_defs(tools) + json_schema_defs = _get_tool_schema_defs(fn_tools) if json_schema_defs: json_schema["$defs"] = json_schema_defs return json_schema