diff --git a/litellm/llms/anthropic/experimental_pass_through/messages/handler.py b/litellm/llms/anthropic/experimental_pass_through/messages/handler.py index 1a4144de39e..69bc671effd 100644 --- a/litellm/llms/anthropic/experimental_pass_through/messages/handler.py +++ b/litellm/llms/anthropic/experimental_pass_through/messages/handler.py @@ -51,14 +51,23 @@ from .utils import AnthropicMessagesRequestUtils, mock_response _RESPONSES_API_PROVIDERS = frozenset({"openai"}) -def _should_route_to_responses_api(custom_llm_provider: Optional[str]) -> bool: +def _should_route_to_responses_api( + custom_llm_provider: Optional[str], + model_info: Optional[Dict] = None, +) -> bool: """Return True when the provider should use the Responses API path. + Routes to the Responses API when: + - The provider is in _RESPONSES_API_PROVIDERS (e.g. openai), OR + - The model has mode=responses in model_info (e.g. github_copilot GPT models) + Set ``litellm.use_chat_completions_url_for_anthropic_messages = True`` to - opt out and route OpenAI/Azure requests through chat/completions instead. + opt out and route all requests through chat/completions instead. """ if litellm.use_chat_completions_url_for_anthropic_messages: return False + if model_info and model_info.get("mode") == "responses": + return True return custom_llm_provider in _RESPONSES_API_PROVIDERS @@ -558,7 +567,7 @@ def anthropic_messages_handler( custom_llm_provider=custom_llm_provider, **kwargs, ) - if _should_route_to_responses_api(custom_llm_provider): + if _should_route_to_responses_api(custom_llm_provider, kwargs.get("model_info")): return LiteLLMMessagesToResponsesAPIHandler.anthropic_messages_handler(**_shared_kwargs) # The in-gateway context_management polyfill runs inside diff --git a/litellm/llms/anthropic/experimental_pass_through/responses_adapters/transformation.py b/litellm/llms/anthropic/experimental_pass_through/responses_adapters/transformation.py index 172e54de98e..7950d918866 100644 --- a/litellm/llms/anthropic/experimental_pass_through/responses_adapters/transformation.py +++ b/litellm/llms/anthropic/experimental_pass_through/responses_adapters/transformation.py @@ -185,7 +185,11 @@ class LiteLLMAnthropicToResponsesAPIAdapter: tool_name = tool_dict.get("name", "") # web_search tool if (isinstance(tool_type, str) and tool_type.startswith("web_search")) or tool_name == "web_search": - result.append({"type": "web_search_preview"}) + web_tool: Dict[str, Any] = {"type": "web_search"} + allowed = tool_dict.get("allowed_domains") + if allowed: + web_tool["filters"] = {"allowed_domains": allowed} + result.append(web_tool) continue func_tool: Dict[str, Any] = {"type": "function", "name": tool_name} if "description" in tool_dict: @@ -204,7 +208,11 @@ class LiteLLMAnthropicToResponsesAPIAdapter: if tc_type == "any": return "required" elif tc_type == "tool": - return {"type": "function", "name": tool_choice.get("name", "")} + name = tool_choice.get("name", "") + # web_search is a hosted tool, not a function + if name == "web_search": + return {"type": "web_search"} + return {"type": "function", "name": name} elif tc_type == "none": return "none" return "auto" diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/responses_adapters/test_responses_adapters_transformation.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/responses_adapters/test_responses_adapters_transformation.py index 606ff39b35e..d5ea8650fc6 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/responses_adapters/test_responses_adapters_transformation.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/responses_adapters/test_responses_adapters_transformation.py @@ -527,16 +527,16 @@ class TestTranslateToolsToResponsesAPI: assert "parameters" not in result[0] def test_web_search_tool_by_name(self): - """Tool named 'web_search' maps to web_search_preview.""" + """Tool named 'web_search' maps to web_search.""" tools = [{"name": "web_search", "type": "custom"}] result = _ADAPTER.translate_tools_to_responses_api(tools) # type: ignore[arg-type] - assert result == [{"type": "web_search_preview"}] + assert result == [{"type": "web_search"}] def test_web_search_tool_by_type_prefix(self): - """Tool with type starting with 'web_search' maps to web_search_preview.""" + """Tool with type starting with 'web_search' maps to web_search.""" tools = [{"name": "search", "type": "web_search_20250305"}] result = _ADAPTER.translate_tools_to_responses_api(tools) # type: ignore[arg-type] - assert result == [{"type": "web_search_preview"}] + assert result == [{"type": "web_search"}] def test_multiple_tools_order_preserved(self): """Multiple tools are converted in order.""" @@ -548,7 +548,7 @@ class TestTranslateToolsToResponsesAPI: result = _ADAPTER.translate_tools_to_responses_api(tools) # type: ignore[arg-type] assert len(result) == 3 assert result[0]["name"] == "tool_a" - assert result[1] == {"type": "web_search_preview"} + assert result[1] == {"type": "web_search"} assert result[2]["name"] == "tool_b" def test_empty_tools_list(self):