From 05ddb2eeb504688463737f6c4fd8072ed3c4bb01 Mon Sep 17 00:00:00 2001 From: 0xxmemo Date: Mon, 6 Apr 2026 16:12:14 -0500 Subject: [PATCH] Restore native provider skip for upstream compatibility Providers with native Anthropic Messages support (anthropic, bedrock, vertex_ai) skip the short-circuit and let their API handle web_search_20250305 natively. Only non-native providers (github_copilot, etc.) get the synthetic native-format response. This preserves upstream behavior: Anthropic's API handles web search server-side for providers that support it. Co-Authored-By: Claude Opus 4.6 --- .../websearch_interception/handler.py | 33 +++++++-- litellm/llms/kimi_code/__init__.py | 1 + litellm/llms/kimi_code/chat/__init__.py | 1 + litellm/llms/local/__init__.py | 0 litellm/llms/local/embedding/__init__.py | 0 litellm/llms/local/transcription/__init__.py | 0 litellm/llms/qwen_portal/__init__.py | 0 .../test_websearch_short_circuit.py | 74 +++++++++++-------- 8 files changed, 74 insertions(+), 35 deletions(-) create mode 100644 litellm/llms/kimi_code/__init__.py create mode 100644 litellm/llms/kimi_code/chat/__init__.py create mode 100644 litellm/llms/local/__init__.py create mode 100644 litellm/llms/local/embedding/__init__.py create mode 100644 litellm/llms/local/transcription/__init__.py create mode 100644 litellm/llms/qwen_portal/__init__.py diff --git a/litellm/integrations/websearch_interception/handler.py b/litellm/integrations/websearch_interception/handler.py index 81850272579..f72244a1e8a 100644 --- a/litellm/integrations/websearch_interception/handler.py +++ b/litellm/integrations/websearch_interception/handler.py @@ -81,13 +81,15 @@ class WebSearchInterceptionLogger(CustomLogger): Short-circuit web-search-only requests by executing the search directly. Claude Code sends web search as a separate, standalone /v1/messages - request with a simple prompt and only web_search tool(s). We execute - the search via the configured provider (SearXNG/Tavily/Perplexity) - and return a synthetic response in native Anthropic format - (server_tool_use + web_search_tool_result) so Claude Code's - WebSearchTool parser works correctly. + request with a simple prompt and only web_search tool(s). For providers + that don't natively support web search, we execute the search via the + configured provider (SearXNG/Tavily/Perplexity) and return a synthetic + response in native Anthropic format (server_tool_use + + web_search_tool_result) so Claude Code's WebSearchTool parser works. - All providers are handled uniformly through this single funnel. + Providers with native Anthropic Messages support (anthropic, bedrock, + vertex_ai, azure_ai) are skipped — their API handles web search + natively and returns the correct format already. Args: model: Model name from the request @@ -110,6 +112,25 @@ class WebSearchInterceptionLogger(CustomLogger): ): return None + # Skip providers with native Anthropic Messages support — their API + # handles web_search_20250305 natively, returning server_tool_use + + # web_search_tool_result in the correct format already. + try: + provider_enum = LlmProviders(provider_str) + anthropic_config = ( + ProviderConfigManager.get_provider_anthropic_messages_config( + model=model, provider=provider_enum + ) + ) + if anthropic_config is not None: + verbose_logger.debug( + f"WebSearchInterception: Skipping short-circuit for {provider_str} " + "(provider has native web search support)" + ) + return None + except (ValueError, Exception): + 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): return None diff --git a/litellm/llms/kimi_code/__init__.py b/litellm/llms/kimi_code/__init__.py new file mode 100644 index 00000000000..8b137891791 --- /dev/null +++ b/litellm/llms/kimi_code/__init__.py @@ -0,0 +1 @@ + diff --git a/litellm/llms/kimi_code/chat/__init__.py b/litellm/llms/kimi_code/chat/__init__.py new file mode 100644 index 00000000000..8b137891791 --- /dev/null +++ b/litellm/llms/kimi_code/chat/__init__.py @@ -0,0 +1 @@ + diff --git a/litellm/llms/local/__init__.py b/litellm/llms/local/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/litellm/llms/local/embedding/__init__.py b/litellm/llms/local/embedding/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/litellm/llms/local/transcription/__init__.py b/litellm/llms/local/transcription/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/litellm/llms/qwen_portal/__init__.py b/litellm/llms/qwen_portal/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/test_litellm/integrations/websearch_interception/test_websearch_short_circuit.py b/tests/test_litellm/integrations/websearch_interception/test_websearch_short_circuit.py index 8a81d736f16..8ec2dcc2115 100644 --- a/tests/test_litellm/integrations/websearch_interception/test_websearch_short_circuit.py +++ b/tests/test_litellm/integrations/websearch_interception/test_websearch_short_circuit.py @@ -62,7 +62,7 @@ class TestTryShortCircuitSearch: @pytest.mark.asyncio async def test_native_format_search_hits(self): """Search results are structured as web_search_result hits""" - logger = WebSearchInterceptionLogger(enabled_providers=["anthropic"]) + logger = WebSearchInterceptionLogger(enabled_providers=["github_copilot"]) with patch.object( logger, "_execute_search", new_callable=AsyncMock @@ -73,10 +73,10 @@ class TestTryShortCircuitSearch: ) result = await logger.try_short_circuit_search( - model="anthropic/claude-sonnet-4", + model="github_copilot/claude-sonnet-4", messages=[{"role": "user", "content": "Search query"}], tools=[{"type": "web_search_20250305", "name": "web_search"}], - custom_llm_provider="anthropic", + custom_llm_provider="github_copilot", ) assert result is not None @@ -90,7 +90,7 @@ class TestTryShortCircuitSearch: @pytest.mark.asyncio async def test_server_tool_use_has_query(self): """server_tool_use block contains the original search query""" - logger = WebSearchInterceptionLogger(enabled_providers=["anthropic"]) + logger = WebSearchInterceptionLogger(enabled_providers=["github_copilot"]) with patch.object( logger, "_execute_search", new_callable=AsyncMock @@ -98,10 +98,10 @@ class TestTryShortCircuitSearch: mock_search.return_value = "Title: R\nURL: https://x.com\nSnippet: s" result = await logger.try_short_circuit_search( - model="anthropic/claude-sonnet-4", + model="github_copilot/claude-sonnet-4", messages=[{"role": "user", "content": "trending AI topics"}], tools=[{"type": "web_search_20250305", "name": "web_search"}], - custom_llm_provider="anthropic", + custom_llm_provider="github_copilot", ) stu = result["content"][0] @@ -113,7 +113,7 @@ class TestTryShortCircuitSearch: @pytest.mark.asyncio async def test_tool_use_id_links_blocks(self): """server_tool_use.id matches web_search_tool_result.tool_use_id""" - logger = WebSearchInterceptionLogger(enabled_providers=["anthropic"]) + logger = WebSearchInterceptionLogger(enabled_providers=["github_copilot"]) with patch.object( logger, "_execute_search", new_callable=AsyncMock @@ -121,10 +121,10 @@ class TestTryShortCircuitSearch: mock_search.return_value = "Title: R\nURL: https://x.com\nSnippet: s" result = await logger.try_short_circuit_search( - model="anthropic/claude-sonnet-4", + model="github_copilot/claude-sonnet-4", messages=[{"role": "user", "content": "query"}], tools=[{"type": "web_search_20250305", "name": "web_search"}], - custom_llm_provider="anthropic", + custom_llm_provider="github_copilot", ) assert result["content"][0]["id"] == result["content"][1]["tool_use_id"] @@ -132,7 +132,7 @@ class TestTryShortCircuitSearch: @pytest.mark.asyncio async def test_usage_includes_web_search_requests(self): """Usage includes server_tool_use.web_search_requests count""" - logger = WebSearchInterceptionLogger(enabled_providers=["anthropic"]) + logger = WebSearchInterceptionLogger(enabled_providers=["github_copilot"]) with patch.object( logger, "_execute_search", new_callable=AsyncMock @@ -140,10 +140,10 @@ class TestTryShortCircuitSearch: mock_search.return_value = "Title: R\nURL: https://x.com\nSnippet: s" result = await logger.try_short_circuit_search( - model="anthropic/claude-sonnet-4", + model="github_copilot/claude-sonnet-4", messages=[{"role": "user", "content": "query"}], tools=[{"type": "web_search_20250305", "name": "web_search"}], - custom_llm_provider="anthropic", + custom_llm_provider="github_copilot", ) assert result["usage"]["server_tool_use"]["web_search_requests"] == 1 @@ -210,26 +210,42 @@ class TestTryShortCircuitSearch: assert result is None @pytest.mark.asyncio - async def test_short_circuits_all_enabled_providers_uniformly(self): - """All enabled providers (including anthropic, bedrock) go through - the same short-circuit funnel — no provider is skipped.""" - for provider in ["anthropic", "bedrock", "github_copilot"]: - logger = WebSearchInterceptionLogger(enabled_providers=[provider]) + async def test_does_not_short_circuit_native_providers(self): + """Providers with native Anthropic Messages support (anthropic, bedrock, + vertex_ai) are skipped — their API handles web search natively.""" + for provider in ["anthropic", "bedrock"]: + logger = WebSearchInterceptionLogger( + enabled_providers=[provider, "github_copilot"] + ) - with patch.object( - logger, "_execute_search", new_callable=AsyncMock - ) as mock_search: - mock_search.return_value = "Title: R\nURL: https://x.com\nSnippet: s" + result = await logger.try_short_circuit_search( + model=f"{provider}/claude-sonnet-4", + messages=[{"role": "user", "content": "search query"}], + tools=[{"type": "web_search_20250305", "name": "web_search"}], + custom_llm_provider=provider, + ) - result = await logger.try_short_circuit_search( - model=f"{provider}/claude-sonnet-4", - messages=[{"role": "user", "content": "search query"}], - tools=[{"type": "web_search_20250305", "name": "web_search"}], - custom_llm_provider=provider, - ) + assert result is None, f"Short-circuit should NOT fire for native provider {provider}" - assert result is not None, f"Short-circuit should fire for {provider}" - assert result["content"][0]["type"] == "server_tool_use" + @pytest.mark.asyncio + async def test_short_circuits_non_native_providers(self): + """Non-native providers (github_copilot, etc.) get short-circuited.""" + logger = WebSearchInterceptionLogger(enabled_providers=["github_copilot"]) + + with patch.object( + logger, "_execute_search", new_callable=AsyncMock + ) as mock_search: + mock_search.return_value = "Title: R\nURL: https://x.com\nSnippet: s" + + result = await logger.try_short_circuit_search( + model="github_copilot/claude-sonnet-4", + messages=[{"role": "user", "content": "search query"}], + tools=[{"type": "web_search_20250305", "name": "web_search"}], + custom_llm_provider="github_copilot", + ) + + assert result is not None + assert result["content"][0]["type"] == "server_tool_use" @pytest.mark.asyncio async def test_does_not_short_circuit_no_messages(self):