Merge pull request #39399 from BerriAI/litellm_python_version_ci

fix: restore Python compatibility and test 3.10 through 3.14
This commit is contained in:
Mateo Wang 2026-09-04 09:21:52 -07:00 committed by GitHub
commit 2e734004f9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
54 changed files with 510 additions and 248 deletions

View file

@ -57,9 +57,15 @@ permissions:
jobs:
run:
name: Run tests
name: ${{ matrix.python-version == '3.12' && 'Run tests' || format('Run tests (Python {0})', matrix.python-version) }}
runs-on: ubuntu-latest
timeout-minutes: ${{ inputs.job-timeout-minutes }}
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
env:
UV_PYTHON: ${{ matrix.python-version }}
permissions:
contents: read
pull-requests: read
@ -82,7 +88,7 @@ jobs:
timeout-minutes: 3
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.12"
python-version: ${{ matrix.python-version }}
- name: Set up uv
if: steps.changes.outputs.decision != 'skip'
@ -96,12 +102,10 @@ jobs:
timeout-minutes: 5
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: |
~/.cache/uv
.venv
key: ${{ runner.os }}-uv-${{ 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-
${{ runner.os }}-uv-downloads-py${{ matrix.python-version }}-
- name: Cache the Rust build
if: steps.changes.outputs.decision != 'skip'
@ -113,6 +117,7 @@ jobs:
timeout-minutes: 8
run: |
.github/scripts/uv_sync_with_retries.sh --frozen --group ci --group proxy-dev --extra google --extra proxy --extra semantic-router --extra saml
uv run --no-sync python -c 'import os, sys; print(sys.version); assert f"{sys.version_info.major}.{sys.version_info.minor}" == os.environ["UV_PYTHON"]'
- name: Cache Prisma binaries
if: steps.changes.outputs.decision != 'skip'
@ -134,13 +139,7 @@ jobs:
WORKERS: ${{ inputs.workers }}
RERUNS: ${{ inputs.reruns }}
DIST: ${{ inputs.dist }}
# coverage.py's sys.monitoring backend (PEP 669), the cheapest core it has.
# It is only the default from Python 3.14, and these shards run 3.12, so it
# has to be asked for. Coverage refuses it when branch measurement is on
# (`branch_right_left` needs > 3.14.0a5) and falls back to the slow core with
# a `no-sysmon` warning, so turning on `branch = true` here means giving this
# back until the runners move to 3.14.
COVERAGE_CORE: sysmon
COVERAGE_CORE: ${{ contains(fromJSON('["3.10", "3.11"]'), matrix.python-version) && 'ctrace' || 'sysmon' }}
run: |
if [ "${WORKERS}" = "0" ]; then
uv run --no-sync pytest ${TEST_PATH:?} \
@ -167,7 +166,7 @@ jobs:
fi
- name: Save coverage report
if: always() && steps.changes.outputs.decision != 'skip'
if: always() && matrix.python-version == '3.12' && steps.changes.outputs.decision != 'skip'
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
with:
name: coverage-${{ inputs.artifact-name }}-${{ github.run_id }}-${{ github.run_attempt }}

View file

@ -18,7 +18,7 @@
"limit": 40
},
"reportDeprecated": {
"limit": 211
"limit": 209
},
"reportDuplicateImport": {
"limit": 19
@ -45,7 +45,7 @@
"limit": 24
},
"reportInvalidTypeForm": {
"limit": 34
"limit": 30
},
"reportInvalidTypeVarUse": {
"limit": 2
@ -105,13 +105,13 @@
"limit": 109
},
"reportUnknownMemberType": {
"limit": 38311
"limit": 38309
},
"reportUnknownParameterType": {
"limit": 19624
"limit": 19622
},
"reportUnknownVariableType": {
"limit": 29847
"limit": 29846
},
"reportUnnecessaryCast": {
"limit": 111

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

@ -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

View file

@ -32,7 +32,7 @@ class ModelsManagementClient:
headers["Authorization"] = f"Bearer {self._api_key}"
return headers
def list(self, return_request: bool = False) -> list[dict[str, Any]] | requests.Request:
def list(self, return_request: bool = False) -> builtins.list[dict[str, Any]] | requests.Request:
"""
Get the list of models supported by the server.

View file

@ -40,7 +40,7 @@ class TeamsManagementClient:
self,
user_id: str | None = None,
organization_id: str | None = None,
) -> list[dict[str, Any]]:
) -> builtins.list[dict[str, Any]]:
"""
List teams that the user belongs to.

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

@ -632,7 +632,7 @@ class ChatCompletionReasoningItem(TypedDict, total=False):
type: Required[Literal["reasoning"]]
id: str
encrypted_content: str | None
summary: list["ChatCompletionReasoningSummaryTextBlock"]
summary: ReadOnly[list[ChatCompletionReasoningSummaryTextBlock]]
class WebSearchOptionsUserLocationApproximate(TypedDict, total=False):

View file

@ -5121,14 +5121,8 @@ def get_response_string(response_obj: ModelResponse | ModelResponseStream) -> st
return "".join(response_parts)
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

@ -54,7 +54,7 @@ def _direct_vector_store_embedding_executor(
def mock_vector_store_search_response(
mock_results: list[VectorStoreSearchResult] | None = None,
mock_results: builtins.list[VectorStoreSearchResult] | None = None,
):
"""Mock response for vector store search"""
if mock_results is None:
@ -108,7 +108,7 @@ def mock_vector_store_create_response(
@client
async def acreate(
name: str | None = None,
file_ids: list[str] | None = None,
file_ids: builtins.list[str] | None = None,
expires_after: dict | None = None,
chunking_strategy: dict | None = None,
metadata: dict[str, str] | None = None,
@ -172,7 +172,7 @@ async def acreate(
@client
def create(
name: str | None = None,
file_ids: list[str] | None = None,
file_ids: builtins.list[str] | None = None,
expires_after: dict | None = None,
chunking_strategy: dict | None = None,
metadata: dict[str, str] | None = None,
@ -285,7 +285,7 @@ def create(
@client
async def asearch(
vector_store_id: str,
query: str | list[str],
query: str | builtins.list[str],
filters: dict | None = None,
max_num_results: int | None = None,
ranking_options: dict | None = None,
@ -360,7 +360,7 @@ async def asearch(
@client
def search(
vector_store_id: str,
query: str | list[str],
query: str | builtins.list[str],
filters: dict | None = None,
max_num_results: int | None = None,
ranking_options: dict | None = None,

View file

@ -46,7 +46,7 @@ proxy = [
"gunicorn>=23.0.0,<24.0",
"uvicorn>=0.33.0,<1.0",
"granian>=2.7.4,<3.0",
"uvloop>=0.21.0,<1.0; sys_platform != 'win32'",
"uvloop>=0.22.1,<1.0; sys_platform != 'win32'",
"fastapi>=0.136.3,<1.0",
"starlette>=1.0.1,<2.0",
"backoff>=2.2.1,<3.0",
@ -179,6 +179,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": 2000
"limit": 1999
},
"ANN202": {
"limit": 835
@ -87,7 +87,7 @@
"limit": 2
},
"DTZ003": {
"limit": 26
"limit": 24
},
"DTZ005": {
"limit": 233

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

@ -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": 11003
}
}

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

@ -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}]

View file

@ -5,7 +5,9 @@ from dataclasses import dataclass, field
from enum import Enum
from pathlib import Path
from time import monotonic
from typing import TYPE_CHECKING, Final, Literal, TypeAlias, assert_never
from typing import TYPE_CHECKING, Final, Literal, TypeAlias
from typing_extensions import assert_never
if TYPE_CHECKING:
from .strategy import CaseSpec, StrategyDefinition

View file

@ -2,7 +2,9 @@ from __future__ import annotations
from collections.abc import Sequence
from dataclasses import dataclass
from typing import Final, Protocol, assert_never
from typing import Final, Protocol
from typing_extensions import assert_never
from .models import CaseDisposition, CaseResult

View file

@ -2,11 +2,12 @@ from __future__ import annotations
import sys
import threading
from collections.abc import Generator
from collections.abc import Generator, Iterator, Mapping
from contextlib import contextmanager
from dataclasses import dataclass
from functools import lru_cache
from pathlib import Path
from types import CodeType, FrameType
from types import CodeType, FrameType, FunctionType, MappingProxyType
from typing import Final
@ -36,7 +37,7 @@ class PythonProfiler:
def __call__(self, frame: FrameType, event: str, _arg: object) -> None:
if event != "call" or frame in self._seen_frames:
return
function_name: Final = self.function_name(frame.f_code)
function_name: Final = self.function_name(frame)
if function_name is None:
return
event_id: Final = len(self.events)
@ -48,11 +49,12 @@ class PythonProfiler:
self._event_ids[frame] = event_id
self.events.append(FunctionTraceEvent(id=event_id, parent_id=parent_id, function=function_name))
def function_name(self, code: CodeType) -> str | None:
def function_name(self, frame: FrameType) -> str | None:
code: Final = frame.f_code
if not code.co_filename.startswith(self._source_root):
return None
relative: Final = code.co_filename.removeprefix(self._source_root)
return f"{relative}:{code.co_firstlineno} {getattr(code, 'co_qualname', code.co_name)}"
return f"{relative}:{code.co_firstlineno} {_qualified_name(frame)}"
class PythonFunctionUsageProfiler:
@ -68,11 +70,65 @@ class PythonFunctionUsageProfiler:
if not code.co_filename.startswith(self._source_root):
return
relative: Final = code.co_filename.removeprefix(self._source_root)
function: Final = f"{relative}:{code.co_firstlineno} {getattr(code, 'co_qualname', code.co_name)}"
function: Final = f"{relative}:{code.co_firstlineno} {_qualified_name(frame)}"
if function in self._functions:
self.called.add(function)
def _qualified_name(frame: FrameType) -> str:
code: Final = frame.f_code
native: Final = getattr(code, "co_qualname", None)
if isinstance(native, str):
return native
enclosing: Final = next(
(
name
for ancestor in _frame_ancestors(frame)
for declared_code, name in _declared_functions(ancestor.f_locals, frozenset())
if declared_code is code
),
None,
)
if enclosing is not None:
return enclosing
module_name: Final = frame.f_globals.get("__name__")
if not isinstance(module_name, str):
return code.co_name
return _module_qualnames(module_name).get(code, code.co_name)
@lru_cache(maxsize=None)
def _module_qualnames(module_name: str) -> Mapping[CodeType, str]:
module: Final = sys.modules.get(module_name)
if module is None:
return MappingProxyType({})
return MappingProxyType(dict(_declared_functions(vars(module), frozenset())))
def _declared_functions(namespace: Mapping[str, object], visited: frozenset[int]) -> Iterator[tuple[CodeType, str]]:
for attribute in tuple(namespace.values()):
for value in _accessors(attribute):
if isinstance(value, FunctionType):
yield from ((wrapped.__code__, wrapped.__qualname__) for wrapped in _unwrapped(value))
elif isinstance(value, type) and id(value) not in visited:
yield from _declared_functions(dict(vars(value)), visited | {id(value)})
def _unwrapped(function: FunctionType) -> Iterator[FunctionType]:
yield function
inner: Final = getattr(function, "__wrapped__", None)
if isinstance(inner, FunctionType):
yield from _unwrapped(inner)
def _accessors(value: object) -> tuple[object, ...]:
if isinstance(value, (staticmethod, classmethod)):
return (value.__func__,)
if isinstance(value, property):
return tuple(accessor for accessor in (value.fget, value.fset, value.fdel) if accessor is not None)
return (value,)
def _frame_ancestors(frame: FrameType) -> Generator[FrameType]:
ancestor: Final = frame.f_back
if ancestor is not None:

View file

@ -11,6 +11,7 @@ import tempfile
import warnings
from collections.abc import Generator, Sequence
from pathlib import Path
from types import CodeType
from typing import TYPE_CHECKING, Final
from pluggy import HookimplMarker
@ -62,9 +63,12 @@ class PythonFunctionReference(BaseModel):
value: object = importlib.import_module(self.module)
for component in self.qualname.split("."):
value = getattr(value, component)
if not callable(value):
raise ValueError(f"Python function is not callable: {self.module}:{self.qualname}")
function: Final = inspect.unwrap(value)
code: Final = getattr(function, "__code__", None)
if code is None:
qualname: Final = getattr(function, "__qualname__", None)
if not isinstance(code, CodeType) or not isinstance(qualname, str):
raise ValueError(f"Python function has no code object: {self.module}:{self.qualname}")
source: Final = Path(code.co_filename).resolve()
try:
@ -74,7 +78,7 @@ class PythonFunctionReference(BaseModel):
return PythonFunctionIdentity(
file=relative.as_posix(),
line=code.co_firstlineno,
qualname=code.co_qualname,
qualname=qualname,
)

View file

@ -3,12 +3,38 @@ from __future__ import annotations
import asyncio
import sys
import threading
from collections.abc import Callable
from functools import wraps
from pathlib import Path
from typing import Final
from types import FunctionType
from typing import Final, ParamSpec, TypeVar, cast
import pytest
from .profiler import FunctionTraceEvent, PythonProfiler, profile_python, profile_python_function_usage
from .profiler import (
FunctionTraceEvent,
PythonProfiler,
_module_qualnames,
profile_python,
profile_python_function_usage,
)
_P = ParamSpec("_P")
_T = TypeVar("_T")
def _passthrough(function: Callable[_P, _T]) -> Callable[_P, _T]:
@wraps(function)
def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _T:
return function(*args, **kwargs)
return wrapper
class Decorated:
@_passthrough
def call(self) -> None:
return None
def _events_named(profiler: PythonProfiler, name: str) -> tuple[FunctionTraceEvent, ...]:
@ -26,6 +52,14 @@ def test_profiler_keeps_repeated_calls() -> None:
assert len(_events_named(profiler, "called")) == 2
def test_profiler_qualifies_decorated_methods_by_class() -> None:
with profile_python(Path(__file__).parent) as profiler:
Decorated().call()
assert any(event.function.endswith(" Decorated.call") for event in profiler.events)
assert _module_qualnames(__name__)[cast(FunctionType, Decorated.call.__wrapped__).__code__] == "Decorated.call"
def test_profiler_records_real_frame_ancestry() -> None:
def called() -> None:
return None
@ -98,8 +132,7 @@ def test_function_usage_profiler_records_only_selected_functions() -> None:
return None
source_root: Final = Path(__file__).parent
function: Final = PythonProfiler(source_root).function_name(selected.__code__)
assert function is not None
function: Final = f"{Path(__file__).name}:{selected.__code__.co_firstlineno} {selected.__qualname__}"
with profile_python_function_usage(source_root, frozenset((function,))) as profiler:
selected()

View file

@ -165,9 +165,10 @@ def run_trace_cases(
) -> tuple[int, HarnessRun]:
selected_scenarios: Final = frozenset(runner_args)
run: Final = HarnessRun.from_cases(cases)
bridge_error: Final = ensure_trace_bridge(repo_root)
runnable_cases: Final = tuple(case for case in cases if isinstance(case.spec, ModuleCaseSpec))
bridge_error: Final = ensure_trace_bridge(repo_root) if runnable_cases else None
if bridge_error is not None:
for harness_case in cases:
for harness_case in runnable_cases:
_record_setup_failure(run, harness_case, bridge_error, "bridge")
run.finished_at = monotonic()
on_update(run)

View file

@ -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={},
),
):

View file

@ -495,7 +495,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
@ -506,7 +506,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)
@ -683,7 +683,7 @@ def test_call_stack_info_skips_guard_frames_when_deployed_without_sources(monkey
@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
@ -692,7 +692,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")
@ -710,7 +710,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
@ -722,7 +722,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

@ -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
):

View file

@ -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()

View file

@ -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,

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

@ -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)

View file

@ -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():
"""

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

@ -15,6 +15,8 @@ import urllib.parse as urlparse
import uvicorn
import yaml
from uvicorn.config import LOOP_FACTORIES
from uvicorn.importer import import_from_string
from litellm.proxy.proxy_cli import ProxyInitializationHelpers, run_server
@ -462,6 +464,12 @@ class TestProxyInitializationHelpers:
with patch("sys.platform", "linux"):
assert ProxyInitializationHelpers._get_loop_type() == "uvloop"
def test_selected_loop_factory_imports_on_this_interpreter(self):
loop_type = ProxyInitializationHelpers._get_loop_type()
if loop_type is None:
pytest.skip("uvicorn picks the loop itself on this platform")
assert callable(import_from_string(LOOP_FACTORIES[loop_type]))
@patch.dict(os.environ, {}, clear=True)
def test_database_url_construction_with_special_characters(self):
# Setup environment variables with special characters that need escaping

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
@ -808,6 +809,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):
"""
@ -823,7 +836,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),
@ -883,7 +896,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:
@ -924,7 +937,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
@ -973,7 +986,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 (
@ -1020,7 +1033,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),
@ -7370,7 +7383,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),
@ -7412,7 +7425,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),
@ -7455,7 +7468,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),
@ -7498,7 +7511,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),
@ -11864,7 +11877,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

