Add mapping for websearch from v1/messages to chat/completions

This commit is contained in:
Sameer Kankute
2026-02-18 13:32:25 +05:30
parent 2517c069ca
commit 6b26b47cd4
4 changed files with 248 additions and 5 deletions
@@ -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]
@@ -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"