mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-11 03:38:38 +00:00
* test(proxy): stop the proxy_server app fixture leaking LITELLM_LOG The session app fixture set LITELLM_LOG=ERROR with os.environ.setdefault and never removed it, so later tests on the same xdist worker inherited it. test_drop_params_env_var spawns a subprocess with os.environ and lost the warning it asserts on. Scope the variable to the import with a MonkeyPatch context * test(secret-detection): give the hand-built redaction request an ASGI path Since #43975 _read_request_body checks the route path via request.scope, and a scope without path raised KeyError that was swallowed into an empty body, so chat_completion failed with a missing messages parameter. Real ASGI scopes always carry path * test(integration): isolate litellm callback lists per sdk test usage-based-routing-v2 Routers register their selector in litellm.callbacks and nothing removes it, not even Router.reset(). The counter TTL and Redis service metrics tests left their selectors behind, and the next usage routing test ran their pre-call checks against its own rpm=1 deployments, raising "Deployment over defined rpm limit". An autouse fixture now gives each sdk test copies of the callback lists and restores the originals afterwards * test(integration): keep the owner-lookup fault proxy off the shared read replica The owned proxy points DATABASE_URL at a scratch database but inherited DATABASE_URL_READ_REPLICA from the replica job, so auth read the shared database and rejected the freshly created key with token_not_found_in_db. Drop the replica variable like the other scratch-database owned proxies * test(integration): request every seeded key in the team owner breakdown The aggregated team activity endpoint now caps breakdown.api_keys at the top 100 keys by default (#43398), so the 300 seeded keys came back as 100 rows. The test guarantees each key is reported with its own owner, so ask for an api_key_limit that covers all seeded keys * test(integration): give every owned Redis its own port in the redis-cache container On CircleCI every owned Redis ran on the fixed port 16379 inside the shared redis-cache container. When an earlier server still held that port, the new one failed to bind, readiness pinged the old server, the pidfile read failed and cleanup then reported "Owned Redis still serves after shutdown" Reserve an ephemeral port for the docker-exec path the same way the local binary path already does, and refuse to start when something already serves the chosen port so the failure names the real cause * test(e2e): skip the Vertex Mistral partner case the e2e project cannot reach The e2e Vertex project gets a 404 publisher model not found for vertex_ai/mistral-small-2503, so the case can only fail * test(e2e): skip the Vertex gpt-oss partner case the e2e project never serves vertex_ai/openai/gpt-oss-120b-maas has hit a 60s read timeout with no response headers on every run in the e2e Vertex project since the case was ported, and no other Vertex partner chat model passes there to switch to * test(e2e): check only stored message content for a leaked card number The Presidio spend-log check ran the card-number pattern over the whole serialized response, so a Luhn-valid usage.cost float (0.0003466000000000001) failed the streaming /v1/messages case although the stored content was <CREDIT_CARD>. The check now reads the content and text strings of the stored response, which is where a raw card would land, and still requires the placeholder there * test(e2e): assert the proxy decodes token-array embeddings for titan The port in #44120 carried over a legacy SDK-direct test that expected Bedrock to reject token ids with a 400. Through the proxy, /embeddings decodes token arrays to text for providers that cannot embed tokens, so titan answers 200. The test now sends a token array and its decoded sentence and requires the two vectors to match, which fails if the proxy stops decoding or decodes with the wrong tokenizer * test(e2e): run the Bedrock extended-thinking round trip on a model that honors enabled thinking us.anthropic.claude-sonnet-5-5 is adaptive-only, so litellm sends thinking.type=enabled with a 1024 budget as adaptive with low effort, and Bedrock returned no reasoning blocks on 5 of 5 identical Converse calls (boto3 direct agreed). us.anthropic.claude-sonnet-4-6 accepts the legacy shape verbatim and returned reasoning on 5 of 5. The non-thinking Bedrock case stays on sonnet-5-5 * test(proxy): stop unit modules forcing DEBUG logging into the event-loop lag tests Five tests/unit modules set verbose_proxy_logger to DEBUG at import, so every xdist worker that collected them logged the 2.4MB pass-through response from a worker thread, and secret redaction of that line held the GIL for ~0.8s+ inside the timed window. The lag tests now pin the LiteLLM loggers to WARNING and freeze gc while timing, and the module-level DEBUG overrides are removed * test(e2e): cite the tokenizer and date behind the titan token-array fixture * test(e2e): let migration seed replicas finish their request-log indexes before cloning Since #43948 a serving proxy builds the two LiteLLM_SpendLogs indexes on a background thread after it reports ready. The seed fixtures stopped the replica at readiness, so every cloned legacy database lacked an index no real deployment would be missing, and the v2 baseline diff refused it. Seeds now wait until both indexes exist and are valid in the database's schema * test(passthrough): give the pass-through MockRequest an httpx URL and ASGI scope #43626 made get_request_route read request.scope during pass-through kwarg setup; the MockRequest in tests/unit/passthrough had neither a scope nor a URL object, so both stream-param tests raised before reaching the code they check. Mirrors the repair #43626 made to the tests/pass_through_unit_tests fake * test(integration): ignore foreign allow_all_keys MCP servers in the access matrix tool list test_toolset_gateway_url_serves_a_team_granted_toolset_to_a_key_without_its_own_grant (#43908) registers an allow_all_keys server on the shared gateway, and allow_all_keys servers are listed to every key by design, so a matrix case running on another xdist worker at the same time saw its tools. The matrix now drops tools of allow_all_keys servers it did not create, read from LiteLLM_MCPServerTable before and after listing, and still compares everything else exactly
538 lines
17 KiB
Python
538 lines
17 KiB
Python
"""Shared fixtures for tests/unit/proxy/proxy_server/.
|
|
|
|
All fixtures and helpers used by PR1/PR2/PR3 test files live here. Do NOT
|
|
add fixtures inside individual test files. If a fixture is missing, add it
|
|
here and update the Notion plan.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import contextlib
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Any, AsyncIterator, Callable, Dict, Iterator, List, Optional
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
import pytest
|
|
|
|
# Repo root, anchored to this file (not CWD) so the path is correct no
|
|
# matter where pytest is invoked from. With the project installed via
|
|
# uv this is defensive — `litellm` already resolves through site-packages
|
|
# — but it lets the harness work in editable-source layouts too.
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[4]))
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# normalize() — used by every dict-equality assertion to scrub volatile fields
|
|
# ---------------------------------------------------------------------------
|
|
|
|
VOLATILE_KEYS = frozenset(
|
|
{
|
|
"created_at",
|
|
"updated_at",
|
|
"key",
|
|
"token",
|
|
"id",
|
|
"request_id",
|
|
"expires",
|
|
"expires_at",
|
|
"litellm_call_id",
|
|
"key_alias",
|
|
"created",
|
|
}
|
|
)
|
|
|
|
|
|
def normalize(data: Any, volatile: frozenset[str] = VOLATILE_KEYS) -> Any:
|
|
"""Replace volatile field values with "<VOLATILE>" so dict equality works.
|
|
|
|
Recursive over dicts and lists. Pass an explicit ``volatile`` set to
|
|
extend or override the default.
|
|
"""
|
|
if isinstance(data, dict):
|
|
return {
|
|
k: ("<VOLATILE>" if k in volatile else normalize(v, volatile))
|
|
for k, v in data.items()
|
|
}
|
|
if isinstance(data, list):
|
|
return [normalize(v, volatile) for v in data]
|
|
return data
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# app + client — session-scoped so app import + TestClient setup amortize
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def app():
|
|
"""Return the proxy_server FastAPI app with lifespan effectively disabled.
|
|
|
|
TestClient used WITHOUT the ``with`` context manager skips the lifespan,
|
|
so the startup event (DB connect, Router init, OTEL setup) never fires.
|
|
Module import still runs once; module-level globals are harmless.
|
|
"""
|
|
with pytest.MonkeyPatch.context() as environment:
|
|
environment.setenv("LITELLM_LOG", os.environ.get("LITELLM_LOG", "ERROR"))
|
|
from litellm.proxy.proxy_server import app as _app
|
|
|
|
return _app
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def client(app):
|
|
"""TestClient wrapping the session app.
|
|
|
|
NOT entered as a context manager — lifespan does not fire. Tests that
|
|
require a real lifespan should use a function-scoped TestClient with
|
|
a ``with`` block locally and accept the per-test cost.
|
|
"""
|
|
from fastapi.testclient import TestClient
|
|
|
|
return TestClient(app, raise_server_exceptions=False)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# mock_prisma — function-scoped MagicMock with the common table methods stubbed
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# Tables most-touched by proxy_server.py routes. Add to this list if a
|
|
# test discovers a missing table.
|
|
_PRISMA_TABLES: List[str] = [
|
|
"litellm_verificationtoken",
|
|
"litellm_teamtable",
|
|
"litellm_usertable",
|
|
"litellm_endusertable",
|
|
"litellm_organizationtable",
|
|
"litellm_organizationmembership",
|
|
"litellm_proxymodeltable",
|
|
"litellm_modeltable",
|
|
"litellm_budgettable",
|
|
"litellm_spendlogs",
|
|
"litellm_invitationlink",
|
|
"litellm_credentialstable",
|
|
"litellm_mcpservertable",
|
|
"litellm_objectpermissiontable",
|
|
"litellm_configtable",
|
|
"litellm_audit_log",
|
|
"litellm_dailyuserspend",
|
|
"litellm_dailyteamspend",
|
|
"litellm_dailytagspend",
|
|
"litellm_managed_object_table",
|
|
"litellm_managed_vector_stores_table",
|
|
"litellm_promptstable",
|
|
"litellm_guardrailstable",
|
|
"litellm_managed_files",
|
|
"litellm_session_token_table",
|
|
"litellm_passthrough_endpoint_table",
|
|
"litellm_cron_job",
|
|
"litellm_passthrough_logs",
|
|
"litellm_health_check_table",
|
|
"litellm_mcpusercredentials",
|
|
]
|
|
|
|
|
|
def _make_table_mock() -> MagicMock:
|
|
table = MagicMock()
|
|
table.find_unique = AsyncMock(return_value=None)
|
|
table.find_many = AsyncMock(return_value=[])
|
|
table.find_first = AsyncMock(return_value=None)
|
|
table.create = AsyncMock()
|
|
table.create_many = AsyncMock()
|
|
table.update = AsyncMock()
|
|
table.update_many = AsyncMock()
|
|
table.upsert = AsyncMock()
|
|
table.delete = AsyncMock()
|
|
table.delete_many = AsyncMock()
|
|
table.count = AsyncMock(return_value=0)
|
|
table.group_by = AsyncMock(return_value=[])
|
|
table.aggregate = AsyncMock(return_value={})
|
|
return table
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_prisma() -> MagicMock:
|
|
"""MagicMock prisma_client with .db.<table> methods stubbed.
|
|
|
|
Default returns: find_unique/find_first -> None, find_many/group_by -> [],
|
|
count -> 0. Override in a test with::
|
|
|
|
mock_prisma.db.litellm_teamtable.find_unique.return_value = ...
|
|
"""
|
|
client_mock = MagicMock()
|
|
client_mock.db = MagicMock()
|
|
client_mock.connect = AsyncMock()
|
|
client_mock.disconnect = AsyncMock()
|
|
client_mock.health_check = AsyncMock(return_value=True)
|
|
for table_name in _PRISMA_TABLES:
|
|
setattr(client_mock.db, table_name, _make_table_mock())
|
|
return client_mock
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# auth_as — context manager that overrides user_api_key_auth dependency
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.fixture
|
|
def auth_as(app) -> Callable[..., contextlib.AbstractContextManager]:
|
|
"""Context manager that overrides ``user_api_key_auth`` for a role.
|
|
|
|
Usage::
|
|
|
|
def test_admin_only(client, auth_as):
|
|
from litellm.proxy._types import LitellmUserRoles
|
|
with auth_as(LitellmUserRoles.PROXY_ADMIN):
|
|
response = client.get("/some/admin/route")
|
|
assert response.status_code == 200
|
|
|
|
Outside the ``with`` block the override is removed so other tests see
|
|
the real dependency.
|
|
"""
|
|
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
|
|
|
|
@contextlib.contextmanager
|
|
def _auth_as(
|
|
role: Any = None,
|
|
user_id: str = "test-user-id",
|
|
team_id: Optional[str] = None,
|
|
api_key: str = "sk-test-key",
|
|
**kwargs: Any,
|
|
) -> Iterator[Any]:
|
|
from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth
|
|
|
|
if role is None:
|
|
role = LitellmUserRoles.PROXY_ADMIN
|
|
|
|
fake_auth = UserAPIKeyAuth(
|
|
api_key=api_key,
|
|
user_id=user_id,
|
|
team_id=team_id,
|
|
user_role=role,
|
|
**kwargs,
|
|
)
|
|
|
|
async def _override() -> UserAPIKeyAuth:
|
|
return fake_auth
|
|
|
|
previous = app.dependency_overrides.get(user_api_key_auth)
|
|
app.dependency_overrides[user_api_key_auth] = _override
|
|
try:
|
|
yield fake_auth
|
|
finally:
|
|
if previous is None:
|
|
app.dependency_overrides.pop(user_api_key_auth, None)
|
|
else:
|
|
app.dependency_overrides[user_api_key_auth] = previous
|
|
|
|
return _auth_as
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Response builders — used by mock_router for parametrized responses
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def make_acompletion_response(
|
|
model: str = "gpt-4",
|
|
messages: Optional[List[Dict[str, Any]]] = None,
|
|
stream: bool = False,
|
|
tools: Optional[List[Dict[str, Any]]] = None,
|
|
content: str = "Hello from mock",
|
|
**kwargs: Any,
|
|
) -> Any:
|
|
"""Build a deterministic chat-completion response.
|
|
|
|
Returns:
|
|
- An async generator when ``stream=True``
|
|
- A tool-call shape when ``tools`` is non-empty
|
|
- A plain text response otherwise
|
|
"""
|
|
from litellm.types.utils import (
|
|
ChatCompletionMessageToolCall,
|
|
Choices,
|
|
Function,
|
|
Message,
|
|
ModelResponse,
|
|
Usage,
|
|
)
|
|
|
|
if stream:
|
|
return _stream_chunks(model=model, content=content)
|
|
|
|
if tools:
|
|
tool_name = tools[0].get("function", {}).get("name", "fake_tool")
|
|
message = Message(
|
|
role="assistant",
|
|
content=None,
|
|
tool_calls=[
|
|
ChatCompletionMessageToolCall(
|
|
id="call_test",
|
|
type="function",
|
|
function=Function(name=tool_name, arguments="{}"),
|
|
)
|
|
],
|
|
)
|
|
else:
|
|
message = Message(role="assistant", content=content)
|
|
|
|
return ModelResponse(
|
|
id="chatcmpl-test",
|
|
choices=[Choices(finish_reason="stop", index=0, message=message)],
|
|
created=0,
|
|
model=model,
|
|
object="chat.completion",
|
|
usage=Usage(prompt_tokens=1, completion_tokens=1, total_tokens=2),
|
|
)
|
|
|
|
|
|
async def _stream_chunks(
|
|
model: str = "gpt-4", content: str = "Hi"
|
|
) -> AsyncIterator[Any]:
|
|
from litellm.types.utils import (
|
|
Delta,
|
|
ModelResponseStream,
|
|
StreamingChoices,
|
|
)
|
|
|
|
for piece in [content, ""]:
|
|
yield ModelResponseStream(
|
|
id="chatcmpl-test",
|
|
choices=[
|
|
StreamingChoices(
|
|
finish_reason=None if piece else "stop",
|
|
index=0,
|
|
delta=Delta(content=piece or None, role="assistant"),
|
|
)
|
|
],
|
|
created=0,
|
|
model=model,
|
|
object="chat.completion.chunk",
|
|
)
|
|
|
|
|
|
def make_embedding_response(
|
|
model: str = "text-embedding-ada-002",
|
|
input: Any = None,
|
|
dimensions: int = 8,
|
|
**kwargs: Any,
|
|
) -> Any:
|
|
from litellm.types.utils import EmbeddingResponse
|
|
|
|
if isinstance(input, list):
|
|
n = len(input)
|
|
elif input is None:
|
|
n = 1
|
|
else:
|
|
n = 1
|
|
return EmbeddingResponse(
|
|
model=model,
|
|
data=[
|
|
{"embedding": [0.0] * dimensions, "index": i, "object": "embedding"}
|
|
for i in range(n)
|
|
],
|
|
object="list",
|
|
usage={"prompt_tokens": n, "total_tokens": n},
|
|
)
|
|
|
|
|
|
def make_image_response(model: str = "dall-e-3", **kwargs: Any) -> Any:
|
|
from litellm.types.utils import ImageResponse
|
|
|
|
return ImageResponse(
|
|
created=0,
|
|
data=[{"url": "https://example.invalid/image.png"}],
|
|
)
|
|
|
|
|
|
def make_speech_response(**kwargs: Any) -> bytes:
|
|
"""Return a fake audio blob. The route serializes bytes to a streaming response."""
|
|
return b"\x00" * 128
|
|
|
|
|
|
def make_transcription_response(**kwargs: Any) -> Any:
|
|
from litellm.types.utils import TranscriptionResponse
|
|
|
|
return TranscriptionResponse(text="hello world")
|
|
|
|
|
|
def make_moderation_response(**kwargs: Any) -> Dict[str, Any]:
|
|
return {
|
|
"id": "modr-test",
|
|
"model": "text-moderation-latest",
|
|
"results": [
|
|
{
|
|
"flagged": False,
|
|
"categories": {},
|
|
"category_scores": {},
|
|
}
|
|
],
|
|
}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# mock_router — fake Router with all the *async* call surfaces stubbed
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_router() -> MagicMock:
|
|
"""A MagicMock standing in for ``llm_router`` with parametrized responses."""
|
|
|
|
async def _acompletion(model: str = "gpt-4", messages=None, **kwargs):
|
|
return make_acompletion_response(model=model, messages=messages, **kwargs)
|
|
|
|
async def _aembedding(model: str = "text-embedding-ada-002", input=None, **kwargs):
|
|
return make_embedding_response(model=model, input=input, **kwargs)
|
|
|
|
async def _aimage_generation(**kwargs):
|
|
return make_image_response(**kwargs)
|
|
|
|
async def _aspeech(**kwargs):
|
|
return make_speech_response(**kwargs)
|
|
|
|
async def _atranscription(**kwargs):
|
|
return make_transcription_response(**kwargs)
|
|
|
|
async def _amoderation(**kwargs):
|
|
return make_moderation_response(**kwargs)
|
|
|
|
router = MagicMock()
|
|
router.acompletion = AsyncMock(side_effect=_acompletion)
|
|
router.aembedding = AsyncMock(side_effect=_aembedding)
|
|
router.aimage_generation = AsyncMock(side_effect=_aimage_generation)
|
|
router.aspeech = AsyncMock(side_effect=_aspeech)
|
|
router.atranscription = AsyncMock(side_effect=_atranscription)
|
|
router.amoderation = AsyncMock(side_effect=_amoderation)
|
|
router.model_list = [
|
|
{"model_name": "gpt-4", "litellm_params": {"model": "gpt-4"}},
|
|
{
|
|
"model_name": "claude-sonnet",
|
|
"litellm_params": {"model": "anthropic/claude-3-5-sonnet-latest"},
|
|
},
|
|
{
|
|
"model_name": "bedrock-claude",
|
|
"litellm_params": {"model": "bedrock/anthropic.claude-3-5-sonnet"},
|
|
},
|
|
]
|
|
router.model_names = ["gpt-4", "claude-sonnet", "bedrock-claude"]
|
|
router.get_model_list = MagicMock(return_value=router.model_list)
|
|
return router
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# mock_callbacks_disabled — autouse: zero out global callbacks per test
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def mock_callbacks_disabled(monkeypatch) -> None:
|
|
"""Wipe ``litellm.callbacks`` and friends so tests don't leak side effects."""
|
|
import litellm
|
|
|
|
for attr in (
|
|
"callbacks",
|
|
"success_callback",
|
|
"failure_callback",
|
|
"_async_success_callback",
|
|
"_async_failure_callback",
|
|
"input_callback",
|
|
"service_callback",
|
|
):
|
|
if hasattr(litellm, attr):
|
|
monkeypatch.setattr(litellm, attr, [], raising=False)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Builders for DB-like objects (used by routes that load from DB)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def make_user(
|
|
user_id: str = "user-test",
|
|
role: Any = None,
|
|
teams: Optional[List[str]] = None,
|
|
max_budget: Optional[float] = None,
|
|
spend: float = 0.0,
|
|
**kwargs: Any,
|
|
) -> Any:
|
|
from litellm.proxy._types import LiteLLM_UserTable, LitellmUserRoles
|
|
|
|
if role is None:
|
|
role = LitellmUserRoles.INTERNAL_USER
|
|
|
|
return LiteLLM_UserTable(
|
|
user_id=user_id,
|
|
user_role=role,
|
|
teams=teams or [],
|
|
max_budget=max_budget,
|
|
spend=spend,
|
|
**kwargs,
|
|
)
|
|
|
|
|
|
def make_team(
|
|
team_id: str = "team-test",
|
|
team_alias: str = "Test Team",
|
|
max_budget: Optional[float] = None,
|
|
spend: float = 0.0,
|
|
members_with_roles: Optional[List[Dict[str, Any]]] = None,
|
|
**kwargs: Any,
|
|
) -> Any:
|
|
from litellm.proxy._types import LiteLLM_TeamTable
|
|
|
|
return LiteLLM_TeamTable(
|
|
team_id=team_id,
|
|
team_alias=team_alias,
|
|
max_budget=max_budget,
|
|
spend=spend,
|
|
members_with_roles=members_with_roles or [],
|
|
**kwargs,
|
|
)
|
|
|
|
|
|
def make_key(
|
|
token: str = "hashed-test-key",
|
|
key_alias: Optional[str] = None,
|
|
team_id: Optional[str] = None,
|
|
user_id: str = "user-test",
|
|
spend: float = 0.0,
|
|
max_budget: Optional[float] = None,
|
|
**kwargs: Any,
|
|
) -> Any:
|
|
from litellm.proxy._types import LiteLLM_VerificationToken
|
|
|
|
return LiteLLM_VerificationToken(
|
|
token=token,
|
|
key_alias=key_alias,
|
|
team_id=team_id,
|
|
user_id=user_id,
|
|
spend=spend,
|
|
max_budget=max_budget,
|
|
**kwargs,
|
|
)
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def reset_login_throttle(monkeypatch):
|
|
"""Clear the Admin UI failed-login counters between tests.
|
|
|
|
`client` is session scoped and the counters live in shared module stores with a 300s block
|
|
window, so without this a failed sign-in test could block unrelated tests later.
|
|
Only the throttle's own keys are removed, so other cache entries remain untouched.
|
|
"""
|
|
from litellm.constants import LOGIN_THROTTLE_CACHE_KEY_PREFIX
|
|
from litellm.proxy import proxy_server as ps
|
|
from litellm.proxy.auth.login_throttle import _BLOCKS, _COUNTERS
|
|
|
|
def _drop_throttle_keys() -> None:
|
|
for store in (_COUNTERS, _BLOCKS):
|
|
for key in tuple(store.cache_dict) + tuple(store.ttl_dict):
|
|
if key.startswith(LOGIN_THROTTLE_CACHE_KEY_PREFIX):
|
|
store.delete_cache(key)
|
|
|
|
monkeypatch.setattr(ps, "redis_usage_cache", None)
|
|
_drop_throttle_keys()
|
|
yield _drop_throttle_keys
|
|
_drop_throttle_keys()
|