From 2f5e4c5ae452c362d21ca3b7a7c20d4371f63c41 Mon Sep 17 00:00:00 2001 From: Yassin Kortam Date: Mon, 10 Aug 2026 16:07:11 -0700 Subject: [PATCH] fix(bedrock): explain why Anthropic's web_search server tool is rejected Bedrock hosts none of Anthropic's web_search server tools, so forwarding one earns an opaque "The provided request is not valid" from AWS that names neither the tool nor a way forward. Every Claude model on Bedrock fails the same way, which reads as a broken deployment rather than an unsupported capability. Reject it in the Bedrock Invoke messages transform with a message naming the tool and the web-search interception settings that do serve it. Interception rewrites the tool into LiteLLM's own search tool before this point, so the guard only fires when it is not configured and the working path is untouched. Resolves LIT-5391 --- .../anthropic_claude3_transformation.py | 40 ++++++++++++++- .../test_anthropic_claude3_transformation.py | 49 +++++++++++++++++++ 2 files changed, 88 insertions(+), 1 deletion(-) 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..6550cac9da1 100644 --- a/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py +++ b/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py @@ -1,4 +1,4 @@ -from collections.abc import AsyncIterator +from collections.abc import AsyncIterator, Mapping from typing import TYPE_CHECKING, Any, Final, cast import httpx @@ -56,6 +56,9 @@ else: LiteLLMLoggingObj = Any +UNSUPPORTED_BEDROCK_SERVER_TOOL_PREFIX: Final = "web_search_" + + class AmazonAnthropicClaudeMessagesConfig( AnthropicMessagesConfig, AmazonInvokeConfig, @@ -605,6 +608,39 @@ class AmazonAnthropicClaudeMessagesConfig( normalize_bedrock_opus_output_config_effort(model=model, output_config=clamped) optional_params["reasoning_effort"] = clamped["effort"] + def _reject_unsupported_server_tools(self, anthropic_messages_request: Mapping[str, object], model: str) -> None: + """Bedrock hosts none of Anthropic's ``web_search_*`` server tools, so + forwarding one earns an opaque ``The provided request is not valid`` from + AWS that names neither the tool nor a way forward. Web-search + interception rewrites the tool before this point, so reaching here means + it is not configured for this request. + """ + tools: Final = anthropic_messages_request.get("tools") + if not isinstance(tools, list): + return + entries: Final = cast(list[Mapping[str, object]], tools) # cast-ok: request body is untyped upstream + unsupported: Final = sorted( + { + tool_type + for tool in entries + if isinstance(tool_type := tool.get("type"), str) + if tool_type.startswith(UNSUPPORTED_BEDROCK_SERVER_TOOL_PREFIX) + } + ) + if not unsupported: + return + raise litellm.BadRequestError( + message=( + f"Amazon Bedrock does not support the Anthropic server tool(s) {unsupported}. " + "Enable LiteLLM's web-search interception to serve them, by adding " + '`websearch_interception` to `litellm_settings.callbacks`, listing "bedrock" under ' + "`litellm_settings.websearch_interception_params.enabled_providers`, and declaring a " + "`search_tools` entry. Otherwise remove the tool from the request." + ), + model=model, + llm_provider="bedrock", + ) + def transform_anthropic_messages_request( self, model: str, @@ -626,6 +662,8 @@ class AmazonAnthropicClaudeMessagesConfig( headers=headers, ) self._normalize_system_role_messages(anthropic_messages_request, model=model) + request_view: Final = cast(Mapping[str, object], anthropic_messages_request) # cast-ok: body is untyped here + self._reject_unsupported_server_tools(request_view, model=model) ######################################################### ############## BEDROCK Invoke SPECIFIC TRANSFORMATION ### ######################################################### 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..d0dc5fcb804 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 @@ -29,6 +29,8 @@ from litellm.llms.bedrock.messages.invoke_transformations.anthropic_claude3_tran AmazonAnthropicClaudeMessagesConfig, AmazonAnthropicClaudeMessagesStreamDecoder, ) +import litellm +from litellm.types.router import GenericLiteLLMParams @pytest.fixture @@ -2580,3 +2582,50 @@ 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 + + +def test_bedrock_messages_rejects_anthropic_web_search_server_tool(): + """Bedrock hosts none of Anthropic's web_search server tools. Forwarding one + earns an opaque 'The provided request is not valid' from AWS that names + neither the tool nor a way forward, so reject it here with a message that + does both.""" + with pytest.raises(litellm.BadRequestError) as excinfo: + AmazonAnthropicClaudeMessagesConfig().transform_anthropic_messages_request( + model="us.anthropic.claude-haiku-4-5-20251001-v1:0", + messages=[{"role": "user", "content": "Anthropic Claude news"}], + anthropic_messages_optional_request_params={ + "max_tokens": 64, + "tools": [{"type": "web_search_20250305", "name": "web_search", "max_uses": 3}], + }, + litellm_params=GenericLiteLLMParams(), + headers={}, + ) + + message = str(excinfo.value) + assert "web_search_20250305" in message + assert "websearch_interception" in message + + +def test_bedrock_messages_allows_the_tool_interception_rewrites_it_into(): + """Web-search interception rewrites the native tool into LiteLLM's own search + tool before this transform runs, and that rewritten tool carries an + input_schema like any custom tool. Rejecting the native type must not touch + it, or enabling interception would break the very path that serves search.""" + body = AmazonAnthropicClaudeMessagesConfig().transform_anthropic_messages_request( + model="us.anthropic.claude-haiku-4-5-20251001-v1:0", + messages=[{"role": "user", "content": "Anthropic Claude news"}], + anthropic_messages_optional_request_params={ + "max_tokens": 64, + "tools": [ + { + "name": "litellm_web_search", + "description": "Search the web", + "input_schema": {"type": "object", "properties": {"query": {"type": "string"}}}, + } + ], + }, + litellm_params=GenericLiteLLMParams(), + headers={}, + ) + + assert [tool["name"] for tool in body["tools"]] == ["litellm_web_search"]