mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-06 08:16:43 +00:00
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:
commit
2e734004f9
54 changed files with 510 additions and 248 deletions
29
.github/workflows/_test-unit-base.yml
vendored
29
.github/workflows/_test-unit-base.yml
vendored
|
|
@ -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 }}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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, ...] = (
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,6 @@
|
|||
"limit": 117
|
||||
},
|
||||
"TQ008": {
|
||||
"limit": 11135
|
||||
"limit": 11003
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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}]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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={},
|
||||
),
|
||||
):
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from importlib import import_module
|
||||
import json
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
|
|
@ -64,7 +65,7 @@ async def test_redis_cluster_async_batch_get(mock_init_redis_cluster):
|
|||
|
||||
@patch("litellm._redis.get_redis_connection_pool")
|
||||
@patch("litellm._redis.get_redis_client")
|
||||
@patch("litellm.caching.redis_cache.RedisCache._setup_health_pings")
|
||||
@patch.object(import_module("litellm.caching.redis_cache").RedisCache, "_setup_health_pings")
|
||||
def test_cache_init_creates_cluster_cache_from_env_var(
|
||||
mock_health, mock_get_client, mock_get_pool, monkeypatch
|
||||
):
|
||||
|
|
@ -91,7 +92,7 @@ def test_cache_init_creates_cluster_cache_from_env_var(
|
|||
|
||||
@patch("litellm._redis.get_redis_connection_pool")
|
||||
@patch("litellm._redis.get_redis_client")
|
||||
@patch("litellm.caching.redis_cache.RedisCache._setup_health_pings")
|
||||
@patch.object(import_module("litellm.caching.redis_cache").RedisCache, "_setup_health_pings")
|
||||
def test_cache_init_creates_redis_cache_without_cluster_config(
|
||||
mock_health, mock_get_client, mock_get_pool, monkeypatch
|
||||
):
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from importlib import import_module
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
|
@ -92,7 +93,7 @@ def _make_redis_cache():
|
|||
patches = [
|
||||
patch("litellm._redis.get_redis_client", return_value=mock_sync_client),
|
||||
patch("litellm._redis.get_redis_connection_pool", return_value=mock_async_pool),
|
||||
patch("litellm.caching.redis_cache.RedisCache._setup_health_pings"),
|
||||
patch.object(import_module("litellm.caching.redis_cache").RedisCache, "_setup_health_pings"),
|
||||
]
|
||||
for p in patches:
|
||||
p.start()
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from importlib import import_module
|
||||
import sys
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
|
|
@ -1453,7 +1454,7 @@ def test_cache_forwards_semantic_cache_embedding_timeout():
|
|||
from litellm.caching.caching import Cache
|
||||
from litellm.types.caching import LiteLLMCacheType
|
||||
|
||||
with patch("litellm.caching.caching.RedisSemanticCache") as backend:
|
||||
with patch.object(import_module("litellm.caching.caching"), "RedisSemanticCache") as backend:
|
||||
Cache(
|
||||
type=LiteLLMCacheType.REDIS_SEMANTIC,
|
||||
similarity_threshold=0.8,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -1,11 +1,16 @@
|
|||
"""Unit tests for MCP OAuth passthrough tool-fetch behavior."""
|
||||
|
||||
import sys
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
|
||||
if sys.version_info < (3, 11):
|
||||
from exceptiongroup import ExceptionGroup
|
||||
|
||||
|
||||
from litellm.proxy._experimental.mcp_server.exceptions import MCPUpstreamAuthError
|
||||
from litellm.proxy._experimental.mcp_server.mcp_server_manager import (
|
||||
MCPServerManager,
|
||||
|
|
@ -37,7 +42,7 @@ def test_extract_upstream_auth_failure_walks_exception_group():
|
|||
inner = httpx.HTTPStatusError("401", request=response.request, response=response)
|
||||
|
||||
try:
|
||||
raise ExceptionGroup("wrapped", [inner]) # noqa: F821 (PEP 654, py3.11+)
|
||||
raise ExceptionGroup("wrapped", [inner])
|
||||
except Exception as group:
|
||||
result = _extract_upstream_auth_failure(group)
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,12 @@ if sys.version_info < (3, 11): # BaseExceptionGroup is a builtin only from 3.11
|
|||
from mcp.types import Tool as MCPTool
|
||||
|
||||
|
||||
requires_semantic_router = pytest.mark.skipif(
|
||||
sys.version_info >= (3, 14), reason="The semantic-router extra excludes Python 3.14"
|
||||
)
|
||||
|
||||
|
||||
@requires_semantic_router
|
||||
@pytest.mark.asyncio
|
||||
async def test_semantic_filter_basic_filtering():
|
||||
"""
|
||||
|
|
@ -145,6 +151,7 @@ async def test_semantic_filter_basic_filtering():
|
|||
print(f" Filter respects top_k parameter correctly")
|
||||
|
||||
|
||||
@requires_semantic_router
|
||||
@pytest.mark.asyncio
|
||||
async def test_semantic_filter_top_k_limiting():
|
||||
"""
|
||||
|
|
@ -328,6 +335,7 @@ async def test_semantic_filter_extract_user_query():
|
|||
assert query3 == ""
|
||||
|
||||
|
||||
@requires_semantic_router
|
||||
@pytest.mark.asyncio
|
||||
async def test_semantic_filter_hook_triggers_on_completion():
|
||||
"""
|
||||
|
|
@ -453,6 +461,7 @@ async def test_semantic_filter_hook_skips_no_tools():
|
|||
print("✅ Hook correctly skips requests without tools")
|
||||
|
||||
|
||||
@requires_semantic_router
|
||||
@pytest.mark.asyncio
|
||||
async def test_semantic_filter_hook_preserves_native_tools():
|
||||
"""
|
||||
|
|
@ -584,6 +593,7 @@ async def test_semantic_filter_hook_preserves_native_tools():
|
|||
)
|
||||
|
||||
|
||||
@requires_semantic_router
|
||||
@pytest.mark.asyncio
|
||||
async def test_semantic_filter_hook_all_native_tools():
|
||||
"""
|
||||
|
|
@ -684,6 +694,7 @@ async def test_semantic_filter_hook_all_native_tools():
|
|||
)
|
||||
|
||||
|
||||
@requires_semantic_router
|
||||
@pytest.mark.asyncio
|
||||
async def test_semantic_filter_hook_responses_api_name_collision():
|
||||
"""
|
||||
|
|
@ -774,6 +785,7 @@ async def test_semantic_filter_hook_responses_api_name_collision():
|
|||
print("✅ Responses API tool with MCP-matching name correctly classified as native")
|
||||
|
||||
|
||||
@requires_semantic_router
|
||||
@pytest.mark.asyncio
|
||||
async def test_semantic_filter_hook_filters_expanded_litellm_proxy_tools():
|
||||
"""
|
||||
|
|
@ -889,6 +901,7 @@ async def test_semantic_filter_hook_filters_expanded_litellm_proxy_tools():
|
|||
print(f"✅ Expanded litellm_proxy tools filtered: {len(expanded_tools)} -> {len(allowed_tools)}, stats={stats}")
|
||||
|
||||
|
||||
@requires_semantic_router
|
||||
@pytest.mark.asyncio
|
||||
async def test_semantic_filter_hook_narrows_mcp_reference_for_chat_completions():
|
||||
"""
|
||||
|
|
@ -1008,6 +1021,7 @@ async def test_semantic_filter_hook_narrows_mcp_reference_for_chat_completions()
|
|||
print(f"✅ chat completions: MCP reference preserved, narrowed to {allowed_tools}")
|
||||
|
||||
|
||||
@requires_semantic_router
|
||||
@pytest.mark.asyncio
|
||||
async def test_semantic_filter_hook_zero_matches_exposes_all_tools_on_both_paths():
|
||||
"""
|
||||
|
|
@ -1126,6 +1140,7 @@ async def test_semantic_filter_hook_zero_matches_exposes_all_tools_on_both_paths
|
|||
print("✅ zero matches: both the MCP reference path and the plain tool path expose every tool")
|
||||
|
||||
|
||||
@requires_semantic_router
|
||||
@pytest.mark.asyncio
|
||||
async def test_semantic_filter_hook_filters_expanded_tools_with_string_input():
|
||||
"""
|
||||
|
|
@ -1266,6 +1281,7 @@ async def test_semantic_filter_hook_expansion_skips_filter_when_disabled():
|
|||
print("✅ Disabled filter: MCP reference untouched, no spurious stats")
|
||||
|
||||
|
||||
@requires_semantic_router
|
||||
@pytest.mark.asyncio
|
||||
async def test_semantic_filter_hook_preserves_tool_order():
|
||||
"""
|
||||
|
|
@ -1651,6 +1667,7 @@ def _make_context_window_filter(state, top_k: int = 3):
|
|||
)
|
||||
|
||||
|
||||
@requires_semantic_router
|
||||
@pytest.mark.asyncio
|
||||
async def test_semantic_filter_fails_closed_on_query_time_context_window_error():
|
||||
"""
|
||||
|
|
@ -1682,6 +1699,7 @@ async def test_semantic_filter_fails_closed_on_query_time_context_window_error()
|
|||
print("✅ Query-time context window overflow fails closed")
|
||||
|
||||
|
||||
@requires_semantic_router
|
||||
@pytest.mark.asyncio
|
||||
async def test_semantic_filter_records_build_time_context_window_error():
|
||||
"""
|
||||
|
|
@ -1715,6 +1733,7 @@ async def test_semantic_filter_records_build_time_context_window_error():
|
|||
print("✅ Build-time context window overflow is recorded and fails closed")
|
||||
|
||||
|
||||
@requires_semantic_router
|
||||
@pytest.mark.asyncio
|
||||
async def test_semantic_filter_hook_fails_closed_on_context_window_error():
|
||||
"""
|
||||
|
|
@ -1762,6 +1781,7 @@ async def test_semantic_filter_hook_fails_closed_on_context_window_error():
|
|||
print("✅ Hook fails closed with actionable 400 on context window overflow")
|
||||
|
||||
|
||||
@requires_semantic_router
|
||||
@pytest.mark.asyncio
|
||||
async def test_semantic_filter_hook_fails_closed_on_expanded_tools_context_window_error():
|
||||
"""
|
||||
|
|
@ -1828,6 +1848,7 @@ async def test_semantic_filter_hook_fails_closed_on_expanded_tools_context_windo
|
|||
print("✅ Expansion path fails closed with actionable 400 on context window overflow")
|
||||
|
||||
|
||||
@requires_semantic_router
|
||||
@pytest.mark.asyncio
|
||||
async def test_semantic_filter_hook_ignores_build_error_for_native_only_tools():
|
||||
"""
|
||||
|
|
@ -2018,6 +2039,7 @@ def _weather_tool():
|
|||
)
|
||||
|
||||
|
||||
@requires_semantic_router
|
||||
@pytest.mark.asyncio
|
||||
async def test_filter_indexes_request_tools_when_startup_index_is_empty():
|
||||
"""
|
||||
|
|
@ -2042,6 +2064,7 @@ async def test_filter_indexes_request_tools_when_startup_index_is_empty():
|
|||
print("✅ Empty startup index is built from authed request-time tools")
|
||||
|
||||
|
||||
@requires_semantic_router
|
||||
@pytest.mark.asyncio
|
||||
async def test_filter_indexes_tools_missing_from_partial_index():
|
||||
"""
|
||||
|
|
@ -2070,6 +2093,7 @@ async def test_filter_indexes_tools_missing_from_partial_index():
|
|||
print("✅ Partial startup index is completed from request-time tools, embedding each tool once")
|
||||
|
||||
|
||||
@requires_semantic_router
|
||||
@pytest.mark.asyncio
|
||||
async def test_filter_fails_open_when_matches_are_not_in_available_tools():
|
||||
"""
|
||||
|
|
@ -2093,6 +2117,7 @@ async def test_filter_fails_open_when_matches_are_not_in_available_tools():
|
|||
print("✅ Matches outside available_tools fail open instead of dropping every tool")
|
||||
|
||||
|
||||
@requires_semantic_router
|
||||
@pytest.mark.asyncio
|
||||
async def test_request_time_context_window_error_is_request_scoped():
|
||||
"""
|
||||
|
|
@ -2129,6 +2154,7 @@ async def test_request_time_context_window_error_is_request_scoped():
|
|||
print("✅ Request-time context window overflow is scoped to the request, not the worker")
|
||||
|
||||
|
||||
@requires_semantic_router
|
||||
@pytest.mark.asyncio
|
||||
async def test_foreign_index_routes_cannot_displace_available_tools():
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ Covers:
|
|||
I) async path propagates optional params to downstream handler
|
||||
"""
|
||||
|
||||
from importlib import import_module
|
||||
import asyncio
|
||||
from typing import List, cast
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
|
@ -62,23 +63,20 @@ def _provider_by_model(model: str, **_: object) -> tuple[str, str, None, None]:
|
|||
def _patch_responses_dispatch():
|
||||
"""Patch everything after the prompt management block so tests stay unit-level."""
|
||||
return [
|
||||
patch(
|
||||
"litellm.responses.main.litellm.get_llm_provider",
|
||||
patch.object(
|
||||
import_module("litellm.responses.main").litellm, "get_llm_provider",
|
||||
side_effect=_provider_by_model,
|
||||
),
|
||||
patch(
|
||||
"litellm.responses.mcp.litellm_proxy_mcp_handler."
|
||||
"LiteLLM_Proxy_MCP_Handler._should_use_litellm_mcp_gateway",
|
||||
patch.object(
|
||||
import_module("litellm.responses.mcp.litellm_proxy_mcp_handler").LiteLLM_Proxy_MCP_Handler, "_should_use_litellm_mcp_gateway",
|
||||
return_value=False,
|
||||
),
|
||||
patch(
|
||||
"litellm.responses.main.ProviderConfigManager"
|
||||
".get_provider_responses_api_config",
|
||||
patch.object(
|
||||
import_module("litellm.responses.main").ProviderConfigManager, "get_provider_responses_api_config",
|
||||
return_value=None,
|
||||
),
|
||||
patch(
|
||||
"litellm.responses.main.litellm_completion_transformation_handler"
|
||||
".response_api_handler",
|
||||
patch.object(
|
||||
import_module("litellm.responses.main").litellm_completion_transformation_handler, "response_api_handler",
|
||||
return_value=MagicMock(),
|
||||
),
|
||||
]
|
||||
|
|
@ -393,8 +391,8 @@ class TestResponsesAPIPromptManagement:
|
|||
|
||||
patches = _patch_responses_dispatch()
|
||||
with (
|
||||
patch(
|
||||
"litellm.responses.main.litellm.get_llm_provider",
|
||||
patch.object(
|
||||
import_module("litellm.responses.main").litellm, "get_llm_provider",
|
||||
side_effect=_provider_by_model,
|
||||
),
|
||||
patches[1],
|
||||
|
|
@ -599,8 +597,8 @@ def test_sync_prompt_swap_resolves_credentials_for_swapped_provider(monkeypatch:
|
|||
|
||||
monkeypatch.setenv("XAI_API_KEY", "sk-xai-test")
|
||||
logging_obj = _make_logging_obj("gpt-4o-mini", [{"role": "user", "content": "hi"}])
|
||||
with patch( # test-quality-ok: handler boundary stub proves creds resolve for the swapped provider without network
|
||||
"litellm.responses.main.base_llm_http_handler.response_api_handler", return_value=MagicMock()
|
||||
with patch.object( # test-quality-ok: handler boundary stub proves creds resolve for the swapped provider without network
|
||||
import_module("litellm.responses.main").base_llm_http_handler, "response_api_handler", return_value=MagicMock()
|
||||
) as mock_handler:
|
||||
litellm.responses(input="hi", model="xai/grok-4", prompt_id="p1", litellm_logging_obj=logging_obj)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from importlib import import_module
|
||||
import base64
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
|
|
@ -580,12 +581,12 @@ def test_responses_extra_body_forwarded_to_completion_transformation_handler():
|
|||
so it was silently dropped.
|
||||
"""
|
||||
with (
|
||||
patch(
|
||||
"litellm.responses.main.ProviderConfigManager.get_provider_responses_api_config",
|
||||
patch.object(
|
||||
import_module("litellm.responses.main").ProviderConfigManager, "get_provider_responses_api_config",
|
||||
return_value=None,
|
||||
),
|
||||
patch(
|
||||
"litellm.responses.main.litellm_completion_transformation_handler.response_api_handler",
|
||||
patch.object(
|
||||
import_module("litellm.responses.main").litellm_completion_transformation_handler, "response_api_handler",
|
||||
) as mock_handler,
|
||||
):
|
||||
mock_handler.return_value = MagicMock()
|
||||
|
|
@ -611,12 +612,12 @@ def test_responses_maps_reasoning_effort_from_litellm_params_to_reasoning():
|
|||
that cannot set extra_body.
|
||||
"""
|
||||
with (
|
||||
patch(
|
||||
"litellm.responses.main.ProviderConfigManager.get_provider_responses_api_config",
|
||||
patch.object(
|
||||
import_module("litellm.responses.main").ProviderConfigManager, "get_provider_responses_api_config",
|
||||
return_value=None,
|
||||
),
|
||||
patch(
|
||||
"litellm.responses.main.litellm_completion_transformation_handler.response_api_handler",
|
||||
patch.object(
|
||||
import_module("litellm.responses.main").litellm_completion_transformation_handler, "response_api_handler",
|
||||
) as mock_handler,
|
||||
):
|
||||
mock_handler.return_value = MagicMock()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from importlib import import_module
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
|
@ -148,8 +149,8 @@ class TestTextFormatConversion:
|
|||
incomplete_details=None,
|
||||
)
|
||||
|
||||
with patch(
|
||||
"litellm.responses.main.base_llm_http_handler.response_api_handler",
|
||||
with patch.object(
|
||||
import_module("litellm.responses.main").base_llm_http_handler, "response_api_handler",
|
||||
new=mock_handler,
|
||||
):
|
||||
litellm._turn_on_debug()
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ Tests the rule-based complexity scoring and tier assignment logic.
|
|||
|
||||
import asyncio
|
||||
import logging
|
||||
import sys
|
||||
from typing import Dict, List
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -33,6 +33,6 @@
|
|||
"limit": 5514
|
||||
},
|
||||
"LIT012": {
|
||||
"limit": 4489
|
||||
"limit": 4487
|
||||
}
|
||||
}
|
||||
|
|
|
|||
68
uv.lock
generated
68
uv.lock
generated
|
|
@ -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]]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue