From c444263e7dae340eedc0b9a3234f0950d1653e70 Mon Sep 17 00:00:00 2001 From: Michal Otmianowski Date: Mon, 25 Aug 2025 10:42:12 +0200 Subject: [PATCH 1/2] verify expires field prior to serving cache entry --- litellm/caching/s3_cache.py | 11 ++++- tests/test_litellm/caching/test_s3_cache.py | 51 ++++++++++++++++++++- 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/litellm/caching/s3_cache.py b/litellm/caching/s3_cache.py index 15f7a5c1e16..e3142ea1359 100644 --- a/litellm/caching/s3_cache.py +++ b/litellm/caching/s3_cache.py @@ -13,6 +13,7 @@ import asyncio import json from functools import partial from typing import Optional +from datetime import datetime from litellm._logging import print_verbose, verbose_logger @@ -72,8 +73,7 @@ class S3Cache(BaseCache): import datetime # Calculate expiration time - expiration_time = datetime.datetime.now() + datetime.timedelta(seconds=ttl) - + expiration_time = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(seconds=ttl) # Upload the data to S3 with the calculated expiration time self.s3_client.put_object( Bucket=self.bucket_name, @@ -126,6 +126,13 @@ class S3Cache(BaseCache): ) if cached_response is not None: + if "Expires" in cached_response: + expires_time = cached_response['Expires'] + current_time = datetime.now(expires_time.tzinfo) + + if current_time > expires_time: + return None + # cached_response is in `b{} convert it to ModelResponse cached_response = ( cached_response["Body"].read().decode("utf-8") diff --git a/tests/test_litellm/caching/test_s3_cache.py b/tests/test_litellm/caching/test_s3_cache.py index dce3f7d585d..9c902768bfc 100644 --- a/tests/test_litellm/caching/test_s3_cache.py +++ b/tests/test_litellm/caching/test_s3_cache.py @@ -56,7 +56,7 @@ def test_s3_cache_set_cache_with_ttl(mock_s3_dependencies): assert "max-age=3600" in call_args[1]["CacheControl"] -def test_s3_cache_get_cache(mock_s3_dependencies): +def test_s3_cache_get_cache_no_expires_info_in_response(mock_s3_dependencies): """Test basic get_cache functionality""" cache = S3Cache("test-bucket") @@ -75,6 +75,54 @@ def test_s3_cache_get_cache(mock_s3_dependencies): assert result == {"key": "value", "number": 42} +def test_s3_cache_get_cache_with_expires_valid(mock_s3_dependencies): + """Test get_cache when response contains Expires and cache entry is still valid""" + cache = S3Cache("test-bucket") + + # Create a future expiration time (1 hour from now) + future_time = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(hours=1) + + mock_response = { + "Body": MagicMock(), + "Expires": future_time + } + mock_response["Body"].read.return_value = b'{"key": "value", "number": 42}' + cache.s3_client.get_object.return_value = mock_response + + result = cache.get_cache("test_key") + + cache.s3_client.get_object.assert_called_once_with( + Bucket="test-bucket", + Key="test_key" + ) + + # Should return the cached value since it's not expired + assert result == {"key": "value", "number": 42} + + +def test_s3_cache_get_cache_with_expires_expired(mock_s3_dependencies): + """Test get_cache when response contains Expires and cache entry is no longer valid""" + cache = S3Cache("test-bucket") + + # Create a past expiration time (1 hour ago) + past_time = datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(hours=1) + + mock_response = { + "Body": MagicMock(), + "Expires": past_time + } + mock_response["Body"].read.return_value = b'{"key": "value", "number": 42}' + cache.s3_client.get_object.return_value = mock_response + + result = cache.get_cache("test_key") + + cache.s3_client.get_object.assert_called_once_with( + Bucket="test-bucket", + Key="test_key" + ) + + # Should return None since the cache entry is expired + assert result is None def test_s3_cache_get_cache_not_found(mock_s3_dependencies): """Test get_cache when key is not found""" @@ -126,7 +174,6 @@ def test_s3_cache_initialization(): cache_with_path = S3Cache("test-bucket", s3_path="my/cache/path") assert cache_with_path.key_prefix == "my/cache/path/" - # ============================================================================ # ASYNC TESTS # ============================================================================ From 3b6462236c5b97bb6260acb367a3e41e4b987ebb Mon Sep 17 00:00:00 2001 From: Michal Otmianowski Date: Mon, 25 Aug 2025 11:09:06 +0200 Subject: [PATCH 2/2] clean imports --- litellm/caching/s3_cache.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/litellm/caching/s3_cache.py b/litellm/caching/s3_cache.py index e3142ea1359..180964605f6 100644 --- a/litellm/caching/s3_cache.py +++ b/litellm/caching/s3_cache.py @@ -13,7 +13,7 @@ import asyncio import json from functools import partial from typing import Optional -from datetime import datetime +from datetime import datetime, timezone, timedelta from litellm._logging import print_verbose, verbose_logger @@ -70,10 +70,9 @@ class S3Cache(BaseCache): if ttl is not None: cache_control = f"immutable, max-age={ttl}, s-maxage={ttl}" - import datetime # Calculate expiration time - expiration_time = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(seconds=ttl) + expiration_time = datetime.now(timezone.utc) + timedelta(seconds=ttl) # Upload the data to S3 with the calculated expiration time self.s3_client.put_object( Bucket=self.bucket_name,