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"]