refactor(proxy): inject settings and Redis cache into LoginThrottle.from_request

Removes the runtime import of proxy_server from login_throttle so the
throttle module no longer participates in the import cycle CodeQL
flagged (py/cyclic-import). Callers pass general_settings and
redis_usage_cache explicitly; behavior is unchanged.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
yucheng 2026-09-13 09:00:20 +00:00
parent 9ca6307b9d
commit ce82033f62
3 changed files with 27 additions and 34 deletions

View file

@ -11,7 +11,7 @@ startup and can be reassigned later.
import asyncio
import hashlib
from collections.abc import Awaitable, Callable
from collections.abc import Awaitable, Callable, Mapping
from dataclasses import dataclass
from functools import cache
from types import MappingProxyType
@ -134,10 +134,10 @@ class LoginThrottle:
enabled: bool = True
@classmethod
def from_request(cls, request: Request) -> "LoginThrottle":
"""Build the throttle for this request from the live proxy settings and caches."""
from litellm.proxy.proxy_server import general_settings, redis_usage_cache
def from_request(
cls, request: Request, general_settings: Mapping[str, object] | None, redis_cache: RedisCache | None
) -> "LoginThrottle":
"""Build the throttle for this request from the proxy's general_settings and shared Redis cache."""
settings: Final = general_settings or _NO_SETTINGS
cidrs: Final = normalize_cidr_ranges(
settings.get(TRUSTED_PROXY_RANGES_KEY), setting_name=TRUSTED_PROXY_RANGES_KEY
@ -167,7 +167,7 @@ class LoginThrottle:
),
username_cache=_FAILED_LOGIN_USERNAME_CACHE,
source_cache=_FAILED_LOGIN_SOURCE_CACHE,
redis_cache=redis_usage_cache,
redis_cache=redis_cache,
enabled=not _rate_limit_disabled(),
)

View file

@ -15853,7 +15853,7 @@ async def login(request: Request):
password=password,
master_key=master_key,
prisma_client=prisma_client,
throttle=LoginThrottle.from_request(request),
throttle=LoginThrottle.from_request(request, general_settings, redis_usage_cache),
general_settings=general_settings,
)
except ProxyException as exc:
@ -15946,7 +15946,7 @@ async def login_v2(request: Request):
password=password,
master_key=master_key,
prisma_client=prisma_client,
throttle=LoginThrottle.from_request(request),
throttle=LoginThrottle.from_request(request, general_settings, redis_usage_cache),
general_settings=general_settings,
)
@ -16018,7 +16018,7 @@ async def login_v3(request: Request):
password=password,
master_key=master_key,
prisma_client=prisma_client,
throttle=LoginThrottle.from_request(request),
throttle=LoginThrottle.from_request(request, general_settings, redis_usage_cache),
general_settings=general_settings,
)

View file

@ -1348,7 +1348,6 @@ async def test_counters_do_not_share_the_key_authentication_cache(monkeypatch):
monkeypatch.setenv("UI_USERNAME", "admin")
monkeypatch.setenv("UI_PASSWORD", "right")
monkeypatch.setattr(ps, "redis_usage_cache", None)
auth_cache_keys_before = set(ps.user_api_key_cache.in_memory_cache.cache_dict)
@ -1356,7 +1355,7 @@ async def test_counters_do_not_share_the_key_authentication_cache(monkeypatch):
request.headers = {}
request.client = MagicMock()
request.client.host = "1.2.3.4"
throttle = LoginThrottle.from_request(request)
throttle = LoginThrottle.from_request(request, general_settings={}, redis_cache=None)
for i in range(25):
with pytest.raises(ProxyException, match="Invalid credentials"):
@ -1368,30 +1367,28 @@ async def test_counters_do_not_share_the_key_authentication_cache(monkeypatch):
)
def test_settings_that_arrive_as_environment_strings_are_honored(monkeypatch):
def test_settings_that_arrive_as_environment_strings_are_honored():
"""An `os.environ/VAR` reference in general_settings resolves to a string, not an int.
Regression: a digit string fell back to the default with only a log line, so an operator
tightening the limits through environment substitution silently kept the stock ceilings.
"""
from litellm.proxy import proxy_server as ps
from litellm.proxy.auth.login_throttle import LoginThrottle
monkeypatch.setattr(
ps,
"general_settings",
{
"max_failed_login_attempts": "7",
"max_failed_login_attempts_per_source": " 70 ",
"failed_login_window_seconds": "not-a-number",
},
)
request = MagicMock()
request.headers = {}
request.client = MagicMock()
request.client.host = "1.2.3.4"
throttle = LoginThrottle.from_request(request)
throttle = LoginThrottle.from_request(
request,
general_settings={
"max_failed_login_attempts": "7",
"max_failed_login_attempts_per_source": " 70 ",
"failed_login_window_seconds": "not-a-number",
},
redis_cache=None,
)
assert throttle.max_attempts == 7
assert throttle.max_attempts_per_source == 70
@ -1404,41 +1401,37 @@ def test_the_disable_flag_is_read_once_not_per_login_attempt(monkeypatch):
With a hosted secret manager in read mode that is a synchronous network call per guess, so a
flood of wrong passwords could exhaust the secret manager even after the source was refused.
"""
from litellm.proxy import proxy_server as ps
from litellm.proxy.auth import login_throttle
reads: Final[list[str]] = [] # mutable-ok: test-only call recorder
monkeypatch.setattr(login_throttle, "get_secret_bool", lambda name, default: reads.append(name) or default)
login_throttle._rate_limit_disabled.cache_clear()
monkeypatch.setattr(ps, "general_settings", {})
request = MagicMock()
request.headers = {}
request.client = MagicMock()
request.client.host = "1.2.3.4"
for _ in range(50):
assert login_throttle.LoginThrottle.from_request(request).enabled is True
assert login_throttle.LoginThrottle.from_request(request, general_settings={}, redis_cache=None).enabled is True
login_throttle._rate_limit_disabled.cache_clear()
assert reads == ["LITELLM_DISABLE_LOGIN_RATE_LIMIT"]
def test_a_negative_or_boolean_setting_falls_back_to_the_default(monkeypatch):
def test_a_negative_or_boolean_setting_falls_back_to_the_default():
"""A limit below one would refuse everyone; a bool is a typo, not a count."""
from litellm.proxy import proxy_server as ps
from litellm.proxy.auth.login_throttle import LoginThrottle
monkeypatch.setattr(
ps,
"general_settings",
{"max_failed_login_attempts": "-7", "max_failed_login_attempts_per_source": True},
)
request = MagicMock()
request.headers = {}
request.client = MagicMock()
request.client.host = "1.2.3.4"
throttle = LoginThrottle.from_request(request)
throttle = LoginThrottle.from_request(
request,
general_settings={"max_failed_login_attempts": "-7", "max_failed_login_attempts_per_source": True},
redis_cache=None,
)
assert throttle.max_attempts == 50
assert throttle.max_attempts_per_source == 250