fix(proxy): load REDIS_* env vars when cache_params has non-connection keys (#26233)

The cache_params env-var fallback in ProxyConfig.load_config was gated on
`len(cache_params.keys()) == 0`, so any non-empty cache_params (e.g. just
`mode: default_off`) silently dropped the Redis env var config and fell
back to in-memory cache. This broke multi-pod deployments because
spend_counter_cache.redis_cache never got wired up and each pod tracked
counters independently.

Replace the length check with a "user did not supply connection details"
check: only populate REDIS_HOST / REDIS_PORT / REDIS_PASSWORD from the
environment when neither `host` nor `url` is present in cache_params.
Other cache_params keys (mode, ttl, etc.) no longer suppress the fallback.

Adds two regression tests exercising ProxyConfig.load_config:
- cache_params with only non-connection keys → env vars load, RedisCache path
- cache_params with explicit host → env vars do not overwrite user config

Fixes #26233
This commit is contained in:
dschulmeist 2026-04-22 14:14:52 +02:00 committed by David Schulmeister
parent b8f7d61400
commit 22a3c50c35
2 changed files with 114 additions and 3 deletions

View file

@ -3032,9 +3032,13 @@ class ProxyConfig:
verbose_proxy_logger.debug("passed cache type=%s", cache_type)
if (
cache_type == "redis" or cache_type == "redis-semantic"
) and len(cache_params.keys()) == 0:
if (cache_type == "redis" or cache_type == "redis-semantic") and (
"host" not in cache_params and "url" not in cache_params
):
# Fall back to REDIS_* env vars whenever the user has not
# supplied connection details in cache_params. Gating on
# empty cache_params silently dropped to in-memory cache
# when unrelated keys like `mode` were set.
cache_host = get_secret("REDIS_HOST", None)
cache_port = get_secret("REDIS_PORT", None)
cache_password = None

View file

@ -4928,3 +4928,110 @@ async def test_increment_spend_counters_team_and_member():
finally:
ps.user_api_key_cache = original_key_cache
ps.spend_counter_cache = original_counter_cache
@pytest.mark.asyncio
async def test_redis_env_vars_loaded_when_cache_params_has_non_connection_keys(
monkeypatch,
):
"""
Regression test for https://github.com/BerriAI/litellm/issues/26233.
When `litellm_settings.cache: true` is combined with a `cache_params`
dict that contains only non-connection keys (e.g. `mode: default_off`),
the proxy must still fall back to the REDIS_HOST / REDIS_PORT /
REDIS_PASSWORD env vars. Previously the env-var fallback was gated on
`cache_params` being completely empty, so any unrelated key silently
dropped the cache to in-memory which breaks multi-pod spend tracking.
"""
import tempfile
from litellm.proxy.proxy_server import ProxyConfig
monkeypatch.setenv("REDIS_HOST", "test-redis.internal")
monkeypatch.setenv("REDIS_PORT", "6379")
monkeypatch.setenv("REDIS_PASSWORD", "fake-test-password")
test_config = {
"model_list": [],
"router_settings": {},
"litellm_settings": {
"cache": True,
"cache_params": {
"mode": "default_off",
},
},
}
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
yaml.dump(test_config, f)
config_file_path = f.name
try:
proxy_config = ProxyConfig()
with patch.object(ProxyConfig, "_init_cache") as mock_init:
await proxy_config.load_config(
router=MagicMock(), config_file_path=config_file_path
)
mock_init.assert_called_once()
actual_cache_params = mock_init.call_args.kwargs["cache_params"]
assert actual_cache_params.get("type") == "redis"
assert actual_cache_params.get("host") == "test-redis.internal"
assert actual_cache_params.get("port") == "6379"
assert actual_cache_params.get("password") == "fake-test-password"
# Preserved user-supplied non-connection keys
assert actual_cache_params.get("mode") == "default_off"
finally:
os.unlink(config_file_path)
@pytest.mark.asyncio
async def test_explicit_cache_params_host_not_overwritten_by_env_vars(monkeypatch):
"""
Companion to test_redis_env_vars_loaded_when_cache_params_has_non_connection_keys.
When `cache_params` explicitly specifies a `host`, the REDIS_* env vars
must NOT overwrite it. The env-var fallback only applies when the user
has supplied no connection details.
"""
import tempfile
from litellm.proxy.proxy_server import ProxyConfig
monkeypatch.setenv("REDIS_HOST", "env-redis.internal")
monkeypatch.setenv("REDIS_PORT", "6379")
test_config = {
"model_list": [],
"router_settings": {},
"litellm_settings": {
"cache": True,
"cache_params": {
"host": "explicit-redis.internal",
"port": 1234,
"mode": "default_off",
},
},
}
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
yaml.dump(test_config, f)
config_file_path = f.name
try:
proxy_config = ProxyConfig()
with patch.object(ProxyConfig, "_init_cache") as mock_init:
await proxy_config.load_config(
router=MagicMock(), config_file_path=config_file_path
)
mock_init.assert_called_once()
actual_cache_params = mock_init.call_args.kwargs["cache_params"]
assert actual_cache_params.get("host") == "explicit-redis.internal"
assert actual_cache_params.get("port") == 1234
assert actual_cache_params.get("mode") == "default_off"
finally:
os.unlink(config_file_path)