From cdcf64725bff63e557ed328d11179e4a906ddd59 Mon Sep 17 00:00:00 2001 From: Denis Khachyan Date: Wed, 27 May 2026 17:10:42 +0300 Subject: [PATCH] fix: strip unsupported Anthropic built-in tools before forwarding to vLLM vLLM's /v1/messages requires input_schema on every tool, so Anthropic built-in tools like web_search, computer_use, text_editor, and bash cause 400 validation errors. Filter them out in transform_anthropic_messages_request. Co-Authored-By: Claude Opus 4.7 --- .../hosted_vllm/messages/transformation.py | 28 +++++++++++++ .../hosted_vllm/test_anthropic_passthrough.py | 39 +++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/litellm/llms/hosted_vllm/messages/transformation.py b/litellm/llms/hosted_vllm/messages/transformation.py index c93888b73aa..d03a37b28c1 100644 --- a/litellm/llms/hosted_vllm/messages/transformation.py +++ b/litellm/llms/hosted_vllm/messages/transformation.py @@ -102,6 +102,26 @@ class HostedVLLMAnthropicMessagesConfig(BaseAnthropicMessagesConfig): base = base[:-3] return f"{base}/v1/messages" + # Anthropic built-in tool types that vLLM does not support in /v1/messages + _UNSUPPORTED_TOOL_TYPES = frozenset({ + "web_search", + "computer", + "text_editor", + "bash", + }) + + def _strip_unsupported_tools(self, tools: Optional[List[Dict]]) -> Optional[List[Dict]]: + """Remove Anthropic built-in tools (web_search, computer_use, etc.) that + vLLM's /v1/messages endpoint does not understand.""" + if not tools: + return tools + return [ + t for t in tools + if not isinstance(t, dict) + or t.get("type", "") not in self._UNSUPPORTED_TOOL_TYPES + and not t.get("type", "").startswith(tuple(f"{p}_" for p in self._UNSUPPORTED_TOOL_TYPES)) + ] + def transform_anthropic_messages_request( self, model: str, @@ -114,6 +134,14 @@ class HostedVLLMAnthropicMessagesConfig(BaseAnthropicMessagesConfig): if max_tokens is None: raise ValueError("max_tokens is required for Anthropic /v1/messages API") + tools = self._strip_unsupported_tools( + anthropic_messages_optional_request_params.get("tools") + ) + if tools: + anthropic_messages_optional_request_params["tools"] = tools + else: + anthropic_messages_optional_request_params.pop("tools", None) + return { "model": model, "messages": messages, diff --git a/tests/litellm/llms/hosted_vllm/test_anthropic_passthrough.py b/tests/litellm/llms/hosted_vllm/test_anthropic_passthrough.py index ecc1766b33f..fc26e4102cb 100644 --- a/tests/litellm/llms/hosted_vllm/test_anthropic_passthrough.py +++ b/tests/litellm/llms/hosted_vllm/test_anthropic_passthrough.py @@ -148,6 +148,45 @@ class TestHostedVLLMConfig: assert "frequency_penalty" not in payload assert "presence_penalty" not in payload + def test_unsupported_tools_are_stripped(self): + """Anthropic built-in tools (web_search, computer_use, etc.) must be + removed because vLLM's /v1/messages requires input_schema on every tool.""" + messages = [{"role": "user", "content": "search the web"}] + tools = [ + {"type": "web_search_20250305", "name": "web_search", "max_uses": 8}, + {"type": "computer_20250124", "name": "computer"}, + {"type": "custom", "name": "my_tool", "input_schema": {"type": "object"}}, + ] + optional_params = {"max_tokens": 100, "tools": tools} + payload = self.config.transform_anthropic_messages_request( + model="qwen", + messages=messages, + anthropic_messages_optional_request_params=optional_params, + litellm_params=GenericLiteLLMParams(), + headers={}, + ) + # Only the custom tool survives + assert payload["tools"] == [ + {"type": "custom", "name": "my_tool", "input_schema": {"type": "object"}}, + ] + + def test_tools_key_removed_when_all_stripped(self): + """If all tools are unsupported, the tools key should be removed entirely.""" + messages = [{"role": "user", "content": "hi"}] + tools = [ + {"type": "web_search_20250305", "name": "web_search"}, + {"type": "bash_20250124", "name": "bash"}, + ] + optional_params = {"max_tokens": 100, "tools": tools} + payload = self.config.transform_anthropic_messages_request( + model="qwen", + messages=messages, + anthropic_messages_optional_request_params=optional_params, + litellm_params=GenericLiteLLMParams(), + headers={}, + ) + assert "tools" not in payload + def test_request_payload_missing_max_tokens_raises(self): with pytest.raises(ValueError, match="max_tokens is required"): self.config.transform_anthropic_messages_request(