mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-15 23:31:29 +00:00
fix(proxy): attach Redis to auth cache by default so CLI SSO login works multi-worker
The CLI SSO login flow stores its pending session in user_api_key_cache, which only attached to Redis when litellm_settings.enable_redis_auth_cache was explicitly set. On any multi-worker/multi-replica deployment /sso/cli/start and the browser's /sso/key/generate could land on different workers, so the second worker's in-memory-only cache never saw the session and the login failed with "Invalid CLI login session". Attach the coordination Redis to user_api_key_cache by default whenever a coordination Redis exists, turning enable_redis_auth_cache into an opt-out (set it to false to keep auth lookups per-worker/DB-only). Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
2166608eb8
commit
22b4d55863
2 changed files with 79 additions and 20 deletions
|
|
@ -3634,7 +3634,7 @@ def _attach_redis_usage_cache(redis_cache: RedisCache, enable_redis_auth_cache:
|
|||
"""
|
||||
Wires an established coordination Redis into the proxy-level caches that
|
||||
consume it directly: the spend counter cache, the cluster-wide config
|
||||
cache, and (only when opted in) the virtual-key auth cache.
|
||||
cache, and (unless explicitly opted out) the virtual-key auth cache.
|
||||
"""
|
||||
spend_counter_cache.attach_redis_cache(
|
||||
redis_cache,
|
||||
|
|
@ -3646,16 +3646,18 @@ def _attach_redis_usage_cache(redis_cache: RedisCache, enable_redis_auth_cache:
|
|||
default_redis_ttl=litellm.default_redis_ttl,
|
||||
)
|
||||
verbose_proxy_logger.info(
|
||||
"enable_redis_auth_cache=True: attached Redis to "
|
||||
"user_api_key_cache — virtual-key lookups are now "
|
||||
"shared across all proxy workers."
|
||||
"attached Redis to user_api_key_cache; virtual-key lookups and "
|
||||
"short-lived cross-worker state (e.g. CLI SSO login sessions) are "
|
||||
"now shared across all proxy workers. Set "
|
||||
"litellm_settings.enable_redis_auth_cache: false to opt out and "
|
||||
"keep the auth cache per-worker/DB-only."
|
||||
)
|
||||
else:
|
||||
verbose_proxy_logger.info(
|
||||
"enable_redis_auth_cache is not set: user_api_key_cache "
|
||||
"remains in-memory only (per-worker). Set "
|
||||
"litellm_settings.enable_redis_auth_cache: true to share "
|
||||
"the auth cache across workers and reduce DB load."
|
||||
"enable_redis_auth_cache is set to false: user_api_key_cache "
|
||||
"remains in-memory only (per-worker). Cross-worker features that "
|
||||
"rely on it (e.g. CLI SSO login) will not work on multi-worker "
|
||||
"deployments."
|
||||
)
|
||||
litellm_config_cache.redis_cache = redis_cache
|
||||
|
||||
|
|
@ -3884,7 +3886,7 @@ class ProxyConfig:
|
|||
coordination_redis_cache = _build_redis_usage_cache(coordination_params.model_dump(exclude_none=True))
|
||||
_attach_redis_usage_cache(
|
||||
coordination_redis_cache,
|
||||
enable_redis_auth_cache=litellm_settings.get("enable_redis_auth_cache", False) is True,
|
||||
enable_redis_auth_cache=litellm_settings.get("enable_redis_auth_cache", True) is not False,
|
||||
)
|
||||
verbose_proxy_logger.info(
|
||||
"coordination_redis: using a standalone Redis from general_settings "
|
||||
|
|
@ -3895,7 +3897,7 @@ class ProxyConfig:
|
|||
def _init_cache(
|
||||
self,
|
||||
cache_params: dict,
|
||||
enable_redis_auth_cache: bool = False,
|
||||
enable_redis_auth_cache: bool = True,
|
||||
) -> RedisCache | None:
|
||||
"""
|
||||
Initializes the response cache and resolves the coordination Redis.
|
||||
|
|
@ -4269,7 +4271,7 @@ class ProxyConfig:
|
|||
_set_redis_usage_cache(
|
||||
self._init_cache(
|
||||
cache_params=cache_params,
|
||||
enable_redis_auth_cache=litellm_settings.get("enable_redis_auth_cache", False) is True,
|
||||
enable_redis_auth_cache=litellm_settings.get("enable_redis_auth_cache", True) is not False,
|
||||
)
|
||||
)
|
||||
if litellm.cache is not None:
|
||||
|
|
@ -7445,7 +7447,7 @@ class ProxyStartupEvent:
|
|||
coordination_redis_cache = _build_redis_usage_cache(coordination_params.model_dump(exclude_none=True))
|
||||
_attach_redis_usage_cache(
|
||||
coordination_redis_cache,
|
||||
enable_redis_auth_cache=litellm_settings.get("enable_redis_auth_cache", False) is True,
|
||||
enable_redis_auth_cache=litellm_settings.get("enable_redis_auth_cache", True) is not False,
|
||||
)
|
||||
if llm_router is not None and llm_router.cache.redis_cache is None:
|
||||
llm_router._update_redis_cache(cache=coordination_redis_cache)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
"""
|
||||
Tests for the enable_redis_auth_cache litellm_settings flag.
|
||||
|
||||
Verifies that _init_cache attaches Redis to user_api_key_cache only when
|
||||
the flag is explicitly set to True, and leaves it in-memory-only otherwise.
|
||||
Verifies that _init_cache attaches Redis to user_api_key_cache by default
|
||||
whenever a coordination Redis exists, and only leaves it in-memory-only when
|
||||
the flag is explicitly set to False (opt-out). Also pins the cross-worker CLI
|
||||
SSO login regression from issue #33253: with the default settings, a login
|
||||
session written by one worker is readable by another.
|
||||
"""
|
||||
|
||||
from contextlib import contextmanager
|
||||
|
|
@ -65,7 +68,7 @@ def _patched_init_cache(litellm_settings: dict, cache_params: dict):
|
|||
fresh_user_cache = DualCache()
|
||||
fresh_spend_cache = DualCache()
|
||||
|
||||
enable_redis_auth_cache = litellm_settings.get("enable_redis_auth_cache", False)
|
||||
enable_redis_auth_cache = litellm_settings.get("enable_redis_auth_cache", True) is not False
|
||||
|
||||
with (
|
||||
patch.object(ps, "user_api_key_cache", fresh_user_cache),
|
||||
|
|
@ -107,15 +110,15 @@ class TestRedisAuthCacheFlag:
|
|||
"enable_redis_auth_cache=False"
|
||||
)
|
||||
|
||||
def test_flag_absent_leaves_user_api_key_cache_in_memory_only(self):
|
||||
"""When enable_redis_auth_cache is not set at all, default is in-memory-only."""
|
||||
def test_flag_absent_attaches_redis_to_user_api_key_cache_by_default(self):
|
||||
"""When enable_redis_auth_cache is not set, Redis attaches by default (issue #33253)."""
|
||||
with _patched_init_cache(
|
||||
litellm_settings={},
|
||||
cache_params={"type": "redis", "host": "localhost", "port": 6379},
|
||||
) as (user_cache, _):
|
||||
assert user_cache.redis_cache is None, (
|
||||
"user_api_key_cache must remain in-memory-only when "
|
||||
"enable_redis_auth_cache is absent from litellm_settings"
|
||||
assert user_cache.redis_cache is not None, (
|
||||
"user_api_key_cache must attach to the coordination Redis by "
|
||||
"default when Redis is configured and the flag is absent"
|
||||
)
|
||||
|
||||
def test_spend_counter_cache_always_gets_redis_regardless_of_flag(self):
|
||||
|
|
@ -143,3 +146,57 @@ class TestRedisAuthCacheFlag:
|
|||
) as (user_cache, spend_cache):
|
||||
assert spend_cache.redis_cache is not None
|
||||
assert user_cache.redis_cache is None
|
||||
|
||||
|
||||
class TestCliSsoLoginCrossWorker:
|
||||
"""
|
||||
Regression for issue #33253: the CLI SSO login flow stores its pending
|
||||
session in ``user_api_key_cache``. On a multi-worker deployment ``/sso/cli/start``
|
||||
and ``/sso/key/generate`` can land on different workers, so the session must
|
||||
survive being read back from a different worker's cache instance. With the
|
||||
default settings and a coordination Redis present, that now works because
|
||||
``user_api_key_cache`` shares the Redis backend across workers.
|
||||
"""
|
||||
|
||||
LOGIN_ID = "cli-abcdef012345"
|
||||
FLOW = {"poll_secret_hash": "deadbeef", "sso_complete": False, "session_data": None}
|
||||
|
||||
def _worker_cache(self, shared_redis, *, attach_redis: bool) -> DualCache:
|
||||
cache = DualCache()
|
||||
if attach_redis:
|
||||
cache.attach_redis_cache(shared_redis)
|
||||
return cache
|
||||
|
||||
def test_default_flow_readable_from_other_worker(self):
|
||||
from litellm.proxy.management_endpoints.ui_sso import (
|
||||
_get_cli_sso_flow_or_raise,
|
||||
_set_cli_sso_flow,
|
||||
)
|
||||
|
||||
shared_redis = _FakeRedisCache()
|
||||
worker_a = self._worker_cache(shared_redis, attach_redis=True)
|
||||
worker_b = self._worker_cache(shared_redis, attach_redis=True)
|
||||
|
||||
_set_cli_sso_flow(login_id=self.LOGIN_ID, cache=worker_a, flow=dict(self.FLOW))
|
||||
|
||||
flow = _get_cli_sso_flow_or_raise(login_id=self.LOGIN_ID, cache=worker_b)
|
||||
assert flow["poll_secret_hash"] == "deadbeef"
|
||||
|
||||
def test_in_memory_only_flow_lost_across_workers(self):
|
||||
"""Without a shared Redis, the pre-fix behaviour reproduces (session lost)."""
|
||||
from fastapi import HTTPException
|
||||
|
||||
from litellm.proxy.management_endpoints.ui_sso import (
|
||||
_get_cli_sso_flow_or_raise,
|
||||
_set_cli_sso_flow,
|
||||
)
|
||||
|
||||
shared_redis = _FakeRedisCache()
|
||||
worker_a = self._worker_cache(shared_redis, attach_redis=False)
|
||||
worker_b = self._worker_cache(shared_redis, attach_redis=False)
|
||||
|
||||
_set_cli_sso_flow(login_id=self.LOGIN_ID, cache=worker_a, flow=dict(self.FLOW))
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
_get_cli_sso_flow_or_raise(login_id=self.LOGIN_ID, cache=worker_b)
|
||||
assert exc_info.value.detail == "Invalid CLI login session"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue