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
This commit is contained in:
Yassin Kortam 2026-08-10 16:07:11 -07:00
parent 9de3315dad
commit 2f5e4c5ae4
2 changed files with 88 additions and 1 deletions

View file

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

View file

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