From fba12c38d47c722ebc9fd9b091903ae10aa2bcf9 Mon Sep 17 00:00:00 2001 From: dhjeon0903 <166296110+dhjeon0903@users.noreply.github.com> Date: Wed, 5 Aug 2026 18:16:28 +0900 Subject: [PATCH] fix(caching): apply default_redis_ttl to Redis writes --- litellm/caching/dual_cache.py | 46 ++++++---- tests/test_litellm/caching/test_dual_cache.py | 90 +++++++++++++++++++ 2 files changed, 120 insertions(+), 16 deletions(-) diff --git a/litellm/caching/dual_cache.py b/litellm/caching/dual_cache.py index 3b181ca23ff..93ae46433cb 100644 --- a/litellm/caching/dual_cache.py +++ b/litellm/caching/dual_cache.py @@ -111,21 +111,35 @@ class DualCache(BaseCache): without it, backfilled entries fall to ``InMemoryCache``'s own default TTL and can outlive the TTL this cache was configured with. """ - if "ttl" not in kwargs and self.default_in_memory_ttl is not None: - return {**kwargs, "ttl": self.default_in_memory_ttl} - return kwargs + return self._write_kwargs(kwargs, self.default_in_memory_ttl) + + @property + def _redis_write_ttl(self) -> float | None: + """ + Default TTL for a Redis write. + + ``default_redis_ttl`` when set, else ``default_in_memory_ttl`` so callers that + configure only the in-memory tier keep the Redis TTL they have always had. + """ + if self.default_redis_ttl is not None: + return self.default_redis_ttl + return self.default_in_memory_ttl + + @staticmethod + def _write_kwargs(kwargs: "dict[str, object]", default_ttl: float | None) -> "dict[str, object]": + """Kwargs for one tier's write: apply ``default_ttl`` unless the caller passed an explicit ``ttl``.""" + if "ttl" in kwargs or default_ttl is None: + return kwargs + return {**kwargs, "ttl": default_ttl} def set_cache(self, key, value, local_only: bool = False, **kwargs): # Update both Redis and in-memory cache try: if self.in_memory_cache is not None: - if "ttl" not in kwargs and self.default_in_memory_ttl is not None: - kwargs["ttl"] = self.default_in_memory_ttl - - self.in_memory_cache.set_cache(key, value, **kwargs) + self.in_memory_cache.set_cache(key, value, **self._write_kwargs(kwargs, self.default_in_memory_ttl)) if self.redis_cache is not None and local_only is False: - self.redis_cache.set_cache(key, value, **kwargs) + self.redis_cache.set_cache(key, value, **self._write_kwargs(kwargs, self._redis_write_ttl)) except Exception as e: print_verbose(e) @@ -340,12 +354,12 @@ class DualCache(BaseCache): print_verbose(f"async set cache: cache key: {key}; local_only: {local_only}; value: {value}") try: if self.in_memory_cache is not None: - if "ttl" not in kwargs and self.default_in_memory_ttl is not None: - kwargs["ttl"] = self.default_in_memory_ttl - await self.in_memory_cache.async_set_cache(key, value, **kwargs) + await self.in_memory_cache.async_set_cache( + key, value, **self._write_kwargs(kwargs, self.default_in_memory_ttl) + ) if self.redis_cache is not None and local_only is False: - await self.redis_cache.async_set_cache(key, value, **kwargs) + await self.redis_cache.async_set_cache(key, value, **self._write_kwargs(kwargs, self._redis_write_ttl)) except Exception as e: verbose_logger.exception("LiteLLM Cache: Excepton async add_cache: %s", e) @@ -357,13 +371,13 @@ class DualCache(BaseCache): print_verbose(f"async batch set cache: cache keys: {cache_list}; local_only: {local_only}") try: if self.in_memory_cache is not None: - if "ttl" not in kwargs and self.default_in_memory_ttl is not None: - kwargs["ttl"] = self.default_in_memory_ttl - await self.in_memory_cache.async_set_cache_pipeline(cache_list=cache_list, **kwargs) + await self.in_memory_cache.async_set_cache_pipeline( + cache_list=cache_list, **self._write_kwargs(kwargs, self.default_in_memory_ttl) + ) if self.redis_cache is not None and local_only is False: await self.redis_cache.async_set_cache_pipeline( - cache_list=cache_list, ttl=kwargs.pop("ttl", None), **kwargs + cache_list=cache_list, **self._write_kwargs(kwargs, self._redis_write_ttl) ) except Exception as e: verbose_logger.exception("LiteLLM Cache: Excepton async add_cache: %s", e) diff --git a/tests/test_litellm/caching/test_dual_cache.py b/tests/test_litellm/caching/test_dual_cache.py index 47be139eb5e..fc2f5c1ae52 100644 --- a/tests/test_litellm/caching/test_dual_cache.py +++ b/tests/test_litellm/caching/test_dual_cache.py @@ -445,3 +445,93 @@ async def test_dual_cache_late_attach_redis_wires_writes_and_ttl_async(): assert mock_redis.async_set_cache.call_args[0][:2] == (key_after, val_after) assert in_memory.get_cache(key_after) == val_after + + +def _redis_ttl(mock_call): + return mock_call.kwargs.get("ttl") + + +def test_default_redis_ttl_applies_to_sync_redis_write(): + mock_redis = MagicMock(spec=RedisCache) + dual_cache = DualCache( + in_memory_cache=InMemoryCache(), redis_cache=mock_redis, default_redis_ttl=600.0 + ) + + dual_cache.set_cache("key", "value") + + assert _redis_ttl(mock_redis.set_cache.call_args) == 600.0 + + +@pytest.mark.asyncio +async def test_default_redis_ttl_applies_to_async_redis_writes(): + mock_redis = MagicMock(spec=RedisCache) + mock_redis.async_set_cache = AsyncMock() + mock_redis.async_set_cache_pipeline = AsyncMock() + dual_cache = DualCache( + in_memory_cache=InMemoryCache(), redis_cache=mock_redis, default_redis_ttl=600.0 + ) + + await dual_cache.async_set_cache("key", "value") + await dual_cache.async_set_cache_pipeline([("key", "value")]) + + assert _redis_ttl(mock_redis.async_set_cache.call_args) == 600.0 + assert _redis_ttl(mock_redis.async_set_cache_pipeline.call_args) == 600.0 + + +def test_update_cache_ttl_redis_only_applies_to_redis_write(): + mock_redis = MagicMock(spec=RedisCache) + dual_cache = DualCache(in_memory_cache=InMemoryCache(), redis_cache=mock_redis) + + dual_cache.update_cache_ttl(default_in_memory_ttl=None, default_redis_ttl=600.0) + dual_cache.set_cache("key", "value") + + assert _redis_ttl(mock_redis.set_cache.call_args) == 600.0 + + +def test_attach_redis_cache_default_redis_ttl_applies_to_redis_write(): + mock_redis = MagicMock(spec=RedisCache) + dual_cache = DualCache(in_memory_cache=InMemoryCache()) + + dual_cache.attach_redis_cache(mock_redis, default_redis_ttl=600.0) + dual_cache.set_cache("key", "value") + + assert _redis_ttl(mock_redis.set_cache.call_args) == 600.0 + + +def test_explicit_ttl_overrides_default_redis_ttl(): + mock_redis = MagicMock(spec=RedisCache) + dual_cache = DualCache( + in_memory_cache=InMemoryCache(), + redis_cache=mock_redis, + default_in_memory_ttl=300.0, + default_redis_ttl=600.0, + ) + + dual_cache.set_cache("key", "value", ttl=42) + + assert _redis_ttl(mock_redis.set_cache.call_args) == 42 + + +def test_default_in_memory_ttl_still_propagates_to_redis_when_no_redis_ttl(): + mock_redis = MagicMock(spec=RedisCache) + dual_cache = DualCache( + in_memory_cache=InMemoryCache(), redis_cache=mock_redis, default_in_memory_ttl=5.0 + ) + + dual_cache.set_cache("key", "value") + + assert _redis_ttl(mock_redis.set_cache.call_args) == 5.0 + + +def test_default_redis_ttl_does_not_leak_into_in_memory_write(): + mock_in_memory = MagicMock(spec=InMemoryCache) + dual_cache = DualCache( + in_memory_cache=mock_in_memory, + redis_cache=MagicMock(spec=RedisCache), + default_in_memory_ttl=300.0, + default_redis_ttl=600.0, + ) + + dual_cache.set_cache("key", "value") + + assert _redis_ttl(mock_in_memory.set_cache.call_args) == 300.0