Skip to content

Commit 4fdc94c

Browse files
GWealecopybara-github
authored andcommitted
fix: honor server-reported Gemini cache expiry
The client derived cache expiry from the local clock right after creation, but network delay or server-side normalization can push the real expiry off that estimate and lead to reuse at the wrong boundary. This records the expire_time the API returns when present, and falls back to the local TTL only when the response omits it. Co-authored-by: George Weale <gweale@google.com> PiperOrigin-RevId: 947961020
1 parent 8de4237 commit 4fdc94c

2 files changed

Lines changed: 29 additions & 1 deletion

File tree

src/google/adk/models/gemini_context_cache_manager.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
from __future__ import annotations
1818

19+
from datetime import datetime
1920
import hashlib
2021
import json
2122
import logging
@@ -532,14 +533,20 @@ async def _create_gemini_cache(
532533
)
533534
# Set precise creation timestamp right after cache creation
534535
created_at = time.time()
536+
server_expire_time = getattr(cached_content, "expire_time", None)
537+
expire_time = (
538+
server_expire_time.timestamp()
539+
if isinstance(server_expire_time, datetime)
540+
else created_at + llm_request.cache_config.ttl_seconds
541+
)
535542
logger.info("Cache created successfully: %s", cached_content.name)
536543

537544
span.set_attribute("cache_name", cached_content.name)
538545

539546
# Return complete cache metadata with precise timing
540547
return CacheMetadata(
541548
cache_name=cached_content.name,
542-
expire_time=created_at + llm_request.cache_config.ttl_seconds,
549+
expire_time=expire_time,
543550
fingerprint=self._generate_cache_fingerprint(
544551
llm_request, cache_contents_count
545552
),

tests/unittests/agents/test_gemini_context_cache_manager.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
"""Tests for GeminiContextCacheManager."""
1616

17+
from datetime import datetime
18+
from datetime import timezone
1719
import time
1820
from unittest.mock import AsyncMock
1921
from unittest.mock import MagicMock
@@ -1157,6 +1159,25 @@ async def test_dynamic_instruction_does_not_break_initial_cache_fingerprint(
11571159
assert result_2.invocations_used == 1
11581160
self.manager.genai_client.aio.caches.create.assert_called_once()
11591161

1162+
async def test_create_cache_uses_server_expire_time(self):
1163+
"""The server-reported expiry is authoritative when it is available."""
1164+
server_expire_time = datetime.fromtimestamp(2_000_000_000, tz=timezone.utc)
1165+
mock_cached_content = types.CachedContent(
1166+
name="projects/test/locations/us-central1/cachedContents/test123",
1167+
expire_time=server_expire_time,
1168+
)
1169+
self.manager.genai_client.aio.caches.create = AsyncMock(
1170+
return_value=mock_cached_content
1171+
)
1172+
llm_request = self.create_llm_request()
1173+
1174+
with patch.object(
1175+
self.manager, "_generate_cache_fingerprint", return_value="test_fp"
1176+
):
1177+
cache_metadata = await self.manager._create_gemini_cache(llm_request, 2)
1178+
1179+
assert cache_metadata.expire_time == server_expire_time.timestamp()
1180+
11601181
async def test_create_http_options_passthrough(self):
11611182
"""Test that create_http_options is passed through to cache creation config."""
11621183
mock_cached_content = AsyncMock()

0 commit comments

Comments
 (0)