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 <noreply@anthropic.com>
This commit is contained in:
0xxmemo 2026-04-06 16:12:14 -05:00
parent 0379a0fe9b
commit 05ddb2eeb5
8 changed files with 74 additions and 35 deletions

View file

@ -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

View file

@ -0,0 +1 @@

View file

@ -0,0 +1 @@

View file

View file

View file

View file

@ -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):