From 20354bfcdcdd0130d257c844c4368d37a4be8990 Mon Sep 17 00:00:00 2001 From: ryan-crabbe-berri Date: Mon, 10 Aug 2026 15:59:18 -0700 Subject: [PATCH] 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 --- .../anthropic_claude3_transformation.py | 43 ++++++++++++++++ .../test_anthropic_claude3_transformation.py | 49 +++++++++++++++++++ 2 files changed, 92 insertions(+) diff --git a/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py b/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py index 85fda3a6522..fad7e7558c2 100644 --- a/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py +++ b/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py @@ -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 diff --git a/tests/test_litellm/llms/bedrock/messages/invoke_transformations/test_anthropic_claude3_transformation.py b/tests/test_litellm/llms/bedrock/messages/invoke_transformations/test_anthropic_claude3_transformation.py index 76bb11cc26d..2d1a5bc4c2a 100644 --- a/tests/test_litellm/llms/bedrock/messages/invoke_transformations/test_anthropic_claude3_transformation.py +++ b/tests/test_litellm/llms/bedrock/messages/invoke_transformations/test_anthropic_claude3_transformation.py @@ -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"