@ -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

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

@ -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)

View file

@ -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()

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,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()

View file

@ -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
@ -50,6 +51,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"
)
def _heuristic_v2_artifact() -> TrainedTierArtifact:
return TrainedTierArtifact(
global_statistics=tuple(
@ -3687,6 +3693,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."""
@ -3715,6 +3722,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,
@ -3749,6 +3757,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
@ -3781,6 +3790,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.
@ -3814,6 +3824,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.
@ -3844,6 +3855,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.
@ -3897,6 +3909,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
@ -3928,6 +3941,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
@ -3991,6 +4005,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."""
@ -4206,6 +4221,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"}]}
@ -4218,6 +4234,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."""
@ -4227,6 +4244,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."""
@ -4234,6 +4252,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."""
@ -4301,6 +4320,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()

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
@ -2604,8 +2605,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,

View file

@ -10,6 +10,41 @@ import litellm
from litellm.types.llms.openai import HttpxBinaryResponseContent
@pytest.mark.parametrize("stream", (False, True))
def test_completion_response_reasoning_summary_round_trip(stream: bool) -> None:
from typing import Final
from litellm.types.llms.openai import (
ChatCompletionReasoningItem,
ChatCompletionReasoningSummaryTextBlock,
)
from litellm.types.utils import (
Choices,
Delta,
Message,
ModelResponse,
ModelResponseStream,
StreamingChoices,
)
reasoning_item: Final = ChatCompletionReasoningItem(
type="reasoning",
id="rs_123",
encrypted_content="encrypted",
summary=[ChatCompletionReasoningSummaryTextBlock(type="summary_text", text="Reasoning summary")],
)
response: Final = (
ModelResponseStream(choices=[StreamingChoices(delta=Delta(reasoning_items=[reasoning_item]))])
if stream
else ModelResponse(choices=[Choices(message=Message(reasoning_items=[reasoning_item]))])
)
message_key: Final = "delta" if stream else "message"
assert response.model_dump()["choices"][0][message_key]["reasoning_items"] == [reasoning_item]
restored: Final = type(response).model_validate_json(response.model_dump_json())
assert restored.model_dump()["choices"][0][message_key]["reasoning_items"] == [reasoning_item]
def test_generic_event():
from litellm.types.llms.openai import GenericEvent

View file

@ -9,6 +9,8 @@ model_dump() it (the #19550 serialization trap).
from unittest.mock import MagicMock, patch
import pytest
import litellm.vector_stores.main as vector_stores_main
from litellm.llms.base_llm.vector_store.transformation import (
RouterVectorStoreEmbeddingExecutor,
@ -22,7 +24,8 @@ MOCK_SEARCH_RESPONSE = {
}
def test_search_wraps_router_into_the_handler_embedding_executor():
@pytest.mark.parametrize("query", ["q", ["q", "another question"]])
def test_search_wraps_router_into_the_handler_embedding_executor(query: str | list[str]):
"""search() hands the HTTP handler a Router-backed embedding executor carrying the
request metadata, and no bare router kwarg (LIT-6750)"""
mock_router = MagicMock()
@ -41,7 +44,7 @@ def test_search_wraps_router_into_the_handler_embedding_executor():
):
response = search(
vector_store_id="bkt:idx",
query="q",
query=query,
custom_llm_provider="s3_vectors",
router=mock_router,
litellm_logging_obj=logger,
@ -51,6 +54,7 @@ def test_search_wraps_router_into_the_handler_embedding_executor():
assert response == MOCK_SEARCH_RESPONSE
mock_handler.assert_called_once()
assert "router" not in mock_handler.call_args.kwargs
assert mock_handler.call_args.kwargs["query"] == query
executor = mock_handler.call_args.kwargs["embedding_executor"]
assert isinstance(executor, RouterVectorStoreEmbeddingExecutor)
assert executor.router is mock_router

View file

@ -33,6 +33,6 @@
"limit": 5514
},
"LIT012": {
"limit": 4489
"limit": 4487
}
}

68
uv.lock generated
View file

@ -4547,6 +4547,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" },
@ -4668,7 +4669,7 @@ requires-dist = [
{ name = "tiktoken", specifier = ">=0.8.0,<1.0" },
{ name = "tokenizers", specifier = ">=0.21.0,<1.0" },
{ name = "uvicorn", marker = "extra == 'proxy'", specifier = ">=0.33.0,<1.0" },
{ name = "uvloop", marker = "sys_platform != 'win32' and extra == 'proxy'", specifier = ">=0.21.0,<1.0" },
{ name = "uvloop", marker = "sys_platform != 'win32' and extra == 'proxy'", specifier = ">=0.22.1,<1.0" },
{ name = "websockets", marker = "extra == 'proxy'", specifier = ">=15.0.1,<16.0" },
]
provides-extras = ["proxy", "cli", "extra-proxy", "utils", "caching", "mcp", "saml", "semantic-router", "mlflow", "grpc", "stt-nvidia-riva", "google", "bedrock-realtime", "proxy-runtime"]
@ -4734,6 +4735,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" },
@ -10054,34 +10056,46 @@ wheels = [
[[package]]
name = "uvloop"
version = "0.21.0"
version = "0.22.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/af/c0/854216d09d33c543f12a44b393c402e89a920b1a0a7dc634c42de91b9cf6/uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3", size = 2492741, upload-time = "2024-10-14T23:38:35.489Z" }
sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3d/76/44a55515e8c9505aa1420aebacf4dd82552e5e15691654894e90d0bd051a/uvloop-0.21.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f", size = 1442019, upload-time = "2024-10-14T23:37:20.068Z" },
{ url = "https://files.pythonhosted.org/packages/35/5a/62d5800358a78cc25c8a6c72ef8b10851bdb8cca22e14d9c74167b7f86da/uvloop-0.21.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d", size = 801898, upload-time = "2024-10-14T23:37:22.663Z" },
{ url = "https://files.pythonhosted.org/packages/f3/96/63695e0ebd7da6c741ccd4489b5947394435e198a1382349c17b1146bb97/uvloop-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f38b2e090258d051d68a5b14d1da7203a3c3677321cf32a95a6f4db4dd8b6f26", size = 3827735, upload-time = "2024-10-14T23:37:25.129Z" },
{ url = "https://files.pythonhosted.org/packages/61/e0/f0f8ec84979068ffae132c58c79af1de9cceeb664076beea86d941af1a30/uvloop-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87c43e0f13022b998eb9b973b5e97200c8b90823454d4bc06ab33829e09fb9bb", size = 3825126, upload-time = "2024-10-14T23:37:27.59Z" },
{ url = "https://files.pythonhosted.org/packages/bf/fe/5e94a977d058a54a19df95f12f7161ab6e323ad49f4dabc28822eb2df7ea/uvloop-0.21.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:10d66943def5fcb6e7b37310eb6b5639fd2ccbc38df1177262b0640c3ca68c1f", size = 3705789, upload-time = "2024-10-14T23:37:29.385Z" },
{ url = "https://files.pythonhosted.org/packages/26/dd/c7179618e46092a77e036650c1f056041a028a35c4d76945089fcfc38af8/uvloop-0.21.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:67dd654b8ca23aed0a8e99010b4c34aca62f4b7fce88f39d452ed7622c94845c", size = 3800523, upload-time = "2024-10-14T23:37:32.048Z" },
{ url = "https://files.pythonhosted.org/packages/57/a7/4cf0334105c1160dd6819f3297f8700fda7fc30ab4f61fbf3e725acbc7cc/uvloop-0.21.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8", size = 1447410, upload-time = "2024-10-14T23:37:33.612Z" },
{ url = "https://files.pythonhosted.org/packages/8c/7c/1517b0bbc2dbe784b563d6ab54f2ef88c890fdad77232c98ed490aa07132/uvloop-0.21.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0", size = 805476, upload-time = "2024-10-14T23:37:36.11Z" },
{ url = "https://files.pythonhosted.org/packages/ee/ea/0bfae1aceb82a503f358d8d2fa126ca9dbdb2ba9c7866974faec1cb5875c/uvloop-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e", size = 3960855, upload-time = "2024-10-14T23:37:37.683Z" },
{ url = "https://files.pythonhosted.org/packages/8a/ca/0864176a649838b838f36d44bf31c451597ab363b60dc9e09c9630619d41/uvloop-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb", size = 3973185, upload-time = "2024-10-14T23:37:40.226Z" },
{ url = "https://files.pythonhosted.org/packages/30/bf/08ad29979a936d63787ba47a540de2132169f140d54aa25bc8c3df3e67f4/uvloop-0.21.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6", size = 3820256, upload-time = "2024-10-14T23:37:42.839Z" },
{ url = "https://files.pythonhosted.org/packages/da/e2/5cf6ef37e3daf2f06e651aae5ea108ad30df3cb269102678b61ebf1fdf42/uvloop-0.21.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d", size = 3937323, upload-time = "2024-10-14T23:37:45.337Z" },
{ url = "https://files.pythonhosted.org/packages/8c/4c/03f93178830dc7ce8b4cdee1d36770d2f5ebb6f3d37d354e061eefc73545/uvloop-0.21.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c", size = 1471284, upload-time = "2024-10-14T23:37:47.833Z" },
{ url = "https://files.pythonhosted.org/packages/43/3e/92c03f4d05e50f09251bd8b2b2b584a2a7f8fe600008bcc4523337abe676/uvloop-0.21.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2", size = 821349, upload-time = "2024-10-14T23:37:50.149Z" },
{ url = "https://files.pythonhosted.org/packages/a6/ef/a02ec5da49909dbbfb1fd205a9a1ac4e88ea92dcae885e7c961847cd51e2/uvloop-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d", size = 4580089, upload-time = "2024-10-14T23:37:51.703Z" },
{ url = "https://files.pythonhosted.org/packages/06/a7/b4e6a19925c900be9f98bec0a75e6e8f79bb53bdeb891916609ab3958967/uvloop-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc", size = 4693770, upload-time = "2024-10-14T23:37:54.122Z" },
{ url = "https://files.pythonhosted.org/packages/ce/0c/f07435a18a4b94ce6bd0677d8319cd3de61f3a9eeb1e5f8ab4e8b5edfcb3/uvloop-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb", size = 4451321, upload-time = "2024-10-14T23:37:55.766Z" },
{ url = "https://files.pythonhosted.org/packages/8f/eb/f7032be105877bcf924709c97b1bf3b90255b4ec251f9340cef912559f28/uvloop-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f", size = 4659022, upload-time = "2024-10-14T23:37:58.195Z" },
{ url = "https://files.pythonhosted.org/packages/3f/8d/2cbef610ca21539f0f36e2b34da49302029e7c9f09acef0b1c3b5839412b/uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281", size = 1468123, upload-time = "2024-10-14T23:38:00.688Z" },
{ url = "https://files.pythonhosted.org/packages/93/0d/b0038d5a469f94ed8f2b2fce2434a18396d8fbfb5da85a0a9781ebbdec14/uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af", size = 819325, upload-time = "2024-10-14T23:38:02.309Z" },
{ url = "https://files.pythonhosted.org/packages/50/94/0a687f39e78c4c1e02e3272c6b2ccdb4e0085fda3b8352fecd0410ccf915/uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6", size = 4582806, upload-time = "2024-10-14T23:38:04.711Z" },
{ url = "https://files.pythonhosted.org/packages/d2/19/f5b78616566ea68edd42aacaf645adbf71fbd83fc52281fba555dc27e3f1/uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816", size = 4701068, upload-time = "2024-10-14T23:38:06.385Z" },
{ url = "https://files.pythonhosted.org/packages/47/57/66f061ee118f413cd22a656de622925097170b9380b30091b78ea0c6ea75/uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc", size = 4454428, upload-time = "2024-10-14T23:38:08.416Z" },
{ url = "https://files.pythonhosted.org/packages/63/9a/0962b05b308494e3202d3f794a6e85abe471fe3cafdbcf95c2e8c713aabd/uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553", size = 4660018, upload-time = "2024-10-14T23:38:10.888Z" },
{ url = "https://files.pythonhosted.org/packages/eb/14/ecceb239b65adaaf7fde510aa8bd534075695d1e5f8dadfa32b5723d9cfb/uvloop-0.22.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ef6f0d4cc8a9fa1f6a910230cd53545d9a14479311e87e3cb225495952eb672c", size = 1343335, upload-time = "2025-10-16T22:16:11.43Z" },
{ url = "https://files.pythonhosted.org/packages/ba/ae/6f6f9af7f590b319c94532b9567409ba11f4fa71af1148cab1bf48a07048/uvloop-0.22.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7cd375a12b71d33d46af85a3343b35d98e8116134ba404bd657b3b1d15988792", size = 742903, upload-time = "2025-10-16T22:16:12.979Z" },
{ url = "https://files.pythonhosted.org/packages/09/bd/3667151ad0702282a1f4d5d29288fce8a13c8b6858bf0978c219cd52b231/uvloop-0.22.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac33ed96229b7790eb729702751c0e93ac5bc3bcf52ae9eccbff30da09194b86", size = 3648499, upload-time = "2025-10-16T22:16:14.451Z" },
{ url = "https://files.pythonhosted.org/packages/b3/f6/21657bb3beb5f8c57ce8be3b83f653dd7933c2fd00545ed1b092d464799a/uvloop-0.22.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:481c990a7abe2c6f4fc3d98781cc9426ebd7f03a9aaa7eb03d3bfc68ac2a46bd", size = 3700133, upload-time = "2025-10-16T22:16:16.272Z" },
{ url = "https://files.pythonhosted.org/packages/09/e0/604f61d004ded805f24974c87ddd8374ef675644f476f01f1df90e4cdf72/uvloop-0.22.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a592b043a47ad17911add5fbd087c76716d7c9ccc1d64ec9249ceafd735f03c2", size = 3512681, upload-time = "2025-10-16T22:16:18.07Z" },
{ url = "https://files.pythonhosted.org/packages/bb/ce/8491fd370b0230deb5eac69c7aae35b3be527e25a911c0acdffb922dc1cd/uvloop-0.22.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1489cf791aa7b6e8c8be1c5a080bae3a672791fcb4e9e12249b05862a2ca9cec", size = 3615261, upload-time = "2025-10-16T22:16:19.596Z" },
{ url = "https://files.pythonhosted.org/packages/c7/d5/69900f7883235562f1f50d8184bb7dd84a2fb61e9ec63f3782546fdbd057/uvloop-0.22.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9", size = 1352420, upload-time = "2025-10-16T22:16:21.187Z" },
{ url = "https://files.pythonhosted.org/packages/a8/73/c4e271b3bce59724e291465cc936c37758886a4868787da0278b3b56b905/uvloop-0.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77", size = 748677, upload-time = "2025-10-16T22:16:22.558Z" },
{ url = "https://files.pythonhosted.org/packages/86/94/9fb7fad2f824d25f8ecac0d70b94d0d48107ad5ece03769a9c543444f78a/uvloop-0.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21", size = 3753819, upload-time = "2025-10-16T22:16:23.903Z" },
{ url = "https://files.pythonhosted.org/packages/74/4f/256aca690709e9b008b7108bc85fba619a2bc37c6d80743d18abad16ee09/uvloop-0.22.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702", size = 3804529, upload-time = "2025-10-16T22:16:25.246Z" },
{ url = "https://files.pythonhosted.org/packages/7f/74/03c05ae4737e871923d21a76fe28b6aad57f5c03b6e6bfcfa5ad616013e4/uvloop-0.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733", size = 3621267, upload-time = "2025-10-16T22:16:26.819Z" },
{ url = "https://files.pythonhosted.org/packages/75/be/f8e590fe61d18b4a92070905497aec4c0e64ae1761498cad09023f3f4b3e/uvloop-0.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473", size = 3723105, upload-time = "2025-10-16T22:16:28.252Z" },
{ url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" },
{ url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" },
{ url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" },
{ url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" },
{ url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" },
{ url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" },
{ url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" },
{ url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" },
{ url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" },
{ url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" },
{ url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" },
{ url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" },
{ url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" },
{ url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" },
{ url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" },
{ url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" },
{ url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" },
{ url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" },
{ url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" },
{ url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" },
{ url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" },
{ url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" },
{ url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" },
{ url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" },
]
[[package]]