fix: address cross-version CI failures

This commit is contained in:
Yujong Lee 2026-09-02 14:17:19 -07:00
parent c8f6531be2
commit 77d6aedf0a
25 changed files with 152 additions and 108 deletions

View file

@ -102,12 +102,10 @@ jobs:
timeout-minutes: 5
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: |
~/.cache/uv
.venv
key: ${{ runner.os }}-uv-py${{ matrix.python-version }}-${{ hashFiles('uv.lock') }}
path: ${{ env.UV_CACHE_DIR }}
key: ${{ runner.os }}-uv-downloads-py${{ matrix.python-version }}-${{ hashFiles('uv.lock') }}
restore-keys: |
${{ runner.os }}-uv-py${{ matrix.python-version }}-
${{ runner.os }}-uv-downloads-py${{ matrix.python-version }}-
- name: Cache the Rust build
if: steps.changes.outputs.decision != 'skip'

View file

@ -18,7 +18,7 @@
"limit": 40
},
"reportDeprecated": {
"limit": 211
"limit": 210
},
"reportDuplicateImport": {
"limit": 19

View file

@ -5,6 +5,7 @@ datasource client {
generator client {
provider = "prisma-client-py"
recursive_type_depth = -1
binaryTargets = ["native", "debian-openssl-1.1.x", "debian-openssl-3.0.x", "linux-musl", "linux-musl-openssl-3.0.x"]
}

View file

@ -5149,14 +5149,8 @@ def get_api_key(llm_provider: str, dynamic_api_key: str | None):
return api_key
def get_utc_datetime():
import datetime as dt
from datetime import datetime
if hasattr(dt, "UTC"):
return datetime.now(dt.UTC)
else:
return datetime.utcnow()
def get_utc_datetime() -> datetime.datetime:
return datetime.datetime.now(datetime.timezone.utc)
def get_max_tokens(model: str) -> int | None:

View file

@ -177,6 +177,7 @@ dev = [
"basedpyright==1.39.7",
"keyring==25.7.0",
"pytest==9.0.3",
"tomli==2.4.1; python_version < '3.11'",
"pytest-mock==3.15.1",
"pytest-asyncio==1.3.0",
"pytest-postgresql==7.0.2",

View file

@ -9,7 +9,7 @@
"limit": 809
},
"ANN201": {
"limit": 2001
"limit": 2000
},
"ANN202": {
"limit": 835
@ -87,7 +87,7 @@
"limit": 2
},
"DTZ003": {
"limit": 26
"limit": 25
},
"DTZ005": {
"limit": 233

View file

@ -24,7 +24,6 @@ seen the red and accepted it.
Usage:
python scripts/budget_ratchet_check.py [--base REF] [budget.json ...]
Stdlib only.
"""
from __future__ import annotations
@ -33,11 +32,15 @@ import argparse
import json
import subprocess
import sys
import tomllib
from pathlib import Path
from types import MappingProxyType
from typing import NamedTuple
if sys.version_info >= (3, 11):
import tomllib
else:
import tomli as tomllib
REPO_ROOT = Path(__file__).resolve().parent.parent
DEFAULT_BASE = "origin/litellm_internal_staging"
DEFAULT_BUDGETS: tuple[str, ...] = (

View file

@ -18,13 +18,17 @@ import json
import re
import subprocess
import sys
import tomllib
from collections import defaultdict
from difflib import SequenceMatcher
from pathlib import Path
from typing import Final, NamedTuple
from textwrap import dedent
if sys.version_info >= (3, 11):
import tomllib
else:
import tomli as tomllib
ROOT = Path(__file__).resolve().parent.parent
MUTMUT_INVOCATION = ["uv", "run", "--no-sync", "--with", "mutmut==3.5.0", "mutmut"]

View file

@ -21,6 +21,6 @@
"limit": 117
},
"TQ008": {
"limit": 11135
"limit": 11103
}
}

View file

@ -6,12 +6,16 @@ from pathlib import Path
import re
import sys
import time
import tomllib
from typing import Callable, Dict, Final, List, Optional, Protocol, Set, Tuple
from packaging.requirements import Requirement
import requests
if sys.version_info >= (3, 11):
import tomllib
else:
import tomli as tomllib
DEFAULT_TRANSITIVE_PIN_PACKAGES = (
"aiofiles",
"anyio",

View file

@ -484,7 +484,7 @@ def _closed_port() -> int:
pytest.param(lambda c: c.async_get_ttl("lit4930"), id="async_get_ttl"),
],
)
async def test_circuit_breaker_opens_when_method_swallows_redis_failure(redis_no_ping, call_method):
async def test_circuit_breaker_opens_when_method_swallows_redis_failure(call_method):
"""A guarded method that swallows its own Redis error must still count as a failure.
These methods catch connection errors and return a default so callers degrade instead
@ -495,7 +495,7 @@ async def test_circuit_breaker_opens_when_method_swallows_redis_failure(redis_no
"""
from litellm.constants import REDIS_CIRCUIT_BREAKER_FAILURE_THRESHOLD
cache = RedisCache(host="127.0.0.1", port=_closed_port(), socket_timeout=0.5)
cache = await asyncio.to_thread(RedisCache, host="127.0.0.1", port=_closed_port(), socket_timeout=0.5)
for _ in range(REDIS_CIRCUIT_BREAKER_FAILURE_THRESHOLD):
await call_method(cache)
@ -505,7 +505,7 @@ async def test_circuit_breaker_opens_when_method_swallows_redis_failure(redis_no
@pytest.mark.asyncio
async def test_circuit_breaker_success_still_resets_the_failure_streak(redis_no_ping):
async def test_circuit_breaker_success_still_resets_the_failure_streak():
"""A reachable Redis must keep the breaker closed, however many earlier calls failed.
The guard now records success only when nothing failed while the method ran, so this
@ -514,7 +514,7 @@ async def test_circuit_breaker_success_still_resets_the_failure_streak(redis_no_
"""
from litellm.constants import REDIS_CIRCUIT_BREAKER_FAILURE_THRESHOLD
cache = RedisCache(host="127.0.0.1", port=_closed_port(), socket_timeout=0.5)
cache = await asyncio.to_thread(RedisCache, host="127.0.0.1", port=_closed_port(), socket_timeout=0.5)
for _ in range(REDIS_CIRCUIT_BREAKER_FAILURE_THRESHOLD - 1):
await cache.async_get_cache("lit4930")
@ -532,7 +532,7 @@ async def test_circuit_breaker_success_still_resets_the_failure_streak(redis_no_
@pytest.mark.asyncio
async def test_circuit_breaker_covers_lua_script_execution(redis_no_ping):
async def test_circuit_breaker_covers_lua_script_execution():
"""Lua script execution must feed the breaker like every other Redis call.
The v3 rate limiter issues all of its Redis traffic through async_register_script, so
@ -544,7 +544,7 @@ async def test_circuit_breaker_covers_lua_script_execution(redis_no_ping):
from litellm.constants import REDIS_CIRCUIT_BREAKER_FAILURE_THRESHOLD
cache = RedisCache(host="127.0.0.1", port=_closed_port(), socket_timeout=0.5)
cache = await asyncio.to_thread(RedisCache, host="127.0.0.1", port=_closed_port(), socket_timeout=0.5)
run_script = cache.async_register_script("return 1")
for _ in range(REDIS_CIRCUIT_BREAKER_FAILURE_THRESHOLD):

View file

@ -258,11 +258,9 @@ async def test_s3_cache_async_set_cache_pipeline(mock_s3_dependencies):
# Verify each call
calls = cache.s3_client.put_object.call_args_list
for i, (key, value) in enumerate(cache_list):
call_args = calls[i][1]
assert call_args["Bucket"] == "test-bucket"
assert call_args["Key"] == key
assert call_args["Body"] == json.dumps(value)
assert {(call.kwargs["Bucket"], call.kwargs["Key"], call.kwargs["Body"]) for call in calls} == {
("test-bucket", key, json.dumps(value)) for key, value in cache_list
}
@pytest.mark.asyncio
@ -285,10 +283,12 @@ async def test_s3_cache_concurrent_async_operations(mock_s3_dependencies):
# Verify each call had correct parameters
calls = cache.s3_client.put_object.call_args_list
for i, call in enumerate(calls):
call_args = call[1]
assert call_args["Bucket"] == "test-bucket"
assert f"concurrent_key_{i}" == call_args["Key"]
assert {call.kwargs["Key"] for call in calls} == {f"concurrent_key_{i}" for i in range(5)}
for call in calls:
assert call.kwargs["Bucket"] == "test-bucket"
payload = json.loads(call.kwargs["Body"])
assert call.kwargs["Key"] == f"concurrent_key_{payload['id']}"
assert payload["data"] == f"test_data_{payload['id']}"
@pytest.mark.asyncio

View file

@ -7,6 +7,7 @@ Covers:
"""
import time
from importlib import import_module
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
@ -107,16 +108,16 @@ class TestResponsesStreamingIteratorMaxDuration:
def test_should_not_raise_when_duration_is_none(self):
it = self._make_base_iterator()
with patch(
"litellm.responses.streaming_iterator.LITELLM_MAX_STREAMING_DURATION_SECONDS",
with patch.object(
import_module("litellm.responses.streaming_iterator"), "LITELLM_MAX_STREAMING_DURATION_SECONDS",
None,
):
it._check_max_streaming_duration()
def test_should_not_raise_when_under_limit(self):
it = self._make_base_iterator()
with patch(
"litellm.responses.streaming_iterator.LITELLM_MAX_STREAMING_DURATION_SECONDS",
with patch.object(
import_module("litellm.responses.streaming_iterator"), "LITELLM_MAX_STREAMING_DURATION_SECONDS",
60.0,
):
it._check_max_streaming_duration()
@ -124,8 +125,8 @@ class TestResponsesStreamingIteratorMaxDuration:
def test_should_raise_timeout_when_exceeded(self):
it = self._make_base_iterator()
it._stream_created_time = time.time() - 20
with patch(
"litellm.responses.streaming_iterator.LITELLM_MAX_STREAMING_DURATION_SECONDS",
with patch.object(
import_module("litellm.responses.streaming_iterator"), "LITELLM_MAX_STREAMING_DURATION_SECONDS",
10.0,
):
with pytest.raises(litellm.Timeout, match="max streaming duration"):

View file

@ -1,3 +1,4 @@
from importlib import import_module
from unittest.mock import AsyncMock, patch
import pytest
@ -163,8 +164,8 @@ async def test_anthropic_messages_with_mcp_forwards_the_callers_mcp_credentials(
).LiteLLM_Proxy_MCP_Handler,
"_process_mcp_tools_without_openai_transform",
new=process,
), patch(
"litellm.responses.mcp.litellm_proxy_mcp_handler.LiteLLM_Proxy_MCP_Handler._execute_tool_calls",
), patch.object(
import_module("litellm.responses.mcp.litellm_proxy_mcp_handler").LiteLLM_Proxy_MCP_Handler, "_execute_tool_calls",
new=execute,
), patch(
"litellm.anthropic_messages", new=AsyncMock(side_effect=responses)
@ -218,11 +219,11 @@ async def test_anthropic_messages_with_mcp_stops_when_every_tool_call_is_skipped
with patch.object(
MCPRequestContext, "resolve", return_value=MCPRequestContext(user_api_key_auth="auth")
), patch(
"litellm.responses.mcp.litellm_proxy_mcp_handler.LiteLLM_Proxy_MCP_Handler._process_mcp_tools_without_openai_transform",
), patch.object(
import_module("litellm.responses.mcp.litellm_proxy_mcp_handler").LiteLLM_Proxy_MCP_Handler, "_process_mcp_tools_without_openai_transform",
new=AsyncMock(return_value=([], {})),
), patch(
"litellm.responses.mcp.litellm_proxy_mcp_handler.LiteLLM_Proxy_MCP_Handler._execute_tool_calls",
), patch.object(
import_module("litellm.responses.mcp.litellm_proxy_mcp_handler").LiteLLM_Proxy_MCP_Handler, "_execute_tool_calls",
new=AsyncMock(return_value=[]),
), patch(
"litellm.anthropic_messages", new=anthropic_messages_mock

View file

@ -13,6 +13,7 @@ Coverage:
import base64
from typing import Any, Dict, List, Optional
from importlib import import_module
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
@ -223,28 +224,28 @@ class TestFileSearchGuardInResponsesMain:
expected = {"ok": True}
with (
patch(
"litellm.responses.main.litellm.get_llm_provider",
patch.object(
import_module("litellm.responses.main").litellm, "get_llm_provider",
return_value=("claude-sonnet-4-5", "anthropic", None, None),
),
patch(
"litellm.responses.main.update_responses_input_with_model_file_ids",
patch.object(
import_module("litellm.responses.main"), "update_responses_input_with_model_file_ids",
return_value="hello",
),
patch(
"litellm.responses.main.update_responses_tools_with_model_file_ids",
patch.object(
import_module("litellm.responses.main"), "update_responses_tools_with_model_file_ids",
return_value=tools,
),
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.ResponsesAPIRequestUtils.get_requested_response_api_optional_param",
patch.object(
import_module("litellm.responses.main").ResponsesAPIRequestUtils, "get_requested_response_api_optional_param",
return_value={},
),
patch(
"litellm.responses.main.run_async_function", return_value=expected
patch.object(
import_module("litellm.responses.main"), "run_async_function", return_value=expected
) as run_async_mock,
):
result = responses(
@ -274,28 +275,28 @@ class TestFileSearchGuardInResponsesMain:
mock_config.supports_native_file_search.return_value = False
with (
patch(
"litellm.responses.main.litellm.get_llm_provider",
patch.object(
import_module("litellm.responses.main").litellm, "get_llm_provider",
return_value=("claude-sonnet-4-5", "anthropic", None, None),
),
patch(
"litellm.responses.main.update_responses_input_with_model_file_ids",
patch.object(
import_module("litellm.responses.main"), "update_responses_input_with_model_file_ids",
return_value="hello",
),
patch(
"litellm.responses.main.update_responses_tools_with_model_file_ids",
patch.object(
import_module("litellm.responses.main"), "update_responses_tools_with_model_file_ids",
return_value=tools,
),
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=mock_config,
),
patch(
"litellm.responses.main.ResponsesAPIRequestUtils.get_requested_response_api_optional_param",
patch.object(
import_module("litellm.responses.main").ResponsesAPIRequestUtils, "get_requested_response_api_optional_param",
return_value={},
),
patch(
"litellm.responses.main.run_async_function", return_value=expected
patch.object(
import_module("litellm.responses.main"), "run_async_function", return_value=expected
) as run_async_mock,
):
result = responses(
@ -758,8 +759,8 @@ class TestEmulatedFileSearchHandler:
mock_search_response.data = [search_result]
with (
patch(
"litellm.responses.file_search.emulated_handler._call_aresponses",
patch.object(
import_module("litellm.responses.file_search.emulated_handler"), "_call_aresponses",
new=AsyncMock(side_effect=[first_resp, final_resp]),
),
patch(
@ -821,8 +822,8 @@ class TestEmulatedFileSearchHandler:
mock_search_response.data = [search_result]
with (
patch(
"litellm.responses.file_search.emulated_handler._call_aresponses",
patch.object(
import_module("litellm.responses.file_search.emulated_handler"), "_call_aresponses",
new=AsyncMock(side_effect=[first_resp_plural, final_resp]),
),
patch(
@ -855,8 +856,8 @@ class TestEmulatedFileSearchHandler:
text="I already know the answer."
)
with patch(
"litellm.responses.file_search.emulated_handler._call_aresponses",
with patch.object(
import_module("litellm.responses.file_search.emulated_handler"), "_call_aresponses",
new=AsyncMock(return_value=direct_resp),
):
result = await aresponses_with_emulated_file_search(
@ -905,8 +906,8 @@ class TestEmulatedFileSearchHandler:
mock_search_response.data = [search_result]
with (
patch(
"litellm.responses.file_search.emulated_handler._call_aresponses",
patch.object(
import_module("litellm.responses.file_search.emulated_handler"), "_call_aresponses",
new=AsyncMock(side_effect=[first_resp, final_resp]),
) as mock_call,
patch(

View file

@ -109,7 +109,7 @@ def test_supported_hooks_limited_to_pre_and_post():
def test_during_call_mode_rejected_at_init():
with pytest.raises(ValueError, match='Event hook GuardrailEventHooks\\.during_call is not in the'):
with pytest.raises(ValueError, match="during_call is not in the supported event hooks"):
StraikerGuardrail(api_key="k", event_hook="during_call")

View file

@ -9,8 +9,9 @@ import subprocess
import types
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Final
from unittest import mock
from unittest.mock import AsyncMock, MagicMock, mock_open, patch
from unittest.mock import AsyncMock, MagicMock, create_autospec, mock_open, patch
import click
import httpx
@ -696,6 +697,18 @@ def test_restructure_always_happens(monkeypatch):
assert ui_path == packaged_ui_path
def _mock_scheduled_proxy_config() -> MagicMock:
config: Final = proxy_server_module.ProxyConfig()
return MagicMock(
spec=proxy_server_module.ProxyConfig,
check_periodic_reloads=create_autospec(config.check_periodic_reloads),
get_credentials=create_autospec(config.get_credentials),
add_deployment=create_autospec(config.add_deployment),
reload_search_tools_from_db=create_autospec(config.reload_search_tools_from_db),
reload_mcp_servers_from_db=create_autospec(config.reload_mcp_servers_from_db),
)
@pytest.mark.asyncio
async def test_initialize_scheduled_jobs_credentials(monkeypatch):
"""
@ -711,7 +724,7 @@ async def test_initialize_scheduled_jobs_credentials(monkeypatch):
mock_proxy_logging = MagicMock(spec=ProxyLogging)
mock_proxy_logging.slack_alerting_instance = MagicMock()
mock_proxy_logging.db_spend_update_writer = MagicMock()
mock_proxy_config = AsyncMock()
mock_proxy_config = _mock_scheduled_proxy_config()
with (
patch("litellm.proxy.proxy_server.proxy_config", mock_proxy_config),
@ -771,7 +784,7 @@ async def test_periodic_reload_job_scheduled_without_store_model_in_db(monkeypat
mock_proxy_logging = MagicMock(spec=ProxyLogging)
mock_proxy_logging.slack_alerting_instance = MagicMock()
mock_proxy_logging.db_spend_update_writer = MagicMock()
mock_proxy_config = AsyncMock()
mock_proxy_config = _mock_scheduled_proxy_config()
scheduler = AsyncIOScheduler()
try:
@ -812,7 +825,7 @@ async def test_initialize_scheduled_jobs_uses_configured_config_reload_interval(
mock_proxy_logging = MagicMock(spec=ProxyLogging)
mock_proxy_logging.slack_alerting_instance = MagicMock()
mock_proxy_logging.db_spend_update_writer = MagicMock()
mock_proxy_config = AsyncMock()
mock_proxy_config = _mock_scheduled_proxy_config()
mock_scheduler = MagicMock()
configured_interval = 47
@ -861,7 +874,7 @@ async def test_initialize_scheduled_jobs_rejects_non_positive_config_reload_inte
mock_proxy_logging = MagicMock(spec=ProxyLogging)
mock_proxy_logging.slack_alerting_instance = MagicMock()
mock_proxy_logging.db_spend_update_writer = MagicMock()
mock_proxy_config = AsyncMock()
mock_proxy_config = _mock_scheduled_proxy_config()
mock_scheduler = MagicMock()
with (
@ -908,7 +921,7 @@ async def test_initialize_scheduled_jobs_hydrates_mcp_when_store_model_in_db_fal
mock_proxy_logging = MagicMock(spec=ProxyLogging)
mock_proxy_logging.slack_alerting_instance = MagicMock()
mock_proxy_logging.db_spend_update_writer = MagicMock()
mock_proxy_config = AsyncMock()
mock_proxy_config = _mock_scheduled_proxy_config()
with (
patch("litellm.proxy.proxy_server.proxy_config", mock_proxy_config),
@ -7258,7 +7271,7 @@ async def test_batch_cost_poller_is_confirmed_before_serving(monkeypatch):
mock_proxy_logging.db_spend_update_writer = MagicMock()
with (
patch("litellm.proxy.proxy_server.proxy_config", AsyncMock()),
patch("litellm.proxy.proxy_server.proxy_config", _mock_scheduled_proxy_config()),
patch("litellm.proxy.proxy_server.store_model_in_db", False),
patch("litellm.proxy.proxy_server.llm_router", MagicMock()),
patch("litellm.proxy.proxy_server.PROXY_BATCH_POLLING_ENABLED", True),
@ -7300,7 +7313,7 @@ async def test_store_model_in_db_db_override_when_config_false():
mock_proxy_logging = MagicMock(spec=ProxyLogging)
mock_proxy_logging.slack_alerting_instance = MagicMock()
mock_proxy_logging.db_spend_update_writer = MagicMock()
mock_proxy_config = AsyncMock()
mock_proxy_config = _mock_scheduled_proxy_config()
with (
patch("litellm.proxy.proxy_server.proxy_config", mock_proxy_config),
@ -7343,7 +7356,7 @@ async def test_store_model_in_db_db_check_skipped_when_already_true(monkeypatch)
mock_proxy_logging = MagicMock(spec=ProxyLogging)
mock_proxy_logging.slack_alerting_instance = MagicMock()
mock_proxy_logging.db_spend_update_writer = MagicMock()
mock_proxy_config = AsyncMock()
mock_proxy_config = _mock_scheduled_proxy_config()
with (
patch("litellm.proxy.proxy_server.proxy_config", mock_proxy_config),
@ -7386,7 +7399,7 @@ async def test_store_model_in_db_db_failure_graceful(monkeypatch):
mock_proxy_logging = MagicMock(spec=ProxyLogging)
mock_proxy_logging.slack_alerting_instance = MagicMock()
mock_proxy_logging.db_spend_update_writer = MagicMock()
mock_proxy_config = AsyncMock()
mock_proxy_config = _mock_scheduled_proxy_config()
with (
patch("litellm.proxy.proxy_server.proxy_config", mock_proxy_config),
@ -11640,7 +11653,7 @@ async def _run_scheduled_background_jobs():
mock_proxy_logging = MagicMock(spec=ProxyLogging)
mock_proxy_logging.slack_alerting_instance = MagicMock()
mock_proxy_logging.db_spend_update_writer = MagicMock()
mock_proxy_config = AsyncMock()
mock_proxy_config = _mock_scheduled_proxy_config()
with (
patch("litellm.proxy.proxy_server.proxy_config", mock_proxy_config),

View file

@ -7,6 +7,7 @@ in expected_responses_api_request/.
import copy
import json
from pathlib import Path
from importlib import import_module
from unittest.mock import AsyncMock, patch
import httpx
@ -405,8 +406,8 @@ async def test_aresponses_websocket_strips_responses_routing_prefix_from_openai_
from litellm.responses.main import _aresponses_websocket
with patch(
"litellm.responses.main.base_llm_http_handler.async_responses_websocket",
with patch.object(
import_module("litellm.responses.main").base_llm_http_handler, "async_responses_websocket",
new_callable=AsyncMock,
) as mock_ws:
await _aresponses_websocket(

View file

@ -15,6 +15,7 @@ Pydantic ValidationError (previously typed as Optional[str]).
"""
import json
from importlib import import_module
from unittest.mock import Mock, patch
import pytest
@ -259,8 +260,8 @@ def test_handle_logging_failed_response_maps_rate_limit_to_429():
{"type": "tokens", "code": "rate_limit_exceeded", "message": "throttled"}
)
with (
patch("litellm.responses.streaming_iterator.run_async_function") as mock_run_async,
patch("litellm.responses.streaming_iterator.executor"),
patch.object(import_module("litellm.responses.streaming_iterator"), "run_async_function") as mock_run_async,
patch.object(import_module("litellm.responses.streaming_iterator"), "executor"),
):
iterator._handle_logging_failed_response()
logged_exception = mock_run_async.call_args.kwargs["exception"]
@ -276,8 +277,8 @@ def test_handle_logging_failed_response_maps_type_field_to_400():
{"type": "invalid_request_error", "code": "invalid_prompt", "message": "bad prompt"}
)
with (
patch("litellm.responses.streaming_iterator.run_async_function") as mock_run_async,
patch("litellm.responses.streaming_iterator.executor"),
patch.object(import_module("litellm.responses.streaming_iterator"), "run_async_function") as mock_run_async,
patch.object(import_module("litellm.responses.streaming_iterator"), "executor"),
):
iterator._handle_logging_failed_response()
logged_exception = mock_run_async.call_args.kwargs["exception"]
@ -296,8 +297,8 @@ def test_handle_logging_failed_response_records_usage_and_cost():
iterator.completed_response = chunk
iterator.logging_obj._response_cost_calculator.return_value = 0.0042
with (
patch("litellm.responses.streaming_iterator.run_async_function"),
patch("litellm.responses.streaming_iterator.executor"),
patch.object(import_module("litellm.responses.streaming_iterator"), "run_async_function"),
patch.object(import_module("litellm.responses.streaming_iterator"), "executor"),
):
iterator._handle_logging_failed_response()
combined_usage = iterator.logging_obj.model_call_details["combined_usage_object"]
@ -315,8 +316,8 @@ def test_handle_logging_failed_response_without_usage_skips_recording():
{"type": "server_error", "code": "server_error", "message": "boom"}
)
with (
patch("litellm.responses.streaming_iterator.run_async_function"),
patch("litellm.responses.streaming_iterator.executor"),
patch.object(import_module("litellm.responses.streaming_iterator"), "run_async_function"),
patch.object(import_module("litellm.responses.streaming_iterator"), "executor"),
):
iterator._handle_logging_failed_response()
assert "combined_usage_object" not in iterator.logging_obj.model_call_details

View file

@ -1,5 +1,6 @@
"""Tests for litellm/router_strategy/auto_router/litellm_encoder.py"""
import sys
from typing import Any, Final
import pytest
@ -7,6 +8,9 @@ import pytest
import litellm
from litellm.constants import DEFAULT_AUTO_ROUTER_MAX_INPUT_CHARS
if sys.version_info >= (3, 14):
pytest.skip("The semantic-router extra excludes Python 3.14", allow_module_level=True)
from litellm.router_strategy.auto_router.litellm_encoder import LiteLLMRouterEncoder

View file

@ -1,8 +1,8 @@
import json
import typing
from pathlib import Path
import pytest
from typing_extensions import get_args, get_type_hints
import litellm
from litellm.types.utils import ModelInfoBase
@ -50,8 +50,8 @@ def _load_cost_map() -> dict:
def test_realtime_is_a_valid_mode_literal():
hints = typing.get_type_hints(ModelInfoBase, include_extras=False)
assert "realtime" in typing.get_args(hints["mode"])
hints = get_type_hints(ModelInfoBase, include_extras=False)
assert "realtime" in get_args(hints["mode"])
@pytest.mark.parametrize("model", REALTIME_ONLY_GPT_MODELS)

View file

@ -16,6 +16,7 @@ from fastapi.testclient import TestClient
import urllib.parse
from importlib import import_module
from unittest.mock import MagicMock, patch
import litellm
@ -2515,8 +2516,8 @@ def test_completion_forwards_store_and_prompt_cache_key_to_mcp_gateway():
prompt_cache_key are named params, so they no longer travel via **kwargs and
must be forwarded explicitly like safety_identifier and service_tier.
"""
with patch(
"litellm.responses.mcp.chat_completions_handler.acompletion_with_mcp"
with patch.object(
import_module("litellm.responses.mcp.chat_completions_handler"), "acompletion_with_mcp"
) as mock_mcp:
result = litellm.completion(
model="openai/gpt-4o",

View file

@ -4,11 +4,15 @@ import re
import shutil
import subprocess
import sys
import tomllib
from pathlib import Path
import pytest
if sys.version_info >= (3, 11):
import tomllib
else:
import tomli as tomllib
_REPO_ROOT = Path(__file__).resolve().parents[2]
_MODULE_PATH = _REPO_ROOT / "scripts" / "ruff_strict_gate.py"
_spec = importlib.util.spec_from_file_location("ruff_strict_gate", _MODULE_PATH)

View file

@ -2,6 +2,7 @@ import asyncio
import json
import logging
import os
from datetime import datetime, timedelta, timezone
from typing import Final
from unittest.mock import AsyncMock, MagicMock, patch
@ -53,6 +54,15 @@ from litellm.utils import (
# Adds the parent directory to the system path
def test_get_utc_datetime_returns_current_aware_utc_time() -> None:
before: Final = datetime.now(timezone.utc)
result: Final = litellm.utils.get_utc_datetime()
after: Final = datetime.now(timezone.utc)
assert result.utcoffset() == timedelta(0)
assert before <= result <= after
def test_usage_openai_cache_write_tokens_populates_both_names():
"""OpenAI reports cache-write tokens as prompt_tokens_details.cache_write_tokens.
The Usage constructor must expose it under both cache_write_tokens (canonical,

2
uv.lock generated
View file

@ -4453,6 +4453,7 @@ dev = [
{ name = "responses" },
{ name = "respx" },
{ name = "ruff" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
{ name = "types-boto3", extra = ["bedrock", "bedrock-agent", "bedrock-runtime", "kms", "s3", "sagemaker-runtime", "sts"] },
{ name = "types-pyyaml" },
{ name = "types-redis" },
@ -4638,6 +4639,7 @@ dev = [
{ name = "responses", specifier = "==0.26.0" },
{ name = "respx", specifier = "==0.22.0" },
{ name = "ruff", specifier = "==0.15.3" },
{ name = "tomli", marker = "python_full_version < '3.11'", specifier = "==2.4.1" },
{ name = "types-boto3", extras = ["bedrock", "bedrock-agent", "bedrock-runtime", "kms", "s3", "sagemaker-runtime", "sts"], specifier = "==1.43.30" },
{ name = "types-pyyaml", specifier = "==6.0.12.20250915" },
{ name = "types-redis", specifier = "==4.6.0.20241004" },