diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 37f3d6e49e0..39c287d97eb 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -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) diff --git a/tests/test_litellm/proxy/test_redis_auth_cache_flag.py b/tests/test_litellm/proxy/test_redis_auth_cache_flag.py index d0cb5ec5465..2b253f6b6ce 100644 --- a/tests/test_litellm/proxy/test_redis_auth_cache_flag.py +++ b/tests/test_litellm/proxy/test_redis_auth_cache_flag.py @@ -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"