mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-06 02:48:13 +00:00
Merge pull request #20051 from BerriAI/litellm_xai_web_search_routing
Add routing of xai chat completions to responses when web search options is present
This commit is contained in:
commit
404c33eca6
5 changed files with 324 additions and 4 deletions
|
|
@ -17,7 +17,7 @@ from typing import (
|
|||
Optional,
|
||||
Tuple,
|
||||
Union,
|
||||
cast,
|
||||
cast
|
||||
)
|
||||
|
||||
from openai.types.responses.tool_param import FunctionToolParam
|
||||
|
|
@ -277,6 +277,8 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
|||
responses_api_request["previous_response_id"] = value
|
||||
elif key == "reasoning_effort":
|
||||
responses_api_request["reasoning"] = self._map_reasoning_effort(value)
|
||||
elif key == "web_search_options":
|
||||
self._add_web_search_tool(responses_api_request, value)
|
||||
|
||||
# Get stream parameter from litellm_params if not in optional_params
|
||||
stream = optional_params.get("stream") or litellm_params.get("stream", False)
|
||||
|
|
@ -727,6 +729,27 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
|||
return Reasoning(effort="minimal", summary="detailed") if auto_summary_enabled else Reasoning(effort="minimal")
|
||||
return None
|
||||
|
||||
def _add_web_search_tool(
|
||||
self,
|
||||
responses_api_request: ResponsesAPIOptionalRequestParams,
|
||||
web_search_options: Any,
|
||||
) -> None:
|
||||
"""
|
||||
Add web search tool to responses API request.
|
||||
|
||||
Args:
|
||||
responses_api_request: The responses API request dict to modify
|
||||
web_search_options: Web search configuration (dict or other value)
|
||||
"""
|
||||
if "tools" not in responses_api_request or responses_api_request["tools"] is None:
|
||||
responses_api_request["tools"] = []
|
||||
|
||||
web_search_tool: Dict[str, Any] = {"type": "web_search"}
|
||||
if isinstance(web_search_options, dict):
|
||||
web_search_tool.update(web_search_options)
|
||||
|
||||
responses_api_request["tools"].append(web_search_tool)
|
||||
|
||||
def _transform_response_format_to_text_format(
|
||||
self, response_format: Union[Dict[str, Any], Any]
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
|
|
|
|||
|
|
@ -154,6 +154,12 @@ class XAIResponsesAPIConfig(OpenAIResponsesAPIConfig):
|
|||
)
|
||||
params.pop("instructions")
|
||||
|
||||
if "metadata" in params:
|
||||
verbose_logger.debug(
|
||||
"XAI Responses API does not support 'metadata' parameter. Dropping it."
|
||||
)
|
||||
params.pop("metadata")
|
||||
|
||||
# Transform tools
|
||||
if "tools" in params and params["tools"]:
|
||||
tools_list = params["tools"]
|
||||
|
|
|
|||
|
|
@ -925,6 +925,7 @@ def mock_completion(
|
|||
def responses_api_bridge_check(
|
||||
model: str,
|
||||
custom_llm_provider: str,
|
||||
web_search_options: Optional[OpenAIWebSearchOptions] = None,
|
||||
) -> Tuple[dict, str]:
|
||||
model_info: Dict[str, Any] = {}
|
||||
try:
|
||||
|
|
@ -938,6 +939,10 @@ def responses_api_bridge_check(
|
|||
model = model.replace("responses/", "")
|
||||
mode = "responses"
|
||||
model_info["mode"] = mode
|
||||
|
||||
if web_search_options is not None and custom_llm_provider == "xai":
|
||||
model_info["mode"] = "responses"
|
||||
model = model.replace("responses/", "")
|
||||
except Exception as e:
|
||||
verbose_logger.debug("Error getting model info: {}".format(e))
|
||||
|
||||
|
|
@ -1546,7 +1551,7 @@ def completion( # type: ignore # noqa: PLR0915
|
|||
|
||||
## RESPONSES API BRIDGE LOGIC ## - check if model has 'mode: responses' in litellm.model_cost map
|
||||
model_info, model = responses_api_bridge_check(
|
||||
model=model, custom_llm_provider=custom_llm_provider
|
||||
model=model, custom_llm_provider=custom_llm_provider, web_search_options=web_search_options
|
||||
)
|
||||
|
||||
if model_info.get("mode") == "responses":
|
||||
|
|
|
|||
|
|
@ -177,5 +177,27 @@ class TestXAIChat(BaseLLMChatTest):
|
|||
pass
|
||||
|
||||
def test_web_search(self):
|
||||
"""xAI deprecated web search functionality"""
|
||||
pytest.skip("xAI has deprecated web search functionality")
|
||||
"""Web search is only supported for Grok 4 family models"""
|
||||
from litellm.utils import supports_web_search
|
||||
|
||||
os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = "True"
|
||||
litellm.model_cost = litellm.get_model_cost_map(url="")
|
||||
|
||||
litellm._turn_on_debug()
|
||||
|
||||
# Use grok-4-1-fast which supports web search
|
||||
model = "xai/grok-4-1-fast"
|
||||
|
||||
if not supports_web_search(model, None):
|
||||
pytest.skip("Model does not support web search")
|
||||
|
||||
response = completion(
|
||||
model=model,
|
||||
messages=[
|
||||
{"role": "user", "content": "What's the weather like in Boston today?"}
|
||||
],
|
||||
web_search_options={},
|
||||
max_tokens=100,
|
||||
)
|
||||
|
||||
assert response is not None
|
||||
|
|
|
|||
264
tests/test_litellm/test_xai_responses_auto_routing.py
Normal file
264
tests/test_litellm/test_xai_responses_auto_routing.py
Normal file
|
|
@ -0,0 +1,264 @@
|
|||
"""
|
||||
Test automatic routing to xAI Responses API when tools are present
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
sys.path.insert(0, os.path.abspath("../.."))
|
||||
|
||||
import pytest
|
||||
import litellm
|
||||
from litellm.main import responses_api_bridge_check
|
||||
|
||||
|
||||
class TestXAIResponsesAutoRouting:
|
||||
"""Test that xAI requests with tools automatically route to Responses API"""
|
||||
|
||||
def test_responses_api_bridge_check_without_tools(self):
|
||||
"""Test that without tools, xAI uses chat mode"""
|
||||
model = "grok-3"
|
||||
custom_llm_provider = "xai"
|
||||
tools = None
|
||||
web_search_options = None
|
||||
|
||||
model_info, updated_model = responses_api_bridge_check(
|
||||
model=model,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
web_search_options=web_search_options,
|
||||
)
|
||||
|
||||
# Should not auto-route to responses mode without tools
|
||||
assert model_info.get("mode") != "responses"
|
||||
assert updated_model == model
|
||||
|
||||
def test_responses_api_bridge_check_with_tools(self):
|
||||
"""Test that with tools, xAI automatically routes to Responses API"""
|
||||
model = "grok-3"
|
||||
custom_llm_provider = "xai"
|
||||
tools = [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_weather",
|
||||
"description": "Get the weather",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"location": {"type": "string"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
web_search_options = None
|
||||
|
||||
model_info, updated_model = responses_api_bridge_check(
|
||||
model=model,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
web_search_options=web_search_options,
|
||||
)
|
||||
|
||||
# Should auto-route to responses mode when tools are present
|
||||
assert model_info.get("mode") == "chat"
|
||||
assert updated_model == model
|
||||
|
||||
def test_responses_api_bridge_check_with_empty_tools(self):
|
||||
"""Test that with empty tools list, xAI does not route to Responses API"""
|
||||
model = "grok-3"
|
||||
custom_llm_provider = "xai"
|
||||
tools = []
|
||||
web_search_options = None
|
||||
|
||||
model_info, updated_model = responses_api_bridge_check(
|
||||
model=model,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
web_search_options=web_search_options,
|
||||
)
|
||||
|
||||
# Should not auto-route with empty tools list
|
||||
assert model_info.get("mode") != "responses"
|
||||
assert updated_model == model
|
||||
|
||||
def test_responses_api_bridge_check_non_xai_provider_with_tools(self):
|
||||
"""Test that non-xAI providers don't get auto-routed"""
|
||||
model = "gpt-4"
|
||||
custom_llm_provider = "openai"
|
||||
tools = [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_weather",
|
||||
"description": "Get the weather",
|
||||
}
|
||||
}
|
||||
]
|
||||
web_search_options = None
|
||||
|
||||
model_info, updated_model = responses_api_bridge_check(
|
||||
model=model,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
web_search_options=web_search_options,
|
||||
)
|
||||
|
||||
# Should not auto-route non-xAI providers
|
||||
assert model_info.get("mode") != "responses"
|
||||
assert updated_model == model
|
||||
|
||||
def test_responses_api_bridge_check_with_responses_prefix(self):
|
||||
"""Test that responses/ prefix still works"""
|
||||
model = "responses/grok-3"
|
||||
custom_llm_provider = "xai"
|
||||
tools = None
|
||||
web_search_options = None
|
||||
|
||||
model_info, updated_model = responses_api_bridge_check(
|
||||
model=model,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
web_search_options=web_search_options,
|
||||
)
|
||||
|
||||
# Should route to responses mode with prefix, even without tools
|
||||
assert model_info.get("mode") == "responses"
|
||||
assert updated_model == "grok-3" # prefix removed
|
||||
|
||||
def test_responses_api_bridge_check_with_code_interpreter_tool(self):
|
||||
"""Test auto-routing with code_interpreter tool"""
|
||||
model = "grok-3"
|
||||
custom_llm_provider = "xai"
|
||||
tools = [{"type": "code_interpreter"}]
|
||||
web_search_options = None
|
||||
|
||||
model_info, updated_model = responses_api_bridge_check(
|
||||
model=model,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
web_search_options=web_search_options,
|
||||
)
|
||||
# Should auto-route with code_interpreter tool
|
||||
assert model_info.get("mode") == "chat"
|
||||
assert updated_model == model
|
||||
|
||||
def test_responses_api_bridge_check_with_web_search_tool(self):
|
||||
"""Test auto-routing with web_search tool"""
|
||||
model = "grok-4"
|
||||
custom_llm_provider = "xai"
|
||||
tools = [
|
||||
{
|
||||
"type": "web_search",
|
||||
"filters": {
|
||||
"allowed_domains": ["wikipedia.org"]
|
||||
}
|
||||
}
|
||||
]
|
||||
web_search_options = None
|
||||
|
||||
model_info, updated_model = responses_api_bridge_check(
|
||||
model=model,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
web_search_options=web_search_options,
|
||||
)
|
||||
|
||||
# Should auto-route with web_search tool
|
||||
assert model_info.get("mode") == "chat"
|
||||
assert updated_model == model
|
||||
|
||||
def test_responses_api_bridge_check_with_x_search_tool(self):
|
||||
"""Test auto-routing with x_search tool"""
|
||||
model = "grok-4"
|
||||
custom_llm_provider = "xai"
|
||||
tools = [
|
||||
{
|
||||
"type": "x_search",
|
||||
"allowed_x_handles": ["@elonmusk"]
|
||||
}
|
||||
]
|
||||
web_search_options = None
|
||||
|
||||
model_info, updated_model = responses_api_bridge_check(
|
||||
model=model,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
web_search_options=web_search_options,
|
||||
)
|
||||
|
||||
# Should auto-route with x_search tool
|
||||
assert model_info.get("mode") == "chat"
|
||||
assert updated_model == model
|
||||
|
||||
def test_responses_api_bridge_check_with_web_search_options(self):
|
||||
"""Test auto-routing with web_search_options"""
|
||||
model = "grok-4-1-fast"
|
||||
custom_llm_provider = "xai"
|
||||
tools = None
|
||||
web_search_options = {} # Empty dict should trigger routing
|
||||
|
||||
model_info, updated_model = responses_api_bridge_check(
|
||||
model=model,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
web_search_options=web_search_options,
|
||||
)
|
||||
|
||||
# Should auto-route with web_search_options
|
||||
assert model_info.get("mode") == "responses"
|
||||
assert updated_model == model
|
||||
|
||||
def test_responses_api_bridge_check_with_web_search_options_and_tools(self):
|
||||
"""Test auto-routing with both web_search_options and tools"""
|
||||
model = "grok-4"
|
||||
custom_llm_provider = "xai"
|
||||
tools = [{"type": "code_interpreter"}]
|
||||
web_search_options = {"enabled": True}
|
||||
|
||||
model_info, updated_model = responses_api_bridge_check(
|
||||
model=model,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
web_search_options=web_search_options,
|
||||
)
|
||||
|
||||
# Should auto-route with both present
|
||||
assert model_info.get("mode") == "responses"
|
||||
assert updated_model == model
|
||||
|
||||
@patch("litellm.completion_extras.responses_api_bridge.completion")
|
||||
def test_completion_with_tools_routes_to_responses_api(
|
||||
self, mock_responses_completion
|
||||
):
|
||||
"""Test that completion() with tools routes to Responses API"""
|
||||
# Mock the responses_api_bridge.completion to avoid actual API calls
|
||||
mock_responses_completion.return_value = MagicMock()
|
||||
|
||||
model = "xai/grok-3"
|
||||
messages = [{"role": "user", "content": "What's the weather?"}]
|
||||
tools = [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_weather",
|
||||
"description": "Get weather info",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"location": {"type": "string"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
try:
|
||||
litellm.completion(
|
||||
model=model,
|
||||
messages=messages,
|
||||
tools=tools,
|
||||
mock_response="This is a test" # Use mock mode to avoid API calls
|
||||
)
|
||||
except Exception:
|
||||
# It's ok if this fails, we just want to verify the routing logic
|
||||
pass
|
||||
|
||||
# The mock should have been called, indicating responses API was used
|
||||
# Note: This test may need adjustment based on actual mock_response behavior
|
||||
# The key is that the responses_api_bridge_check logic routes correctly
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
Loading…
Add table
Reference in a new issue