From f2b399d451c5f094568848bdd01b0965c967bb71 Mon Sep 17 00:00:00 2001 From: lei_lei <96427312+leilei3167@users.noreply.github.com> Date: Sun, 30 Aug 2026 10:49:44 +0800 Subject: [PATCH 1/7] fix(websearch): recognize conventional function tool name --- .../websearch_interception/tools.py | 8 ++--- .../test_websearch_tools.py | 34 +++++++++++++++++++ 2 files changed, 38 insertions(+), 4 deletions(-) create mode 100644 tests/test_litellm/integrations/websearch_interception/test_websearch_tools.py diff --git a/litellm/integrations/websearch_interception/tools.py b/litellm/integrations/websearch_interception/tools.py index 97c6c90d2ba..04178d88179 100644 --- a/litellm/integrations/websearch_interception/tools.py +++ b/litellm/integrations/websearch_interception/tools.py @@ -156,12 +156,12 @@ def is_web_search_tool_chat_completion(tool: dict[str, Any]) -> bool: """ Check if a tool is a web search tool for Chat Completions API (strict check). - This is a stricter version that ONLY checks for the exact LiteLLM web search tool name. + This is a stricter version that checks the recognized web search tool names. Use this for Chat Completions API to avoid false positives with user-defined tools. Detects ONLY: - LiteLLM standard: name == "litellm_web_search" (Anthropic format) - - OpenAI format: type == "function" with function.name == "litellm_web_search" + - OpenAI format: type == "function" with function.name == "litellm_web_search" or "web_search" Args: tool: Tool dictionary to check @@ -186,7 +186,7 @@ def is_web_search_tool_chat_completion(tool: dict[str, Any]) -> bool: if tool_type == "function" and "function" in tool: function_def: Final = tool.get("function", {}) function_name: Final = function_def.get("name", "") - if function_name == LITELLM_WEB_SEARCH_TOOL_NAME: + if function_name in (LITELLM_WEB_SEARCH_TOOL_NAME, "web_search"): return True # Check for LiteLLM standard tool (Anthropic format) @@ -270,7 +270,7 @@ def is_web_search_tool(tool: dict[str, Any]) -> bool: if tool_type == "function" and "function" in tool: function_def: Final = tool.get("function", {}) function_name: Final = function_def.get("name", "") - if function_name == LITELLM_WEB_SEARCH_TOOL_NAME: + if function_name in (LITELLM_WEB_SEARCH_TOOL_NAME, "web_search"): return True # Check for LiteLLM standard tool (Anthropic format) diff --git a/tests/test_litellm/integrations/websearch_interception/test_websearch_tools.py b/tests/test_litellm/integrations/websearch_interception/test_websearch_tools.py new file mode 100644 index 00000000000..afba4306272 --- /dev/null +++ b/tests/test_litellm/integrations/websearch_interception/test_websearch_tools.py @@ -0,0 +1,34 @@ +"""Unit tests for web search tool shape detection.""" + +import pytest + +from litellm.integrations.websearch_interception.tools import ( + is_web_search_tool, + is_web_search_tool_chat_completion, +) + + +@pytest.mark.parametrize( + "tool", + [ + {"type": "function", "function": {"name": "litellm_web_search"}}, + {"type": "function", "function": {"name": "web_search"}}, + ], +) +def test_openai_function_web_search_shapes_are_detected(tool): + """Recognize both LiteLLM and conventional web_search function names.""" + assert is_web_search_tool(tool) is True + assert is_web_search_tool_chat_completion(tool) is True + + +@pytest.mark.parametrize( + "tool", + [ + {"type": "function", "function": {"name": "web_search_helper"}}, + {"type": "function", "function": {"name": "search"}}, + ], +) +def test_unrelated_openai_function_tools_are_not_detected(tool): + """Do not classify similarly named user tools as web search.""" + assert is_web_search_tool(tool) is False + assert is_web_search_tool_chat_completion(tool) is False From a6223241823fb9e4a07abbb40e927f52ade0f12f Mon Sep 17 00:00:00 2001 From: lei_lei <96427312+leilei3167@users.noreply.github.com> Date: Sun, 30 Aug 2026 12:18:13 +0800 Subject: [PATCH 2/7] fix(websearch): preserve user-defined web_search tools --- .../websearch_interception/tools.py | 21 +++++++++++++--- .../test_websearch_tools.py | 25 +++++++++++++++++-- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/litellm/integrations/websearch_interception/tools.py b/litellm/integrations/websearch_interception/tools.py index 04178d88179..d5c72f6eae6 100644 --- a/litellm/integrations/websearch_interception/tools.py +++ b/litellm/integrations/websearch_interception/tools.py @@ -161,7 +161,9 @@ def is_web_search_tool_chat_completion(tool: dict[str, Any]) -> bool: Detects ONLY: - LiteLLM standard: name == "litellm_web_search" (Anthropic format) - - OpenAI format: type == "function" with function.name == "litellm_web_search" or "web_search" + - OpenAI format: type == "function" with function.name == "litellm_web_search" + or a bare function definition whose name is "web_search" + or a bare function definition whose name is "web_search" Args: tool: Tool dictionary to check @@ -185,8 +187,15 @@ def is_web_search_tool_chat_completion(tool: dict[str, Any]) -> bool: # Check for OpenAI format: {"type": "function", "function": {"name": "litellm_web_search"}} if tool_type == "function" and "function" in tool: function_def: Final = tool.get("function", {}) + if not isinstance(function_def, dict): + return False function_name: Final = function_def.get("name", "") - if function_name in (LITELLM_WEB_SEARCH_TOOL_NAME, "web_search"): + if function_name == LITELLM_WEB_SEARCH_TOOL_NAME: + return True + # The conventional name is only recognized for the name-only shape. + # A user-defined function may also be named ``web_search``; preserving + # any additional definition fields lets that tool pass through intact. + if function_name == "web_search" and set(function_def) == {"name"}: return True # Check for LiteLLM standard tool (Anthropic format) @@ -269,8 +278,14 @@ def is_web_search_tool(tool: dict[str, Any]) -> bool: # Check for OpenAI format: {"type": "function", "function": {"name": "..."}} if tool_type == "function" and "function" in tool: function_def: Final = tool.get("function", {}) + if not isinstance(function_def, dict): + return False function_name: Final = function_def.get("name", "") - if function_name in (LITELLM_WEB_SEARCH_TOOL_NAME, "web_search"): + if function_name == LITELLM_WEB_SEARCH_TOOL_NAME: + return True + # Do not hijack a user-defined function that happens to use the + # conventional name and carries its own schema or description. + if function_name == "web_search" and set(function_def) == {"name"}: return True # Check for LiteLLM standard tool (Anthropic format) diff --git a/tests/test_litellm/integrations/websearch_interception/test_websearch_tools.py b/tests/test_litellm/integrations/websearch_interception/test_websearch_tools.py index afba4306272..06ffd1b311f 100644 --- a/tests/test_litellm/integrations/websearch_interception/test_websearch_tools.py +++ b/tests/test_litellm/integrations/websearch_interception/test_websearch_tools.py @@ -1,5 +1,7 @@ """Unit tests for web search tool shape detection.""" +from typing import Any + import pytest from litellm.integrations.websearch_interception.tools import ( @@ -15,7 +17,7 @@ from litellm.integrations.websearch_interception.tools import ( {"type": "function", "function": {"name": "web_search"}}, ], ) -def test_openai_function_web_search_shapes_are_detected(tool): +def test_openai_function_web_search_shapes_are_detected(tool: dict[str, Any]): """Recognize both LiteLLM and conventional web_search function names.""" assert is_web_search_tool(tool) is True assert is_web_search_tool_chat_completion(tool) is True @@ -28,7 +30,26 @@ def test_openai_function_web_search_shapes_are_detected(tool): {"type": "function", "function": {"name": "search"}}, ], ) -def test_unrelated_openai_function_tools_are_not_detected(tool): +def test_unrelated_openai_function_tools_are_not_detected(tool: dict[str, Any]): """Do not classify similarly named user tools as web search.""" assert is_web_search_tool(tool) is False assert is_web_search_tool_chat_completion(tool) is False + + +@pytest.mark.parametrize( + "tool", + [ + { + "type": "function", + "function": { + "name": "web_search", + "description": "Search a private index", + "parameters": {"type": "object", "properties": {}}, + }, + }, + ], +) +def test_user_defined_web_search_functions_are_not_detected(tool: dict[str, Any]): + """Preserve user-defined schemas even when their name is web_search.""" + assert is_web_search_tool(tool) is False + assert is_web_search_tool_chat_completion(tool) is False From af0e0f478a6254f24d1b7d9f63895d89bf129fe7 Mon Sep 17 00:00:00 2001 From: lei_lei <96427312+leilei3167@users.noreply.github.com> Date: Sun, 30 Aug 2026 12:35:12 +0800 Subject: [PATCH 3/7] fix(websearch): avoid mutable key check --- litellm/integrations/websearch_interception/tools.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/litellm/integrations/websearch_interception/tools.py b/litellm/integrations/websearch_interception/tools.py index d5c72f6eae6..e5ca0b8ba8f 100644 --- a/litellm/integrations/websearch_interception/tools.py +++ b/litellm/integrations/websearch_interception/tools.py @@ -195,7 +195,7 @@ def is_web_search_tool_chat_completion(tool: dict[str, Any]) -> bool: # The conventional name is only recognized for the name-only shape. # A user-defined function may also be named ``web_search``; preserving # any additional definition fields lets that tool pass through intact. - if function_name == "web_search" and set(function_def) == {"name"}: + if function_name == "web_search" and len(function_def) == 1 and "name" in function_def: return True # Check for LiteLLM standard tool (Anthropic format) @@ -285,7 +285,7 @@ def is_web_search_tool(tool: dict[str, Any]) -> bool: return True # Do not hijack a user-defined function that happens to use the # conventional name and carries its own schema or description. - if function_name == "web_search" and set(function_def) == {"name"}: + if function_name == "web_search" and len(function_def) == 1 and "name" in function_def: return True # Check for LiteLLM standard tool (Anthropic format) From f78c6592c1dc2cf5cd343182dd842c99faac5ac2 Mon Sep 17 00:00:00 2001 From: lei_lei <96427312+leilei3167@users.noreply.github.com> Date: Sun, 30 Aug 2026 14:21:25 +0800 Subject: [PATCH 4/7] test: cover invalid web search function shapes --- .../integrations/websearch_interception/test_websearch_tools.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_litellm/integrations/websearch_interception/test_websearch_tools.py b/tests/test_litellm/integrations/websearch_interception/test_websearch_tools.py index 06ffd1b311f..c0ece4e8e7d 100644 --- a/tests/test_litellm/integrations/websearch_interception/test_websearch_tools.py +++ b/tests/test_litellm/integrations/websearch_interception/test_websearch_tools.py @@ -28,6 +28,7 @@ def test_openai_function_web_search_shapes_are_detected(tool: dict[str, Any]): [ {"type": "function", "function": {"name": "web_search_helper"}}, {"type": "function", "function": {"name": "search"}}, + {"type": "function", "function": None}, ], ) def test_unrelated_openai_function_tools_are_not_detected(tool: dict[str, Any]): From 7cb01e0795765f23ab9c88919b138dbc9116a43b Mon Sep 17 00:00:00 2001 From: leilei3167 Date: Wed, 16 Sep 2026 15:10:18 +0000 Subject: [PATCH 5/7] fix(websearch): gate conventional web_search name behind opt-in Name-only OpenAI function tools named web_search are valid user tools. Recognize that conventional envelope only when websearch_interception_params.recognize_conventional_web_search_name is true, so enabling interception no longer hijacks those handlers by default. Addresses greptile P1 on #40951. --- .../websearch_interception/handler.py | 34 +++++++++++++++---- .../websearch_interception/tools.py | 30 ++++++++++------ .../integrations/websearch_interception.py | 6 ++++ .../test_websearch_tools.py | 14 ++++++-- 4 files changed, 63 insertions(+), 21 deletions(-) diff --git a/litellm/integrations/websearch_interception/handler.py b/litellm/integrations/websearch_interception/handler.py index 587da997f94..463dd928c1e 100644 --- a/litellm/integrations/websearch_interception/handler.py +++ b/litellm/integrations/websearch_interception/handler.py @@ -213,6 +213,7 @@ class WebSearchInterceptionLogger(CustomLogger): enabled_providers: list[LlmProviders | str] | None = None, search_tool_name: str | None = None, max_agentic_loops: int | None = None, + recognize_conventional_web_search_name: bool = False, ): """ Args: @@ -225,6 +226,10 @@ class WebSearchInterceptionLogger(CustomLogger): max_agentic_loops: How many follow-up model calls one intercepted request may chain before the loop is refused and the turn ends. If None, LiteLLM's default of 3 applies. + recognize_conventional_web_search_name: When True, treat name-only OpenAI + function tools named ``web_search`` as server search. + Default False so a user-defined name-only ``web_search`` + still reaches the client handler. """ super().__init__() # Convert enum values to strings for comparison @@ -234,8 +239,19 @@ class WebSearchInterceptionLogger(CustomLogger): self.enabled_providers = [p.value if isinstance(p, LlmProviders) else p for p in enabled_providers] self.search_tool_name = search_tool_name self.max_agentic_loops = self._validated_max_agentic_loops(max_agentic_loops) + self.recognize_conventional_web_search_name = bool(recognize_conventional_web_search_name) self._request_has_websearch = False # Track if current request has web search + def _is_web_search_tool(self, tool: dict[str, Any]) -> bool: + return is_web_search_tool( + tool, recognize_conventional_name=self.recognize_conventional_web_search_name + ) + + def _is_web_search_tool_chat_completion(self, tool: dict[str, Any]) -> bool: + return is_web_search_tool_chat_completion( + tool, recognize_conventional_name=self.recognize_conventional_web_search_name + ) + @staticmethod def _validated_max_agentic_loops(max_agentic_loops: object) -> int | None: """ @@ -305,7 +321,7 @@ class WebSearchInterceptionLogger(CustomLogger): pass # unknown provider enum → safe to short-circuit # All tools must be web search tools - if not all(is_web_search_tool(t) for t in tools): + if not all(self._is_web_search_tool(t) for t in tools): return None # Extract search query from the last user message @@ -419,7 +435,7 @@ class WebSearchInterceptionLogger(CustomLogger): if call_type in (CallTypes.responses, CallTypes.aresponses): return self._convert_responses_tools(kwargs=kwargs, tools=tools) - has_websearch: Final = any(is_web_search_tool(t) for t in tools) + has_websearch: Final = any(self._is_web_search_tool(t) for t in tools) if not has_websearch: return None @@ -437,7 +453,7 @@ class WebSearchInterceptionLogger(CustomLogger): # Convert native/custom web_search tools to LiteLLM standard converted_tools: Final = [] for tool in tools: - if is_web_search_tool(tool): + if self._is_web_search_tool(tool): # Convert to LiteLLM standard web search tool converted_tool = get_litellm_web_search_tool_openai() converted_tools.append(converted_tool) @@ -509,6 +525,9 @@ class WebSearchInterceptionLogger(CustomLogger): enabled_providers_str: Final = config.get("enabled_providers", None) search_tool_name: Final = config.get("search_tool_name", None) max_agentic_loops: Final = config.get("max_agentic_loops", None) + recognize_conventional_web_search_name: Final = bool( + config.get("recognize_conventional_web_search_name", False) + ) # Convert string provider names to LlmProviders enum values enabled_providers: list[LlmProviders | str] | None = None @@ -527,6 +546,7 @@ class WebSearchInterceptionLogger(CustomLogger): enabled_providers=enabled_providers, search_tool_name=search_tool_name, max_agentic_loops=max_agentic_loops, + recognize_conventional_web_search_name=recognize_conventional_web_search_name, ) @staticmethod @@ -591,7 +611,7 @@ class WebSearchInterceptionLogger(CustomLogger): return None # Check if any tool is a web search tool - has_websearch: Final = any(is_web_search_tool(t) for t in tools) + has_websearch: Final = any(self._is_web_search_tool(t) for t in tools) if not has_websearch: return None @@ -612,7 +632,7 @@ class WebSearchInterceptionLogger(CustomLogger): # Convert native web search tools to LiteLLM standard converted_tools: Final[list[dict[str, object]]] = [] for tool in tools: - if is_web_search_tool(tool): + if self._is_web_search_tool(tool): standard_tool = get_litellm_web_search_tool() converted_tools.append(standard_tool) verbose_logger.debug( @@ -687,7 +707,7 @@ class WebSearchInterceptionLogger(CustomLogger): return False, {} # Check if tools include any web search tool (LiteLLM standard or native) - has_websearch_tool: Final = any(is_web_search_tool(t) for t in (tools or [])) + has_websearch_tool: Final = any(self._is_web_search_tool(t) for t in (tools or [])) if not has_websearch_tool: verbose_logger.debug("WebSearchInterception: No web search tool in request") return False, {} @@ -783,7 +803,7 @@ class WebSearchInterceptionLogger(CustomLogger): return False, {} # Check if tools include any web search tool (strict check for chat completions) - has_websearch_tool: Final = any(is_web_search_tool_chat_completion(t) for t in (tools or [])) + has_websearch_tool: Final = any(self._is_web_search_tool_chat_completion(t) for t in (tools or [])) if not has_websearch_tool: verbose_logger.debug("WebSearchInterception: No litellm_web_search tool in request") return False, {} diff --git a/litellm/integrations/websearch_interception/tools.py b/litellm/integrations/websearch_interception/tools.py index e5ca0b8ba8f..97667d9fcf5 100644 --- a/litellm/integrations/websearch_interception/tools.py +++ b/litellm/integrations/websearch_interception/tools.py @@ -152,7 +152,7 @@ def is_web_search_tool_responses(tool: Mapping[str, object]) -> bool: return tool_type == "web_search" or tool_type.startswith("web_search_") -def is_web_search_tool_chat_completion(tool: dict[str, Any]) -> bool: +def is_web_search_tool_chat_completion(tool: dict[str, Any], *, recognize_conventional_name: bool = False) -> bool: """ Check if a tool is a web search tool for Chat Completions API (strict check). @@ -162,8 +162,7 @@ def is_web_search_tool_chat_completion(tool: dict[str, Any]) -> bool: Detects ONLY: - LiteLLM standard: name == "litellm_web_search" (Anthropic format) - OpenAI format: type == "function" with function.name == "litellm_web_search" - or a bare function definition whose name is "web_search" - or a bare function definition whose name is "web_search" + - Optionally (recognize_conventional_name=True): name-only function ``web_search`` Args: tool: Tool dictionary to check @@ -192,10 +191,14 @@ def is_web_search_tool_chat_completion(tool: dict[str, Any]) -> bool: function_name: Final = function_def.get("name", "") if function_name == LITELLM_WEB_SEARCH_TOOL_NAME: return True - # The conventional name is only recognized for the name-only shape. - # A user-defined function may also be named ``web_search``; preserving - # any additional definition fields lets that tool pass through intact. - if function_name == "web_search" and len(function_def) == 1 and "name" in function_def: + # Name-only ``web_search`` is opt-in: without the flag, a valid + # user-defined function with only a name still reaches the client. + if ( + recognize_conventional_name + and function_name == "web_search" + and len(function_def) == 1 + and "name" in function_def + ): return True # Check for LiteLLM standard tool (Anthropic format) @@ -226,7 +229,7 @@ def is_anthropic_native_web_search_tool(tool: Mapping[str, object]) -> bool: return tool_type.startswith("web_search_") and tool_type != "function" -def is_web_search_tool(tool: dict[str, Any]) -> bool: +def is_web_search_tool(tool: dict[str, Any], *, recognize_conventional_name: bool = False) -> bool: """ Check if a tool is a web search tool (native or LiteLLM standard). @@ -283,9 +286,14 @@ def is_web_search_tool(tool: dict[str, Any]) -> bool: function_name: Final = function_def.get("name", "") if function_name == LITELLM_WEB_SEARCH_TOOL_NAME: return True - # Do not hijack a user-defined function that happens to use the - # conventional name and carries its own schema or description. - if function_name == "web_search" and len(function_def) == 1 and "name" in function_def: + # Name-only ``web_search`` is opt-in: without the flag, a valid + # user-defined function with only a name still reaches the client. + if ( + recognize_conventional_name + and function_name == "web_search" + and len(function_def) == 1 + and "name" in function_def + ): return True # Check for LiteLLM standard tool (Anthropic format) diff --git a/litellm/types/integrations/websearch_interception.py b/litellm/types/integrations/websearch_interception.py index 7926b9eee0a..7209445542b 100644 --- a/litellm/types/integrations/websearch_interception.py +++ b/litellm/types/integrations/websearch_interception.py @@ -47,3 +47,9 @@ class WebSearchInterceptionConfig(TypedDict, total=False): max_agentic_loops: ReadOnly[int | None] """How many follow-up model calls one intercepted request may chain. If None, LiteLLM's default of 3 applies.""" + + recognize_conventional_web_search_name: bool + """When True, treat name-only OpenAI function tools named ``web_search`` as + server search tools. Default is off so a user-defined name-only ``web_search`` + function still reaches the client handler. + """ diff --git a/tests/test_litellm/integrations/websearch_interception/test_websearch_tools.py b/tests/test_litellm/integrations/websearch_interception/test_websearch_tools.py index c0ece4e8e7d..b9a74e99be9 100644 --- a/tests/test_litellm/integrations/websearch_interception/test_websearch_tools.py +++ b/tests/test_litellm/integrations/websearch_interception/test_websearch_tools.py @@ -14,15 +14,23 @@ from litellm.integrations.websearch_interception.tools import ( "tool", [ {"type": "function", "function": {"name": "litellm_web_search"}}, - {"type": "function", "function": {"name": "web_search"}}, ], ) -def test_openai_function_web_search_shapes_are_detected(tool: dict[str, Any]): - """Recognize both LiteLLM and conventional web_search function names.""" +def test_openai_function_litellm_web_search_is_detected(tool: dict[str, Any]): + """Recognize the LiteLLM standard web_search function name.""" assert is_web_search_tool(tool) is True assert is_web_search_tool_chat_completion(tool) is True +def test_conventional_web_search_requires_opt_in(): + """Name-only web_search stays a user tool unless explicitly opted in.""" + tool = {"type": "function", "function": {"name": "web_search"}} + assert is_web_search_tool(tool) is False + assert is_web_search_tool_chat_completion(tool) is False + assert is_web_search_tool(tool, recognize_conventional_name=True) is True + assert is_web_search_tool_chat_completion(tool, recognize_conventional_name=True) is True + + @pytest.mark.parametrize( "tool", [ From a5463b543dc2254bc9740fa802871e3520c99695 Mon Sep 17 00:00:00 2001 From: leilei3167 Date: Wed, 16 Sep 2026 15:23:33 +0000 Subject: [PATCH 6/7] style(websearch): ruff-format handler call site --- litellm/integrations/websearch_interception/handler.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/litellm/integrations/websearch_interception/handler.py b/litellm/integrations/websearch_interception/handler.py index 463dd928c1e..4ef710e9859 100644 --- a/litellm/integrations/websearch_interception/handler.py +++ b/litellm/integrations/websearch_interception/handler.py @@ -243,9 +243,7 @@ class WebSearchInterceptionLogger(CustomLogger): self._request_has_websearch = False # Track if current request has web search def _is_web_search_tool(self, tool: dict[str, Any]) -> bool: - return is_web_search_tool( - tool, recognize_conventional_name=self.recognize_conventional_web_search_name - ) + return is_web_search_tool(tool, recognize_conventional_name=self.recognize_conventional_web_search_name) def _is_web_search_tool_chat_completion(self, tool: dict[str, Any]) -> bool: return is_web_search_tool_chat_completion( From f92341acf67a987e55dbb83906710933d7c1015e Mon Sep 17 00:00:00 2001 From: leilei3167 Date: Wed, 16 Sep 2026 15:38:33 +0000 Subject: [PATCH 7/7] fix(websearch): mark conventional-name flag ReadOnly for LIT012 TypedDict writable fields count against the type-discipline budget. --- litellm/types/integrations/websearch_interception.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/types/integrations/websearch_interception.py b/litellm/types/integrations/websearch_interception.py index 7209445542b..1690e751660 100644 --- a/litellm/types/integrations/websearch_interception.py +++ b/litellm/types/integrations/websearch_interception.py @@ -48,7 +48,7 @@ class WebSearchInterceptionConfig(TypedDict, total=False): max_agentic_loops: ReadOnly[int | None] """How many follow-up model calls one intercepted request may chain. If None, LiteLLM's default of 3 applies.""" - recognize_conventional_web_search_name: bool + recognize_conventional_web_search_name: ReadOnly[bool] """When True, treat name-only OpenAI function tools named ``web_search`` as server search tools. Default is off so a user-defined name-only ``web_search`` function still reaches the client handler.