mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-03 02:22:24 +00:00
* ci: run the unit_selection.sh shard files on every event instead of only fork pull requests Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * ci: rename fork-flag to unit-flag now that it applies on every event * test: move tests/test_litellm root and small trees into tests/unit Pure renames, no content changes. Follow-up commits in this PR fix references, merge the three files that already existed in tests/unit, keep live-provider tests in tests/test_litellm and wire CI. * test: carry tests/test_litellm conftest isolation into tests/unit Callback lists, routing fallbacks, cached HTTP clients, logger state, AWS, proxy-URL and keychain env, and session-end client cleanup now reset for unit tests too. The environment isolation owns its MonkeyPatch so a test's own monkeypatch is undone before the model-cost teardown runs. * test: merge, split and prune the moved root and small-tree tests Merge batches/test_batch_utils.py and the chat_completions and messages dispatch tests into the files that already existed in tests/unit. Keep the live Gemini interactions tests, the async image-fetch format test and the OpenAI embedding scorer test in tests/test_litellm since they need real network or keys. Put test_router.py under tests/unit/test_router so the existing package no longer shadows it. Delete eight tests the audit found superseded by stronger ones kept in this move. * ci: run the moved root and small-tree tests under their legacy flags Add the misc and responses-caching-types flags to unit_selection.sh and CircleCI, extend enterprise-routing and mcp-integration, and point the legacy GHA shards, Makefile, redis-compat workflow, merge smoke manifest and change classifier at the new paths. * test: make the new tests/unit directories packages tests/unit/test_package_layout.py requires every directory to carry an __init__.py, and without one the moved and retained test_litellm_responses_bridge.py modules collide on import. * test: scope the unit socket block to tests/unit in shared sessions The GHA shards collect the legacy test-path and the unit selection in one pytest session. The unit conftest's loopback-only block leaked into legacy modules that reach the network at import. The legacy conftest now lifts the restriction at collect and setup time, and the unit conftest re-applies it when collecting its own modules. * test: give the shard-script tests their own GITHUB_OUTPUT They only passed where the runner set it. The CircleCI unit job's env allowlist drops it, so the script's redirect failed there. * test: point the router and module-deletion checks at tests/unit router_code_coverage and code_qa_check_tests only searched tests/test_litellm, so the moved router tests no longer counted. The two silent-experiment tests the audit deleted were the only direct callers of those methods; they are replaced with tests that assert the forwarded shadow request and the recursion guard. --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
236 lines
8.4 KiB
Python
236 lines
8.4 KiB
Python
"""
|
|
Test automatic routing to xAI Responses API when tools are present
|
|
"""
|
|
|
|
import json
|
|
from collections.abc import Mapping
|
|
from typing import Final
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import httpx
|
|
import pytest
|
|
import litellm
|
|
from litellm.llms.custom_httpx.http_handler import HTTPHandler
|
|
from litellm.main import responses_api_bridge_check
|
|
|
|
|
|
class _RecordingResponsesHandler:
|
|
"""MockTransport handler that serves a canned /responses reply and keeps the body xAI would have received"""
|
|
|
|
def __init__(self, reply: Mapping[str, object]) -> None:
|
|
self.reply: Final = reply
|
|
self.request_body: Mapping[str, object] | None = None
|
|
|
|
def __call__(self, request: httpx.Request) -> httpx.Response:
|
|
self.request_body = json.loads(request.content)
|
|
return httpx.Response(200, json=dict(self.reply), request=request)
|
|
|
|
|
|
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_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_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
|
|
|
|
def test_responses_api_bridge_check_with_web_search_options_on_unmapped_model(self):
|
|
"""web search must reach /responses even for a model missing from the cost map, chat returns 410"""
|
|
model_info, updated_model = responses_api_bridge_check(
|
|
model="grok-not-in-cost-map",
|
|
custom_llm_provider="xai",
|
|
web_search_options={"search_context_size": "medium"},
|
|
)
|
|
|
|
assert model_info.get("mode") == "responses"
|
|
assert updated_model == "grok-not-in-cost-map"
|
|
|
|
@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
|
|
|
|
def test_system_message_survives_web_search_bridge(self):
|
|
"""A system message becomes 'instructions' on the bridged /responses call, and xAI accepts it"""
|
|
handler: Final = _RecordingResponsesHandler(
|
|
reply={
|
|
"id": "resp_test",
|
|
"object": "response",
|
|
"created_at": 0,
|
|
"status": "completed",
|
|
"model": "grok-4.6",
|
|
"output": [
|
|
{
|
|
"type": "message",
|
|
"id": "msg_test",
|
|
"status": "completed",
|
|
"role": "assistant",
|
|
"content": [{"type": "output_text", "text": "1.0.0", "annotations": []}],
|
|
}
|
|
],
|
|
"usage": {"input_tokens": 1, "output_tokens": 1, "total_tokens": 2},
|
|
}
|
|
)
|
|
|
|
response: Final = litellm.completion(
|
|
model="xai/grok-4.6",
|
|
messages=[
|
|
{"role": "system", "content": "Answer briefly."},
|
|
{"role": "user", "content": "newest litellm version?"},
|
|
],
|
|
web_search_options={"search_context_size": "medium"},
|
|
api_key="fake-key",
|
|
client=HTTPHandler(client=httpx.Client(transport=httpx.MockTransport(handler))),
|
|
)
|
|
|
|
assert response.choices[0].message.content == "1.0.0"
|
|
assert handler.request_body is not None
|
|
assert handler.request_body["instructions"] == "Answer briefly."
|
|
assert handler.request_body["tools"] == [{"type": "web_search"}]
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-v"])
|