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:
Krrish Dholakia 2026-07-14 19:03:11 +00:00
parent 2166608eb8
commit 22b4d55863
2 changed files with 79 additions and 20 deletions

View file

@ -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)

View file

@ -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"