fix(bedrock): reject Anthropic server-side web_search tool with actionable error (#36473)

* fix(bedrock): reject Anthropic server-side web_search tool with actionable error

Bedrock's Anthropic Messages endpoints cannot execute Anthropic's
server-side web_search tool, so forwarding it returns an opaque
"The provided request is not valid" 400 from Bedrock. Fail fast in the
invoke transform with an error that names the unsupported tool, the
model, and links the web search interception docs as the fix.

* refactor(bedrock): address review nits on web_search guard typing
This commit is contained in:
ryan-crabbe-berri 2026-08-10 15:59:18 -07:00 committed by GitHub
parent f1ed4690bb
commit 20354bfcdc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 92 additions and 0 deletions

View file

@ -67,6 +67,8 @@ class AmazonAnthropicClaudeMessagesConfig(
DEFAULT_BEDROCK_ANTHROPIC_API_VERSION = "bedrock-2023-05-31"
WEBSEARCH_INTERCEPTION_DOCS_URL = "https://docs.litellm.ai/docs/integrations/websearch_interception"
@property
def custom_llm_provider(self) -> str | None:
return "bedrock"
@ -572,6 +574,45 @@ class AmazonAnthropicClaudeMessagesConfig(
return filtered_betas
@staticmethod
def _reject_unsupported_web_search_tools(anthropic_messages_request: dict[str, object], model: str) -> None:
"""
Bedrock's Anthropic endpoints cannot execute Anthropic's server-side
``web_search_*`` tool; forwarding it returns an opaque
"The provided request is not valid" 400 from Bedrock. Fail fast with an
error that names the problem and the fix instead.
When web search interception is enabled
(``litellm_settings.callbacks: ["websearch_interception"]``), the tool
is converted to a regular function tool before this transform runs, so
this guard never fires.
"""
from litellm.integrations.websearch_interception.tools import (
is_anthropic_native_web_search_tool,
)
tools: Final = anthropic_messages_request.get("tools")
if not isinstance(tools, list):
return
web_search_tool: Final = next(
(t for t in tools if isinstance(t, dict) and is_anthropic_native_web_search_tool(t)),
None,
)
if web_search_tool is None:
return
raise litellm.BadRequestError(
message=(
f"Bedrock does not support Anthropic's server-side web search tool "
f"(tool type '{web_search_tool.get('type')}', model '{model}'). "
"To use web search with this model, enable LiteLLM's web search interception "
"so the proxy executes the search instead: "
f"{AmazonAnthropicClaudeMessagesConfig.WEBSEARCH_INTERCEPTION_DOCS_URL}. "
"Alternatively, remove the web_search tool from the request."
),
model=model,
llm_provider="bedrock",
)
def _strip_unsupported_bedrock_invoke_fields(
self,
anthropic_messages_request: dict,
@ -630,6 +671,8 @@ class AmazonAnthropicClaudeMessagesConfig(
############## BEDROCK Invoke SPECIFIC TRANSFORMATION ###
#########################################################
self._reject_unsupported_web_search_tools(anthropic_messages_request=anthropic_messages_request, model=model)
# 1. anthropic_version is required for all claude models
if "anthropic_version" not in anthropic_messages_request:
anthropic_messages_request["anthropic_version"] = self.DEFAULT_BEDROCK_ANTHROPIC_API_VERSION

View file

@ -2580,3 +2580,52 @@ def test_replayed_intercepted_search_turn_leaves_no_unsupported_block_for_bedroc
assert "server_tool_use" not in serialized
assert expected_evidence in serialized
assert "Rome was founded in 753 BC." in serialized
@pytest.mark.parametrize("tool_type", ["web_search_20250305", "web_search_20260209"])
def test_bedrock_invoke_messages_rejects_server_web_search_tool(tool_type: str):
"""Bedrock can't execute Anthropic's server-side web search; the transform
must raise an actionable 400 pointing at the interception docs instead of
letting Bedrock return an opaque "provided request is not valid"."""
import litellm
from litellm.types.router import GenericLiteLLMParams
cfg = AmazonAnthropicClaudeMessagesConfig()
with pytest.raises(litellm.BadRequestError) as exc_info:
cfg.transform_anthropic_messages_request(
model="us.anthropic.claude-haiku-4-5-20251001-v1:0",
messages=[{"role": "user", "content": "search the web for litellm"}],
anthropic_messages_optional_request_params={
"max_tokens": 128,
"tools": [{"type": tool_type, "name": "web_search", "max_uses": 5}],
},
litellm_params=GenericLiteLLMParams(),
headers={},
)
assert "https://docs.litellm.ai/docs/integrations/websearch_interception" in str(exc_info.value)
assert "us.anthropic.claude-haiku-4-5-20251001-v1:0" in str(exc_info.value)
def test_bedrock_invoke_messages_allows_converted_websearch_function_tool():
"""The interception hook rewrites web_search into a plain custom tool
(litellm_web_search); that converted shape must pass through untouched."""
from litellm.types.router import GenericLiteLLMParams
cfg = AmazonAnthropicClaudeMessagesConfig()
result = cfg.transform_anthropic_messages_request(
model="us.anthropic.claude-haiku-4-5-20251001-v1:0",
messages=[{"role": "user", "content": "search the web for litellm"}],
anthropic_messages_optional_request_params={
"max_tokens": 128,
"tools": [
{
"name": "litellm_web_search",
"description": "Search the web",
"input_schema": {"type": "object", "properties": {"query": {"type": "string"}}},
}
],
},
litellm_params=GenericLiteLLMParams(),
headers={},
)
assert result["tools"][0]["name"] == "litellm_web_search"