mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-05 08:07:05 +00:00
fix: resolve Python 3.14 OCR annotations and remaining matrix failures
This commit is contained in:
parent
729ec4c7b8
commit
cfcaaa03d6
15 changed files with 134 additions and 76 deletions
|
|
@ -45,7 +45,7 @@
|
|||
"limit": 25
|
||||
},
|
||||
"reportInvalidTypeForm": {
|
||||
"limit": 34
|
||||
"limit": 32
|
||||
},
|
||||
"reportInvalidTypeVarUse": {
|
||||
"limit": 2
|
||||
|
|
@ -105,13 +105,13 @@
|
|||
"limit": 109
|
||||
},
|
||||
"reportUnknownMemberType": {
|
||||
"limit": 38350
|
||||
"limit": 38348
|
||||
},
|
||||
"reportUnknownParameterType": {
|
||||
"limit": 19626
|
||||
"limit": 19624
|
||||
},
|
||||
"reportUnknownVariableType": {
|
||||
"limit": 29890
|
||||
"limit": 29889
|
||||
},
|
||||
"reportUnnecessaryCast": {
|
||||
"limit": 111
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
Base OCR transformation configuration.
|
||||
"""
|
||||
|
||||
import builtins
|
||||
from collections.abc import Mapping
|
||||
from typing import TYPE_CHECKING, Any, Final, Literal
|
||||
|
||||
|
|
@ -93,8 +94,8 @@ class OCRResponse(LiteLLMPydanticObjectBase):
|
|||
document_annotation: Any | None = None
|
||||
usage_info: OCRUsageInfo | None = None
|
||||
content: str | None = None
|
||||
tables: list[dict[str, object]] | None = None
|
||||
keyValuePairs: list[dict[str, object]] | None = None
|
||||
tables: list[dict[str, builtins.object]] | None = None
|
||||
keyValuePairs: list[dict[str, builtins.object]] | None = None
|
||||
object: str = "ocr"
|
||||
|
||||
model_config = {"extra": "allow"}
|
||||
|
|
@ -102,11 +103,11 @@ class OCRResponse(LiteLLMPydanticObjectBase):
|
|||
# Define private attributes using PrivateAttr
|
||||
_hidden_params: dict = PrivateAttr(default_factory=dict)
|
||||
|
||||
def set_provider_native_response(self, native_response: Mapping[str, object]) -> None:
|
||||
def set_provider_native_response(self, native_response: Mapping[str, builtins.object]) -> None:
|
||||
"""Keep the provider's own response payload alongside the normalized one."""
|
||||
self._hidden_params[PROVIDER_NATIVE_RESPONSE_KEY] = native_response
|
||||
|
||||
def get_provider_native_response(self) -> Mapping[str, object] | None:
|
||||
def get_provider_native_response(self) -> Mapping[str, builtins.object] | None:
|
||||
"""The provider's own response payload, when `req_format=native` was requested."""
|
||||
native_response: Final = self._hidden_params.get(PROVIDER_NATIVE_RESPONSE_KEY)
|
||||
return native_response if isinstance(native_response, dict) else None
|
||||
|
|
|
|||
|
|
@ -21,6 +21,6 @@
|
|||
"limit": 117
|
||||
},
|
||||
"TQ008": {
|
||||
"limit": 11103
|
||||
"limit": 11069
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -100,6 +100,8 @@ def test_proxy_reducto_ocr_json_passthrough_data_uri(client_no_auth):
|
|||
pages=[OCRPage(index=0, markdown="Proxy OCR")],
|
||||
model="parse-v3",
|
||||
usage_info=OCRUsageInfo(pages_processed=1, credits=1),
|
||||
tables=[{"cells": [["Total", 42]], "page": 1}],
|
||||
keyValuePairs=[{"key": "approved", "value": True, "confidence": 0.9}],
|
||||
)
|
||||
|
||||
data_uri = "data:application/pdf;base64,JVBERi0xLjQK"
|
||||
|
|
@ -135,3 +137,5 @@ def test_proxy_reducto_ocr_json_passthrough_data_uri(client_no_auth):
|
|||
assert response_body["object"] == "ocr"
|
||||
assert response_body["usage_info"]["credits"] == 1
|
||||
assert response_body["pages"][0]["markdown"] == "Proxy OCR"
|
||||
assert response_body["tables"] == [{"cells": [["Total", 42]], "page": 1}]
|
||||
assert response_body["keyValuePairs"] == [{"key": "approved", "value": True, "confidence": 0.9}]
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from importlib import import_module
|
||||
from unittest.mock import MagicMock, AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
|
@ -13,15 +14,12 @@ def mock_gcs_dependencies():
|
|||
mock_async_client = AsyncMock()
|
||||
|
||||
with (
|
||||
patch(
|
||||
"litellm.caching.gcs_cache._get_httpx_client", return_value=mock_sync_client
|
||||
patch.object(import_module("litellm.caching.gcs_cache"), "_get_httpx_client", return_value=mock_sync_client
|
||||
),
|
||||
patch(
|
||||
"litellm.caching.gcs_cache.get_async_httpx_client",
|
||||
patch.object(import_module("litellm.caching.gcs_cache"), "get_async_httpx_client",
|
||||
return_value=mock_async_client,
|
||||
),
|
||||
patch(
|
||||
"litellm.caching.gcs_cache.GCSBucketBase.sync_construct_request_headers",
|
||||
patch.object(import_module("litellm.caching.gcs_cache").GCSBucketBase, "sync_construct_request_headers",
|
||||
return_value={},
|
||||
),
|
||||
):
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from importlib import import_module
|
||||
import json
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
|
|
@ -64,7 +65,7 @@ async def test_redis_cluster_async_batch_get(mock_init_redis_cluster):
|
|||
|
||||
@patch("litellm._redis.get_redis_connection_pool")
|
||||
@patch("litellm._redis.get_redis_client")
|
||||
@patch("litellm.caching.redis_cache.RedisCache._setup_health_pings")
|
||||
@patch.object(import_module("litellm.caching.redis_cache").RedisCache, "_setup_health_pings")
|
||||
def test_cache_init_creates_cluster_cache_from_env_var(
|
||||
mock_health, mock_get_client, mock_get_pool, monkeypatch
|
||||
):
|
||||
|
|
@ -91,7 +92,7 @@ def test_cache_init_creates_cluster_cache_from_env_var(
|
|||
|
||||
@patch("litellm._redis.get_redis_connection_pool")
|
||||
@patch("litellm._redis.get_redis_client")
|
||||
@patch("litellm.caching.redis_cache.RedisCache._setup_health_pings")
|
||||
@patch.object(import_module("litellm.caching.redis_cache").RedisCache, "_setup_health_pings")
|
||||
def test_cache_init_creates_redis_cache_without_cluster_config(
|
||||
mock_health, mock_get_client, mock_get_pool, monkeypatch
|
||||
):
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from importlib import import_module
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
|
@ -92,7 +93,7 @@ def _make_redis_cache():
|
|||
patches = [
|
||||
patch("litellm._redis.get_redis_client", return_value=mock_sync_client),
|
||||
patch("litellm._redis.get_redis_connection_pool", return_value=mock_async_pool),
|
||||
patch("litellm.caching.redis_cache.RedisCache._setup_health_pings"),
|
||||
patch.object(import_module("litellm.caching.redis_cache").RedisCache, "_setup_health_pings"),
|
||||
]
|
||||
for p in patches:
|
||||
p.start()
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from importlib import import_module
|
||||
import sys
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
|
|
@ -1453,7 +1454,7 @@ def test_cache_forwards_semantic_cache_embedding_timeout():
|
|||
from litellm.caching.caching import Cache
|
||||
from litellm.types.caching import LiteLLMCacheType
|
||||
|
||||
with patch("litellm.caching.caching.RedisSemanticCache") as backend:
|
||||
with patch.object(import_module("litellm.caching.caching"), "RedisSemanticCache") as backend:
|
||||
Cache(
|
||||
type=LiteLLMCacheType.REDIS_SEMANTIC,
|
||||
similarity_threshold=0.8,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,16 @@
|
|||
"""Unit tests for MCP OAuth passthrough tool-fetch behavior."""
|
||||
|
||||
import sys
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
|
||||
if sys.version_info < (3, 11):
|
||||
from exceptiongroup import ExceptionGroup
|
||||
|
||||
|
||||
from litellm.proxy._experimental.mcp_server.exceptions import MCPUpstreamAuthError
|
||||
from litellm.proxy._experimental.mcp_server.mcp_server_manager import (
|
||||
MCPServerManager,
|
||||
|
|
@ -37,7 +42,7 @@ def test_extract_upstream_auth_failure_walks_exception_group():
|
|||
inner = httpx.HTTPStatusError("401", request=response.request, response=response)
|
||||
|
||||
try:
|
||||
raise ExceptionGroup("wrapped", [inner]) # noqa: F821 (PEP 654, py3.11+)
|
||||
raise ExceptionGroup("wrapped", [inner])
|
||||
except Exception as group:
|
||||
result = _extract_upstream_auth_failure(group)
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,12 @@ if sys.version_info < (3, 11): # BaseExceptionGroup is a builtin only from 3.11
|
|||
from mcp.types import Tool as MCPTool
|
||||
|
||||
|
||||
requires_semantic_router = pytest.mark.skipif(
|
||||
sys.version_info >= (3, 14), reason="The semantic-router extra excludes Python 3.14"
|
||||
)
|
||||
|
||||
|
||||
@requires_semantic_router
|
||||
@pytest.mark.asyncio
|
||||
async def test_semantic_filter_basic_filtering():
|
||||
"""
|
||||
|
|
@ -145,6 +151,7 @@ async def test_semantic_filter_basic_filtering():
|
|||
print(f" Filter respects top_k parameter correctly")
|
||||
|
||||
|
||||
@requires_semantic_router
|
||||
@pytest.mark.asyncio
|
||||
async def test_semantic_filter_top_k_limiting():
|
||||
"""
|
||||
|
|
@ -328,6 +335,7 @@ async def test_semantic_filter_extract_user_query():
|
|||
assert query3 == ""
|
||||
|
||||
|
||||
@requires_semantic_router
|
||||
@pytest.mark.asyncio
|
||||
async def test_semantic_filter_hook_triggers_on_completion():
|
||||
"""
|
||||
|
|
@ -453,6 +461,7 @@ async def test_semantic_filter_hook_skips_no_tools():
|
|||
print("✅ Hook correctly skips requests without tools")
|
||||
|
||||
|
||||
@requires_semantic_router
|
||||
@pytest.mark.asyncio
|
||||
async def test_semantic_filter_hook_preserves_native_tools():
|
||||
"""
|
||||
|
|
@ -584,6 +593,7 @@ async def test_semantic_filter_hook_preserves_native_tools():
|
|||
)
|
||||
|
||||
|
||||
@requires_semantic_router
|
||||
@pytest.mark.asyncio
|
||||
async def test_semantic_filter_hook_all_native_tools():
|
||||
"""
|
||||
|
|
@ -684,6 +694,7 @@ async def test_semantic_filter_hook_all_native_tools():
|
|||
)
|
||||
|
||||
|
||||
@requires_semantic_router
|
||||
@pytest.mark.asyncio
|
||||
async def test_semantic_filter_hook_responses_api_name_collision():
|
||||
"""
|
||||
|
|
@ -774,6 +785,7 @@ async def test_semantic_filter_hook_responses_api_name_collision():
|
|||
print("✅ Responses API tool with MCP-matching name correctly classified as native")
|
||||
|
||||
|
||||
@requires_semantic_router
|
||||
@pytest.mark.asyncio
|
||||
async def test_semantic_filter_hook_filters_expanded_litellm_proxy_tools():
|
||||
"""
|
||||
|
|
@ -889,6 +901,7 @@ async def test_semantic_filter_hook_filters_expanded_litellm_proxy_tools():
|
|||
print(f"✅ Expanded litellm_proxy tools filtered: {len(expanded_tools)} -> {len(allowed_tools)}, stats={stats}")
|
||||
|
||||
|
||||
@requires_semantic_router
|
||||
@pytest.mark.asyncio
|
||||
async def test_semantic_filter_hook_narrows_mcp_reference_for_chat_completions():
|
||||
"""
|
||||
|
|
@ -1008,6 +1021,7 @@ async def test_semantic_filter_hook_narrows_mcp_reference_for_chat_completions()
|
|||
print(f"✅ chat completions: MCP reference preserved, narrowed to {allowed_tools}")
|
||||
|
||||
|
||||
@requires_semantic_router
|
||||
@pytest.mark.asyncio
|
||||
async def test_semantic_filter_hook_zero_matches_exposes_all_tools_on_both_paths():
|
||||
"""
|
||||
|
|
@ -1126,6 +1140,7 @@ async def test_semantic_filter_hook_zero_matches_exposes_all_tools_on_both_paths
|
|||
print("✅ zero matches: both the MCP reference path and the plain tool path expose every tool")
|
||||
|
||||
|
||||
@requires_semantic_router
|
||||
@pytest.mark.asyncio
|
||||
async def test_semantic_filter_hook_filters_expanded_tools_with_string_input():
|
||||
"""
|
||||
|
|
@ -1266,6 +1281,7 @@ async def test_semantic_filter_hook_expansion_skips_filter_when_disabled():
|
|||
print("✅ Disabled filter: MCP reference untouched, no spurious stats")
|
||||
|
||||
|
||||
@requires_semantic_router
|
||||
@pytest.mark.asyncio
|
||||
async def test_semantic_filter_hook_preserves_tool_order():
|
||||
"""
|
||||
|
|
@ -1651,6 +1667,7 @@ def _make_context_window_filter(state, top_k: int = 3):
|
|||
)
|
||||
|
||||
|
||||
@requires_semantic_router
|
||||
@pytest.mark.asyncio
|
||||
async def test_semantic_filter_fails_closed_on_query_time_context_window_error():
|
||||
"""
|
||||
|
|
@ -1682,6 +1699,7 @@ async def test_semantic_filter_fails_closed_on_query_time_context_window_error()
|
|||
print("✅ Query-time context window overflow fails closed")
|
||||
|
||||
|
||||
@requires_semantic_router
|
||||
@pytest.mark.asyncio
|
||||
async def test_semantic_filter_records_build_time_context_window_error():
|
||||
"""
|
||||
|
|
@ -1715,6 +1733,7 @@ async def test_semantic_filter_records_build_time_context_window_error():
|
|||
print("✅ Build-time context window overflow is recorded and fails closed")
|
||||
|
||||
|
||||
@requires_semantic_router
|
||||
@pytest.mark.asyncio
|
||||
async def test_semantic_filter_hook_fails_closed_on_context_window_error():
|
||||
"""
|
||||
|
|
@ -1762,6 +1781,7 @@ async def test_semantic_filter_hook_fails_closed_on_context_window_error():
|
|||
print("✅ Hook fails closed with actionable 400 on context window overflow")
|
||||
|
||||
|
||||
@requires_semantic_router
|
||||
@pytest.mark.asyncio
|
||||
async def test_semantic_filter_hook_fails_closed_on_expanded_tools_context_window_error():
|
||||
"""
|
||||
|
|
@ -1828,6 +1848,7 @@ async def test_semantic_filter_hook_fails_closed_on_expanded_tools_context_windo
|
|||
print("✅ Expansion path fails closed with actionable 400 on context window overflow")
|
||||
|
||||
|
||||
@requires_semantic_router
|
||||
@pytest.mark.asyncio
|
||||
async def test_semantic_filter_hook_ignores_build_error_for_native_only_tools():
|
||||
"""
|
||||
|
|
@ -2018,6 +2039,7 @@ def _weather_tool():
|
|||
)
|
||||
|
||||
|
||||
@requires_semantic_router
|
||||
@pytest.mark.asyncio
|
||||
async def test_filter_indexes_request_tools_when_startup_index_is_empty():
|
||||
"""
|
||||
|
|
@ -2042,6 +2064,7 @@ async def test_filter_indexes_request_tools_when_startup_index_is_empty():
|
|||
print("✅ Empty startup index is built from authed request-time tools")
|
||||
|
||||
|
||||
@requires_semantic_router
|
||||
@pytest.mark.asyncio
|
||||
async def test_filter_indexes_tools_missing_from_partial_index():
|
||||
"""
|
||||
|
|
@ -2070,6 +2093,7 @@ async def test_filter_indexes_tools_missing_from_partial_index():
|
|||
print("✅ Partial startup index is completed from request-time tools, embedding each tool once")
|
||||
|
||||
|
||||
@requires_semantic_router
|
||||
@pytest.mark.asyncio
|
||||
async def test_filter_fails_open_when_matches_are_not_in_available_tools():
|
||||
"""
|
||||
|
|
@ -2093,6 +2117,7 @@ async def test_filter_fails_open_when_matches_are_not_in_available_tools():
|
|||
print("✅ Matches outside available_tools fail open instead of dropping every tool")
|
||||
|
||||
|
||||
@requires_semantic_router
|
||||
@pytest.mark.asyncio
|
||||
async def test_request_time_context_window_error_is_request_scoped():
|
||||
"""
|
||||
|
|
@ -2129,6 +2154,7 @@ async def test_request_time_context_window_error_is_request_scoped():
|
|||
print("✅ Request-time context window overflow is scoped to the request, not the worker")
|
||||
|
||||
|
||||
@requires_semantic_router
|
||||
@pytest.mark.asyncio
|
||||
async def test_foreign_index_routes_cannot_displace_available_tools():
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ Includes file_search emulation: the flag must be forwarded on inner aresponses
|
|||
calls so routed requests do not hit a custom api_base /v1/responses endpoint.
|
||||
"""
|
||||
|
||||
from importlib import import_module
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
|
||||
|
|
@ -17,11 +18,11 @@ from litellm.types.utils import Choices, Message, ModelResponse, Usage
|
|||
class TestUseResponsesApiBridgeFlag:
|
||||
"""Test that bridge opt-in forces the chat completions path."""
|
||||
|
||||
@patch(
|
||||
"litellm.responses.main.litellm_completion_transformation_handler.response_api_handler"
|
||||
@patch.object(
|
||||
import_module("litellm.responses.main").litellm_completion_transformation_handler, "response_api_handler"
|
||||
)
|
||||
@patch(
|
||||
"litellm.responses.main.ProviderConfigManager.get_provider_responses_api_config"
|
||||
@patch.object(
|
||||
import_module("litellm.responses.main").ProviderConfigManager, "get_provider_responses_api_config"
|
||||
)
|
||||
def test_bridge_used_when_use_chat_completions_api_true(
|
||||
self, mock_get_config, mock_bridge_handler
|
||||
|
|
@ -39,11 +40,11 @@ class TestUseResponsesApiBridgeFlag:
|
|||
|
||||
mock_bridge_handler.assert_called_once()
|
||||
|
||||
@patch(
|
||||
"litellm.responses.main.litellm_completion_transformation_handler.response_api_handler"
|
||||
@patch.object(
|
||||
import_module("litellm.responses.main").litellm_completion_transformation_handler, "response_api_handler"
|
||||
)
|
||||
@patch(
|
||||
"litellm.responses.main.ProviderConfigManager.get_provider_responses_api_config"
|
||||
@patch.object(
|
||||
import_module("litellm.responses.main").ProviderConfigManager, "get_provider_responses_api_config"
|
||||
)
|
||||
def test_bridge_used_when_model_uses_chat_completions_prefix(
|
||||
self, mock_get_config, mock_bridge_handler
|
||||
|
|
@ -62,9 +63,9 @@ class TestUseResponsesApiBridgeFlag:
|
|||
# Model string is provider-normalized after resolution; prefix only forces the bridge.
|
||||
assert mock_bridge_handler.call_args.kwargs["model"].endswith("my-custom-model")
|
||||
|
||||
@patch("litellm.responses.main.base_llm_http_handler.response_api_handler")
|
||||
@patch(
|
||||
"litellm.responses.main.ProviderConfigManager.get_provider_responses_api_config"
|
||||
@patch.object(import_module("litellm.responses.main").base_llm_http_handler, "response_api_handler")
|
||||
@patch.object(
|
||||
import_module("litellm.responses.main").ProviderConfigManager, "get_provider_responses_api_config"
|
||||
)
|
||||
def test_native_forwarding_when_flag_absent(
|
||||
self, mock_get_config, mock_native_handler
|
||||
|
|
@ -82,11 +83,11 @@ class TestUseResponsesApiBridgeFlag:
|
|||
|
||||
mock_native_handler.assert_called_once()
|
||||
|
||||
@patch(
|
||||
"litellm.responses.main.litellm_completion_transformation_handler.response_api_handler"
|
||||
@patch.object(
|
||||
import_module("litellm.responses.main").litellm_completion_transformation_handler, "response_api_handler"
|
||||
)
|
||||
@patch(
|
||||
"litellm.responses.main.ProviderConfigManager.get_provider_responses_api_config"
|
||||
@patch.object(
|
||||
import_module("litellm.responses.main").ProviderConfigManager, "get_provider_responses_api_config"
|
||||
)
|
||||
def test_flag_does_not_leak_into_kwargs(self, mock_get_config, mock_bridge_handler):
|
||||
"""use_chat_completions_api should be popped and not passed to the bridge handler."""
|
||||
|
|
@ -104,11 +105,11 @@ class TestUseResponsesApiBridgeFlag:
|
|||
all_kwargs = call_kwargs.kwargs if call_kwargs.kwargs else {}
|
||||
assert "use_chat_completions_api" not in all_kwargs
|
||||
|
||||
@patch(
|
||||
"litellm.responses.main.litellm_completion_transformation_handler.response_api_handler"
|
||||
@patch.object(
|
||||
import_module("litellm.responses.main").litellm_completion_transformation_handler, "response_api_handler"
|
||||
)
|
||||
@patch(
|
||||
"litellm.responses.main.ProviderConfigManager.get_provider_responses_api_config"
|
||||
@patch.object(
|
||||
import_module("litellm.responses.main").ProviderConfigManager, "get_provider_responses_api_config"
|
||||
)
|
||||
def test_bridge_used_when_provider_config_none(
|
||||
self, mock_get_config, mock_bridge_handler
|
||||
|
|
@ -127,8 +128,8 @@ class TestUseResponsesApiBridgeFlag:
|
|||
mock_bridge_handler.assert_called_once()
|
||||
|
||||
@patch("litellm.acompletion")
|
||||
@patch(
|
||||
"litellm.responses.main.ProviderConfigManager.get_provider_responses_api_config"
|
||||
@patch.object(
|
||||
import_module("litellm.responses.main").ProviderConfigManager, "get_provider_responses_api_config"
|
||||
)
|
||||
async def test_allowed_openai_params_forwarded_through_bridge(
|
||||
self, mock_get_config, mock_acompletion
|
||||
|
|
@ -164,9 +165,9 @@ class TestUseResponsesApiBridgeFlag:
|
|||
"reasoning_effort"
|
||||
]
|
||||
|
||||
@patch("litellm.responses.file_search.emulated_handler._call_aresponses")
|
||||
@patch(
|
||||
"litellm.responses.main.ProviderConfigManager.get_provider_responses_api_config"
|
||||
@patch.object(import_module("litellm.responses.file_search.emulated_handler"), "_call_aresponses")
|
||||
@patch.object(
|
||||
import_module("litellm.responses.main").ProviderConfigManager, "get_provider_responses_api_config"
|
||||
)
|
||||
async def test_bridge_flag_forwarded_to_file_search_emulation(
|
||||
self, mock_get_config, mock_call_aresponses
|
||||
|
|
@ -206,12 +207,12 @@ class TestUseResponsesApiBridgeFlag:
|
|||
call_kwargs.get("use_chat_completions_api") is True
|
||||
), "use_chat_completions_api should be forwarded to inner aresponses call"
|
||||
|
||||
@patch(
|
||||
"litellm.responses.main.litellm_completion_transformation_handler.response_api_handler"
|
||||
@patch.object(
|
||||
import_module("litellm.responses.main").litellm_completion_transformation_handler, "response_api_handler"
|
||||
)
|
||||
@patch("litellm.vector_stores.main.asearch")
|
||||
@patch(
|
||||
"litellm.responses.main.ProviderConfigManager.get_provider_responses_api_config"
|
||||
@patch.object(
|
||||
import_module("litellm.responses.main").ProviderConfigManager, "get_provider_responses_api_config"
|
||||
)
|
||||
async def test_bridge_flag_prevents_native_responses_endpoint_call(
|
||||
self, mock_get_config, mock_asearch, mock_bridge_handler
|
||||
|
|
@ -280,10 +281,10 @@ class TestUseResponsesApiBridgeFlag:
|
|||
assert result is not None
|
||||
assert result.id is not None
|
||||
|
||||
@patch("litellm.responses.main.base_llm_http_handler.response_api_handler")
|
||||
@patch.object(import_module("litellm.responses.main").base_llm_http_handler, "response_api_handler")
|
||||
@patch("litellm.vector_stores.main.asearch")
|
||||
@patch(
|
||||
"litellm.responses.main.ProviderConfigManager.get_provider_responses_api_config"
|
||||
@patch.object(
|
||||
import_module("litellm.responses.main").ProviderConfigManager, "get_provider_responses_api_config"
|
||||
)
|
||||
async def test_without_bridge_flag_uses_native_endpoint(
|
||||
self, mock_get_config, mock_asearch, mock_native_handler
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ Covers:
|
|||
I) async path propagates optional params to downstream handler
|
||||
"""
|
||||
|
||||
from importlib import import_module
|
||||
import asyncio
|
||||
from typing import List, cast
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
|
@ -62,23 +63,20 @@ def _provider_by_model(model: str, **_: object) -> tuple[str, str, None, None]:
|
|||
def _patch_responses_dispatch():
|
||||
"""Patch everything after the prompt management block so tests stay unit-level."""
|
||||
return [
|
||||
patch(
|
||||
"litellm.responses.main.litellm.get_llm_provider",
|
||||
patch.object(
|
||||
import_module("litellm.responses.main").litellm, "get_llm_provider",
|
||||
side_effect=_provider_by_model,
|
||||
),
|
||||
patch(
|
||||
"litellm.responses.mcp.litellm_proxy_mcp_handler."
|
||||
"LiteLLM_Proxy_MCP_Handler._should_use_litellm_mcp_gateway",
|
||||
patch.object(
|
||||
import_module("litellm.responses.mcp.litellm_proxy_mcp_handler").LiteLLM_Proxy_MCP_Handler, "_should_use_litellm_mcp_gateway",
|
||||
return_value=False,
|
||||
),
|
||||
patch(
|
||||
"litellm.responses.main.ProviderConfigManager"
|
||||
".get_provider_responses_api_config",
|
||||
patch.object(
|
||||
import_module("litellm.responses.main").ProviderConfigManager, "get_provider_responses_api_config",
|
||||
return_value=None,
|
||||
),
|
||||
patch(
|
||||
"litellm.responses.main.litellm_completion_transformation_handler"
|
||||
".response_api_handler",
|
||||
patch.object(
|
||||
import_module("litellm.responses.main").litellm_completion_transformation_handler, "response_api_handler",
|
||||
return_value=MagicMock(),
|
||||
),
|
||||
]
|
||||
|
|
@ -393,8 +391,8 @@ class TestResponsesAPIPromptManagement:
|
|||
|
||||
patches = _patch_responses_dispatch()
|
||||
with (
|
||||
patch(
|
||||
"litellm.responses.main.litellm.get_llm_provider",
|
||||
patch.object(
|
||||
import_module("litellm.responses.main").litellm, "get_llm_provider",
|
||||
side_effect=_provider_by_model,
|
||||
),
|
||||
patches[1],
|
||||
|
|
@ -599,8 +597,8 @@ def test_sync_prompt_swap_resolves_credentials_for_swapped_provider(monkeypatch:
|
|||
|
||||
monkeypatch.setenv("XAI_API_KEY", "sk-xai-test")
|
||||
logging_obj = _make_logging_obj("gpt-4o-mini", [{"role": "user", "content": "hi"}])
|
||||
with patch( # test-quality-ok: handler boundary stub proves creds resolve for the swapped provider without network
|
||||
"litellm.responses.main.base_llm_http_handler.response_api_handler", return_value=MagicMock()
|
||||
with patch.object( # test-quality-ok: handler boundary stub proves creds resolve for the swapped provider without network
|
||||
import_module("litellm.responses.main").base_llm_http_handler, "response_api_handler", return_value=MagicMock()
|
||||
) as mock_handler:
|
||||
litellm.responses(input="hi", model="xai/grok-4", prompt_id="p1", litellm_logging_obj=logging_obj)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from importlib import import_module
|
||||
import base64
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
|
|
@ -580,12 +581,12 @@ def test_responses_extra_body_forwarded_to_completion_transformation_handler():
|
|||
so it was silently dropped.
|
||||
"""
|
||||
with (
|
||||
patch(
|
||||
"litellm.responses.main.ProviderConfigManager.get_provider_responses_api_config",
|
||||
patch.object(
|
||||
import_module("litellm.responses.main").ProviderConfigManager, "get_provider_responses_api_config",
|
||||
return_value=None,
|
||||
),
|
||||
patch(
|
||||
"litellm.responses.main.litellm_completion_transformation_handler.response_api_handler",
|
||||
patch.object(
|
||||
import_module("litellm.responses.main").litellm_completion_transformation_handler, "response_api_handler",
|
||||
) as mock_handler,
|
||||
):
|
||||
mock_handler.return_value = MagicMock()
|
||||
|
|
@ -611,12 +612,12 @@ def test_responses_maps_reasoning_effort_from_litellm_params_to_reasoning():
|
|||
that cannot set extra_body.
|
||||
"""
|
||||
with (
|
||||
patch(
|
||||
"litellm.responses.main.ProviderConfigManager.get_provider_responses_api_config",
|
||||
patch.object(
|
||||
import_module("litellm.responses.main").ProviderConfigManager, "get_provider_responses_api_config",
|
||||
return_value=None,
|
||||
),
|
||||
patch(
|
||||
"litellm.responses.main.litellm_completion_transformation_handler.response_api_handler",
|
||||
patch.object(
|
||||
import_module("litellm.responses.main").litellm_completion_transformation_handler, "response_api_handler",
|
||||
) as mock_handler,
|
||||
):
|
||||
mock_handler.return_value = MagicMock()
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from importlib import import_module
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
|
@ -148,8 +149,8 @@ class TestTextFormatConversion:
|
|||
incomplete_details=None,
|
||||
)
|
||||
|
||||
with patch(
|
||||
"litellm.responses.main.base_llm_http_handler.response_api_handler",
|
||||
with patch.object(
|
||||
import_module("litellm.responses.main").base_llm_http_handler, "response_api_handler",
|
||||
new=mock_handler,
|
||||
):
|
||||
litellm._turn_on_debug()
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ Tests the rule-based complexity scoring and tier assignment logic.
|
|||
|
||||
import asyncio
|
||||
import logging
|
||||
import sys
|
||||
from typing import Dict, List
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
|
|
@ -46,6 +47,11 @@ from litellm.types.router import (
|
|||
)
|
||||
|
||||
|
||||
requires_semantic_router = pytest.mark.skipif(
|
||||
sys.version_info >= (3, 14), reason="The semantic-router extra excludes Python 3.14"
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_router_instance():
|
||||
"""Create a mock LiteLLM Router instance."""
|
||||
|
|
@ -3413,6 +3419,7 @@ class FakeEmbeddingRouter:
|
|||
class TestSemanticKeywordTierRules:
|
||||
"""Test embedding-based keyword_tier_rules matching."""
|
||||
|
||||
@requires_semantic_router
|
||||
@pytest.mark.asyncio
|
||||
async def test_semantic_match_routes_to_rule_tier(self, basic_config):
|
||||
"""A paraphrase (no literal keyword) still routes via embedding similarity."""
|
||||
|
|
@ -3441,6 +3448,7 @@ class TestSemanticKeywordTierRules:
|
|||
assert result.model == "o1-preview" # REASONING via semantic match
|
||||
assert fake_router.async_embedding_calls, "expected an embedding call for the prompt"
|
||||
|
||||
@requires_semantic_router
|
||||
@pytest.mark.asyncio
|
||||
async def test_tier_matches_on_best_utterance_not_diluted_by_others(self, basic_config):
|
||||
"""A tier with several keywords must match if the query is close to ANY of them,
|
||||
|
|
@ -3475,6 +3483,7 @@ class TestSemanticKeywordTierRules:
|
|||
assert result is not None
|
||||
assert result.model == "o1-preview" # REASONING via best-utterance semantic match
|
||||
|
||||
@requires_semantic_router
|
||||
@pytest.mark.asyncio
|
||||
async def test_semantic_embedding_call_carries_caller_metadata(self, basic_config):
|
||||
"""The query embedding call must carry the caller's metadata/litellm_metadata
|
||||
|
|
@ -3507,6 +3516,7 @@ class TestSemanticKeywordTierRules:
|
|||
assert fake_router.async_embedding_kwargs[0]["metadata"] == {**caller_metadata, **origin}
|
||||
assert fake_router.async_embedding_kwargs[0]["litellm_metadata"] == {**caller_litellm_metadata, **origin}
|
||||
|
||||
@requires_semantic_router
|
||||
@pytest.mark.asyncio
|
||||
async def test_semantic_embedding_call_captures_request_body_in_proxy_server_request(self, basic_config):
|
||||
"""The query embedding call must supply proxy_server_request so its request is logged.
|
||||
|
|
@ -3540,6 +3550,7 @@ class TestSemanticKeywordTierRules:
|
|||
assert body["model"] == "fake-embed"
|
||||
assert body["input"] == ["roll out my k8s cluster"]
|
||||
|
||||
@requires_semantic_router
|
||||
@pytest.mark.asyncio
|
||||
async def test_semantic_embedding_call_propagates_turn_off_message_logging(self, basic_config):
|
||||
"""A caller's turn_off_message_logging must reach the query embedding call.
|
||||
|
|
@ -3570,6 +3581,7 @@ class TestSemanticKeywordTierRules:
|
|||
assert fake_router.async_embedding_kwargs, "expected an embedding call for the prompt"
|
||||
assert fake_router.async_embedding_kwargs[0]["turn_off_message_logging"] is True
|
||||
|
||||
@requires_semantic_router
|
||||
@pytest.mark.asyncio
|
||||
async def test_semantic_embedding_call_strips_budget_reservation(self, basic_config):
|
||||
"""The embedding call must not carry the parent request's budget reservation.
|
||||
|
|
@ -3623,6 +3635,7 @@ class TestSemanticKeywordTierRules:
|
|||
"budget_reservation": {"reserved_cost": 1.0},
|
||||
}
|
||||
|
||||
@requires_semantic_router
|
||||
@pytest.mark.asyncio
|
||||
async def test_semantic_routelayer_build_runs_off_event_loop(self, basic_config):
|
||||
"""Building the SemanticRouter embeds route utterances via a synchronous provider
|
||||
|
|
@ -3654,6 +3667,7 @@ class TestSemanticKeywordTierRules:
|
|||
# ...and none of it ran on the event-loop thread.
|
||||
assert all(tid != loop_thread_id for tid in fake_router.sync_embedding_thread_ids)
|
||||
|
||||
@requires_semantic_router
|
||||
@pytest.mark.asyncio
|
||||
async def test_concurrent_cold_start_builds_routelayer_once(self, basic_config):
|
||||
"""Concurrent first requests must not each construct the route index (which would
|
||||
|
|
@ -3717,6 +3731,7 @@ class TestSemanticKeywordTierRules:
|
|||
assert result is not None
|
||||
assert result.model == "gpt-4o-mini" # SIMPLE via scoring fallback
|
||||
|
||||
@requires_semantic_router
|
||||
@pytest.mark.asyncio
|
||||
async def test_route_embeddings_cached_across_requests(self, basic_config):
|
||||
"""The route layer is built once and reused on subsequent requests."""
|
||||
|
|
@ -3932,6 +3947,7 @@ class TestKeywordOverrideEdgeCases:
|
|||
)
|
||||
assert router._lexical_tier_override("deploy to k8s and reason step by step") is None
|
||||
|
||||
@requires_semantic_router
|
||||
def test_semantic_routelayer_requires_embedding_model(self, mock_router_instance, basic_config):
|
||||
"""Building the route layer without an embedding model raises (defensive invariant)."""
|
||||
config = {**basic_config, "keyword_tier_rules": [{"keywords": ["k8s"], "tier": "REASONING"}]}
|
||||
|
|
@ -3944,6 +3960,7 @@ class TestKeywordOverrideEdgeCases:
|
|||
with pytest.raises(ValueError, match="embedding_model is required"):
|
||||
router._get_or_create_semantic_routelayer()
|
||||
|
||||
@requires_semantic_router
|
||||
@pytest.mark.asyncio
|
||||
async def test_semantic_override_maps_first_of_list(self, mock_router_instance, basic_config):
|
||||
"""A list RouteChoice result maps to the first entry's tier."""
|
||||
|
|
@ -3953,6 +3970,7 @@ class TestKeywordOverrideEdgeCases:
|
|||
router._semantic_routelayer = _StubRouteLayer([RouteChoice(name="COMPLEX"), RouteChoice(name="SIMPLE")])
|
||||
assert await router._semantic_tier_override("anything", {}) == ComplexityTier.COMPLEX
|
||||
|
||||
@requires_semantic_router
|
||||
@pytest.mark.asyncio
|
||||
async def test_semantic_override_empty_list_returns_none(self, mock_router_instance, basic_config):
|
||||
"""An empty list result falls through to scoring."""
|
||||
|
|
@ -3960,6 +3978,7 @@ class TestKeywordOverrideEdgeCases:
|
|||
router._semantic_routelayer = _StubRouteLayer([])
|
||||
assert await router._semantic_tier_override("anything", {}) is None
|
||||
|
||||
@requires_semantic_router
|
||||
@pytest.mark.asyncio
|
||||
async def test_semantic_override_unknown_route_name_returns_none(self, mock_router_instance, basic_config):
|
||||
"""A matched route whose name is not a ComplexityTier is ignored."""
|
||||
|
|
@ -4027,6 +4046,7 @@ class TestRoutingDecisionCauseLogging:
|
|||
# A literal match must not be mislabelled as semantic.
|
||||
assert "cause=semantic_keyword_match" not in router_log_capture.text
|
||||
|
||||
@requires_semantic_router
|
||||
@pytest.mark.asyncio
|
||||
async def test_semantic_keyword_match_logs_its_cause(self, basic_config, router_log_capture):
|
||||
fake_router = FakeEmbeddingRouter()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue