mirror of
https://github.com/tiennm99/litellm.git
synced 2026-07-29 08:21:22 +00:00
Merge pull request #21465 from BerriAI/litellm_map_anthropi_web_search_to_chat
Add mapping for websearch from v1/messages to chat/completions
This commit is contained in:
@@ -299,6 +299,26 @@ class LiteLLMAnthropicMessagesAdapter:
|
||||
"""
|
||||
return ["messages", "metadata", "system", "tool_choice", "tools", "thinking", "output_format"]
|
||||
|
||||
def _is_web_search_tool(self, tool: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
Check if a tool is an Anthropic web search tool.
|
||||
|
||||
Anthropic web search tools have:
|
||||
- type starting with "web_search" (e.g., "web_search_20260209")
|
||||
- name = "web_search"
|
||||
|
||||
Args:
|
||||
tool: Tool definition dict
|
||||
|
||||
Returns:
|
||||
True if this is a web search tool
|
||||
"""
|
||||
tool_type = tool.get("type", "")
|
||||
tool_name = tool.get("name", "")
|
||||
return (
|
||||
isinstance(tool_type, str) and tool_type.startswith("web_search")
|
||||
) or tool_name == "web_search"
|
||||
|
||||
def translate_anthropic_messages_to_openai( # noqa: PLR0915
|
||||
self,
|
||||
messages: List[
|
||||
@@ -872,10 +892,25 @@ class LiteLLMAnthropicMessagesAdapter:
|
||||
if "tools" in anthropic_message_request:
|
||||
tools = anthropic_message_request["tools"]
|
||||
if tools:
|
||||
new_kwargs["tools"], tool_name_mapping = self.translate_anthropic_tools_to_openai(
|
||||
tools=cast(List[AllAnthropicToolsValues], tools),
|
||||
model=new_kwargs.get("model"),
|
||||
)
|
||||
# Separate web search tools from regular tools
|
||||
web_search_tools = []
|
||||
regular_tools = []
|
||||
for tool in tools:
|
||||
if self._is_web_search_tool(cast(Dict[str, Any], tool)):
|
||||
web_search_tools.append(tool)
|
||||
else:
|
||||
regular_tools.append(tool)
|
||||
|
||||
# If web search tools are present, add web_search_options parameter
|
||||
if web_search_tools:
|
||||
new_kwargs["web_search_options"] = {} # type: ignore
|
||||
|
||||
# Only translate regular tools (non-web-search)
|
||||
if regular_tools:
|
||||
new_kwargs["tools"], tool_name_mapping = self.translate_anthropic_tools_to_openai(
|
||||
tools=cast(List[AllAnthropicToolsValues], regular_tools),
|
||||
model=new_kwargs.get("model"),
|
||||
)
|
||||
|
||||
## CONVERT THINKING
|
||||
if "thinking" in anthropic_message_request:
|
||||
|
||||
@@ -1072,7 +1072,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
|
||||
elif param == "modalities" and isinstance(value, list):
|
||||
response_modalities = self.map_response_modalities(value)
|
||||
optional_params["responseModalities"] = response_modalities
|
||||
elif param == "web_search_options" and value and isinstance(value, dict):
|
||||
elif param == "web_search_options" and isinstance(value, dict):
|
||||
_tools = self._map_web_search_options(value)
|
||||
optional_params = self._add_tools_to_optional_params(
|
||||
optional_params, [_tools]
|
||||
|
||||
+128
@@ -1811,3 +1811,131 @@ def test_translate_openai_response_to_anthropic_input_tokens_no_cache():
|
||||
# Validate: input_tokens should equal prompt_tokens when no caching
|
||||
assert anthropic_response["usage"]["input_tokens"] == 100
|
||||
assert anthropic_response["usage"]["output_tokens"] == 50
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# Web Search Tool Transformation Tests
|
||||
# =====================================================================
|
||||
|
||||
|
||||
def test_is_web_search_tool():
|
||||
"""Test detection of Anthropic web search tools."""
|
||||
adapter = LiteLLMAnthropicMessagesAdapter()
|
||||
|
||||
# Tool with type starting with "web_search" should be detected
|
||||
web_search_tool_with_type = {
|
||||
"type": "web_search_20260209",
|
||||
"name": "web_search",
|
||||
}
|
||||
assert adapter._is_web_search_tool(web_search_tool_with_type) is True
|
||||
|
||||
# Tool with name "web_search" should be detected
|
||||
web_search_tool_with_name = {
|
||||
"name": "web_search",
|
||||
}
|
||||
assert adapter._is_web_search_tool(web_search_tool_with_name) is True
|
||||
|
||||
# Regular function tool should not be detected
|
||||
regular_tool = {
|
||||
"name": "get_weather",
|
||||
"description": "Get weather info",
|
||||
"input_schema": {"type": "object"},
|
||||
}
|
||||
assert adapter._is_web_search_tool(regular_tool) is False
|
||||
|
||||
|
||||
def test_translate_anthropic_to_openai_with_web_search_tool():
|
||||
"""
|
||||
Test that Anthropic web search tools are converted to web_search_options parameter.
|
||||
|
||||
When a user sends an Anthropic /v1/messages request with {"type": "web_search_20260209"}
|
||||
tool, it should be transformed to OpenAI format with web_search_options: {} parameter.
|
||||
"""
|
||||
from litellm.types.llms.anthropic import AnthropicMessagesRequest
|
||||
|
||||
anthropic_request = AnthropicMessagesRequest(
|
||||
model="gemini-2.5-flash-lite",
|
||||
max_tokens=4096,
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Search for the current prices of AAPL and GOOGL",
|
||||
}
|
||||
],
|
||||
tools=[
|
||||
{
|
||||
"type": "web_search_20260209",
|
||||
"name": "web_search",
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
adapter = LiteLLMAnthropicMessagesAdapter()
|
||||
openai_request, tool_name_mapping = adapter.translate_anthropic_to_openai(
|
||||
anthropic_message_request=anthropic_request
|
||||
)
|
||||
|
||||
# web_search_options should be added
|
||||
assert "web_search_options" in openai_request
|
||||
assert openai_request["web_search_options"] == {}
|
||||
|
||||
# web search tool should NOT be in the tools array
|
||||
assert "tools" not in openai_request or openai_request.get("tools") == []
|
||||
|
||||
# tool_name_mapping should be empty since no regular tools were present
|
||||
assert tool_name_mapping == {}
|
||||
|
||||
|
||||
def test_translate_anthropic_to_openai_with_mixed_tools():
|
||||
"""
|
||||
Test that web search tools are separated from regular tools.
|
||||
|
||||
When a request has both web search tools and regular function tools,
|
||||
only the regular tools should be in the tools array, and web_search_options
|
||||
should be added.
|
||||
"""
|
||||
from litellm.types.llms.anthropic import AnthropicMessagesRequest
|
||||
|
||||
anthropic_request = AnthropicMessagesRequest(
|
||||
model="gemini-2.5-flash-lite",
|
||||
max_tokens=4096,
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Get weather and search the web",
|
||||
}
|
||||
],
|
||||
tools=[
|
||||
{
|
||||
"type": "web_search_20260209",
|
||||
"name": "web_search",
|
||||
},
|
||||
{
|
||||
"name": "get_weather",
|
||||
"description": "Get weather information",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"location": {"type": "string"}
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
adapter = LiteLLMAnthropicMessagesAdapter()
|
||||
openai_request, tool_name_mapping = adapter.translate_anthropic_to_openai(
|
||||
anthropic_message_request=anthropic_request
|
||||
)
|
||||
|
||||
# web_search_options should be added
|
||||
assert "web_search_options" in openai_request
|
||||
assert openai_request["web_search_options"] == {}
|
||||
|
||||
# Only get_weather tool should be in the tools array
|
||||
assert "tools" in openai_request
|
||||
assert len(openai_request["tools"]) == 1
|
||||
assert openai_request["tools"][0]["function"]["name"] == "get_weather"
|
||||
|
||||
# tool_name_mapping should be empty for short tool names
|
||||
assert tool_name_mapping == {}
|
||||
|
||||
@@ -3224,6 +3224,7 @@ def test_video_metadata_only_for_gemini_3():
|
||||
def test_chunk_parser_handles_prompt_feedback_block():
|
||||
"""Test chunk_parser correctly handles promptFeedback.blockReason"""
|
||||
from unittest.mock import Mock
|
||||
|
||||
from litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini import (
|
||||
ModelResponseIterator,
|
||||
)
|
||||
@@ -3260,6 +3261,7 @@ def test_chunk_parser_handles_prompt_feedback_block():
|
||||
def test_chunk_parser_handles_prompt_feedback_safety_block():
|
||||
"""Test chunk_parser handles different blockReason types (SAFETY)"""
|
||||
from unittest.mock import Mock
|
||||
|
||||
from litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini import (
|
||||
ModelResponseIterator,
|
||||
)
|
||||
@@ -3294,6 +3296,7 @@ def test_chunk_parser_handles_prompt_feedback_safety_block():
|
||||
def test_chunk_parser_handles_prompt_feedback_block_with_usage():
|
||||
"""Test chunk_parser correctly extracts usageMetadata when promptFeedback.blockReason is present"""
|
||||
from unittest.mock import Mock
|
||||
|
||||
from litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini import (
|
||||
ModelResponseIterator,
|
||||
)
|
||||
@@ -3429,3 +3432,80 @@ def test_vertex_ai_traffic_type_surfaced_in_responses_api():
|
||||
|
||||
assert responses_api_response.provider_specific_fields["traffic_type"] == "ON_DEMAND"
|
||||
|
||||
|
||||
def test_vertex_ai_web_search_options_parameter():
|
||||
"""
|
||||
Test that web_search_options parameter is transformed to googleSearch tool.
|
||||
|
||||
When a user provides web_search_options as a parameter (not as a tool in the tools array),
|
||||
it should be transformed to Gemini's googleSearch tool.
|
||||
|
||||
This is important for the /v1/messages -> chat/completions -> Gemini flow:
|
||||
- Anthropic web search tool -> web_search_options parameter -> Gemini googleSearch tool
|
||||
|
||||
Input (optional_params):
|
||||
{"web_search_options": {}}
|
||||
|
||||
Expected Output:
|
||||
tools=[{"googleSearch": {}}]
|
||||
"""
|
||||
from litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini import (
|
||||
VertexGeminiConfig,
|
||||
)
|
||||
|
||||
v = VertexGeminiConfig()
|
||||
|
||||
# Simulate the map_openai_params flow
|
||||
optional_params = {}
|
||||
|
||||
# When web_search_options is present, it should be mapped to a tool
|
||||
web_search_options = {}
|
||||
_tools = v._map_web_search_options(web_search_options)
|
||||
|
||||
# Verify the tool is a googleSearch tool
|
||||
assert "googleSearch" in _tools, f"Expected googleSearch in tool, got {_tools.keys()}"
|
||||
assert _tools["googleSearch"] == {}, f"Expected empty googleSearch config, got {_tools['googleSearch']}"
|
||||
|
||||
|
||||
def test_vertex_ai_web_search_options_in_map_openai_params():
|
||||
"""
|
||||
Test that web_search_options is properly handled in map_openai_params.
|
||||
|
||||
This tests the full flow where web_search_options parameter is converted
|
||||
to a googleSearch tool and added to optional_params.
|
||||
|
||||
Input:
|
||||
optional_params with web_search_options: {}
|
||||
|
||||
Expected:
|
||||
optional_params should have tools with googleSearch
|
||||
"""
|
||||
from litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini import (
|
||||
VertexGeminiConfig,
|
||||
)
|
||||
|
||||
v = VertexGeminiConfig()
|
||||
|
||||
# Simulate optional_params passed to map_openai_params
|
||||
optional_params = {
|
||||
"web_search_options": {}
|
||||
}
|
||||
|
||||
# Call the transformation that happens in map_openai_params
|
||||
# Lines 1075-1079 in vertex_and_google_ai_studio_gemini.py (after fix)
|
||||
web_search_value = optional_params.get("web_search_options")
|
||||
if isinstance(web_search_value, dict): # Fixed: removed 'value and' check to support empty dicts
|
||||
_tools = v._map_web_search_options(web_search_value)
|
||||
# Simulate _add_tools_to_optional_params
|
||||
optional_params = v._add_tools_to_optional_params(optional_params, [_tools])
|
||||
|
||||
# Remove web_search_options as it's been transformed
|
||||
optional_params.pop("web_search_options", None)
|
||||
|
||||
# Verify the transformation
|
||||
assert "tools" in optional_params, "tools should be added to optional_params"
|
||||
assert len(optional_params["tools"]) == 1, "Should have exactly one tool"
|
||||
assert "googleSearch" in optional_params["tools"][0], "Tool should be googleSearch"
|
||||
assert optional_params["tools"][0]["googleSearch"] == {}, "googleSearch should be empty config"
|
||||
assert "web_search_options" not in optional_params, "web_search_options should be removed after transformation"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user