2025-09-19 13:16:17 -07:00
|
|
|
# Copyright 2025 Google LLC
|
|
|
|
|
#
|
|
|
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
|
# you may not use this file except in compliance with the License.
|
|
|
|
|
# You may obtain a copy of the License at
|
|
|
|
|
#
|
|
|
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
#
|
|
|
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
|
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
|
# See the License for the specific language governing permissions and
|
|
|
|
|
# limitations under the License.
|
|
|
|
|
|
|
|
|
|
"""Tests for GeminiContextCacheManager."""
|
|
|
|
|
|
|
|
|
|
import time
|
|
|
|
|
from unittest.mock import AsyncMock
|
|
|
|
|
from unittest.mock import MagicMock
|
|
|
|
|
from unittest.mock import patch
|
|
|
|
|
|
|
|
|
|
from google.adk.agents.context_cache_config import ContextCacheConfig
|
|
|
|
|
from google.adk.models.cache_metadata import CacheMetadata
|
|
|
|
|
from google.adk.models.gemini_context_cache_manager import GeminiContextCacheManager
|
|
|
|
|
from google.adk.models.llm_request import LlmRequest
|
|
|
|
|
from google.adk.models.llm_response import LlmResponse
|
|
|
|
|
from google.genai import Client
|
|
|
|
|
from google.genai import types
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestGeminiContextCacheManager:
|
|
|
|
|
"""Test suite for GeminiContextCacheManager."""
|
|
|
|
|
|
|
|
|
|
def setup_method(self):
|
|
|
|
|
"""Set up test fixtures."""
|
|
|
|
|
mock_client = AsyncMock(spec=Client)
|
|
|
|
|
self.manager = GeminiContextCacheManager(mock_client)
|
|
|
|
|
self.cache_config = ContextCacheConfig(
|
|
|
|
|
cache_intervals=10,
|
|
|
|
|
ttl_seconds=1800,
|
|
|
|
|
min_tokens=0, # Allow caching for tests
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def create_llm_request(self, cache_metadata=None, contents_count=3):
|
|
|
|
|
"""Helper to create test LlmRequest."""
|
|
|
|
|
contents = []
|
|
|
|
|
for i in range(contents_count):
|
|
|
|
|
contents.append(
|
|
|
|
|
types.Content(
|
|
|
|
|
role="user", parts=[types.Part(text=f"Test message {i}")]
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Create tools for testing fingerprinting
|
|
|
|
|
tools = [
|
|
|
|
|
types.Tool(
|
|
|
|
|
function_declarations=[
|
|
|
|
|
types.FunctionDeclaration(
|
|
|
|
|
name="test_tool",
|
|
|
|
|
description="A test tool",
|
|
|
|
|
parameters=types.Schema(
|
|
|
|
|
type=types.Type.OBJECT,
|
|
|
|
|
properties={
|
|
|
|
|
"param": types.Schema(type=types.Type.STRING)
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
tool_config = types.ToolConfig(
|
|
|
|
|
function_calling_config=types.FunctionCallingConfig(mode="AUTO")
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return LlmRequest(
|
|
|
|
|
model="gemini-2.0-flash",
|
|
|
|
|
contents=contents,
|
|
|
|
|
config=types.GenerateContentConfig(
|
|
|
|
|
system_instruction="Test instruction",
|
|
|
|
|
tools=tools,
|
|
|
|
|
tool_config=tool_config,
|
|
|
|
|
),
|
|
|
|
|
cache_config=self.cache_config,
|
|
|
|
|
cache_metadata=cache_metadata,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def create_cache_metadata(
|
2025-10-09 21:19:00 -07:00
|
|
|
self, invocations_used=0, expired=False, contents_count=3
|
2025-09-19 13:16:17 -07:00
|
|
|
):
|
|
|
|
|
"""Helper to create test CacheMetadata."""
|
|
|
|
|
current_time = time.time()
|
|
|
|
|
expire_time = current_time - 300 if expired else current_time + 1800
|
|
|
|
|
|
|
|
|
|
return CacheMetadata(
|
|
|
|
|
cache_name="projects/test/locations/us-central1/cachedContents/test123",
|
|
|
|
|
expire_time=expire_time,
|
|
|
|
|
fingerprint="test_fingerprint",
|
|
|
|
|
invocations_used=invocations_used,
|
2025-10-09 21:19:00 -07:00
|
|
|
contents_count=contents_count,
|
2025-09-19 13:16:17 -07:00
|
|
|
created_at=current_time - 600,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_init(self):
|
|
|
|
|
"""Test manager initialization."""
|
|
|
|
|
mock_client = MagicMock(spec=Client)
|
|
|
|
|
manager = GeminiContextCacheManager(mock_client)
|
|
|
|
|
assert manager is not None
|
|
|
|
|
assert manager.genai_client == mock_client
|
|
|
|
|
|
2025-10-09 21:19:00 -07:00
|
|
|
async def test_handle_context_caching_no_existing_cache(self):
|
|
|
|
|
"""Test handling context caching with no existing cache returns fingerprint-only metadata."""
|
|
|
|
|
llm_request = self.create_llm_request(contents_count=5)
|
2025-09-19 13:16:17 -07:00
|
|
|
|
|
|
|
|
with patch.object(
|
|
|
|
|
self.manager, "_generate_cache_fingerprint", return_value="test_fp"
|
|
|
|
|
):
|
|
|
|
|
result = await self.manager.handle_context_caching(llm_request)
|
|
|
|
|
|
|
|
|
|
assert result is not None
|
2025-10-09 21:19:00 -07:00
|
|
|
# Should return fingerprint-only metadata (no active cache)
|
|
|
|
|
assert result.cache_name is None
|
|
|
|
|
assert result.expire_time is None
|
|
|
|
|
assert result.invocations_used is None
|
|
|
|
|
assert result.created_at is None
|
2025-09-19 13:16:17 -07:00
|
|
|
assert result.fingerprint == "test_fp"
|
2025-10-09 21:19:00 -07:00
|
|
|
assert result.contents_count == 5 # Total contents count
|
2025-09-19 13:16:17 -07:00
|
|
|
|
2025-10-09 21:19:00 -07:00
|
|
|
# No cache should be created
|
|
|
|
|
self.manager.genai_client.aio.caches.create.assert_not_called()
|
2025-09-19 13:16:17 -07:00
|
|
|
|
|
|
|
|
async def test_handle_context_caching_valid_existing_cache(self):
|
|
|
|
|
"""Test handling context caching with valid existing cache."""
|
|
|
|
|
|
|
|
|
|
# Create request with existing valid cache
|
|
|
|
|
existing_cache = self.create_cache_metadata(invocations_used=5)
|
|
|
|
|
llm_request = self.create_llm_request(cache_metadata=existing_cache)
|
|
|
|
|
|
|
|
|
|
with patch.object(self.manager, "_is_cache_valid", return_value=True):
|
|
|
|
|
result = await self.manager.handle_context_caching(llm_request)
|
|
|
|
|
|
|
|
|
|
assert result is not None
|
|
|
|
|
# Verify that existing cache metadata is preserved (copied)
|
|
|
|
|
assert result.cache_name == existing_cache.cache_name
|
|
|
|
|
assert (
|
|
|
|
|
result.invocations_used == existing_cache.invocations_used
|
|
|
|
|
) # Should preserve original invocations_used
|
|
|
|
|
assert (
|
|
|
|
|
result.expire_time == existing_cache.expire_time
|
|
|
|
|
) # Should preserve original expire_time
|
|
|
|
|
assert (
|
|
|
|
|
result.fingerprint == existing_cache.fingerprint
|
|
|
|
|
) # Should preserve original fingerprint
|
|
|
|
|
assert (
|
|
|
|
|
result.created_at == existing_cache.created_at
|
|
|
|
|
) # Should preserve original created_at
|
|
|
|
|
|
|
|
|
|
# Verify it's a copy, not the same object
|
|
|
|
|
assert result is not existing_cache
|
|
|
|
|
|
|
|
|
|
# Should not create new cache
|
|
|
|
|
self.manager.genai_client.aio.caches.create.assert_not_called()
|
|
|
|
|
|
2025-10-09 21:19:00 -07:00
|
|
|
async def test_handle_context_caching_invalid_cache_fingerprint_match(self):
|
|
|
|
|
"""Test invalid cache with matching fingerprint creates new cache."""
|
2025-09-19 13:16:17 -07:00
|
|
|
# Setup mocks
|
|
|
|
|
mock_cached_content = AsyncMock()
|
|
|
|
|
mock_cached_content.name = (
|
|
|
|
|
"projects/test/locations/us-central1/cachedContents/new456"
|
|
|
|
|
)
|
|
|
|
|
self.manager.genai_client.aio.caches.create = AsyncMock(
|
|
|
|
|
return_value=mock_cached_content
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Create request with invalid existing cache
|
|
|
|
|
existing_cache = self.create_cache_metadata(
|
|
|
|
|
invocations_used=15
|
|
|
|
|
) # Exceeds cache_intervals
|
|
|
|
|
llm_request = self.create_llm_request(cache_metadata=existing_cache)
|
2025-10-02 19:21:16 -07:00
|
|
|
llm_request.cacheable_contents_token_count = (
|
|
|
|
|
2048 # Add token count for cache creation
|
|
|
|
|
)
|
2025-09-19 13:16:17 -07:00
|
|
|
|
|
|
|
|
with (
|
|
|
|
|
patch.object(self.manager, "_is_cache_valid", return_value=False),
|
|
|
|
|
patch.object(self.manager, "cleanup_cache") as mock_cleanup,
|
|
|
|
|
patch.object(
|
2025-10-09 21:19:00 -07:00
|
|
|
self.manager,
|
|
|
|
|
"_generate_cache_fingerprint",
|
|
|
|
|
return_value="test_fingerprint", # Match old fingerprint
|
2025-09-19 13:16:17 -07:00
|
|
|
),
|
|
|
|
|
):
|
|
|
|
|
|
|
|
|
|
result = await self.manager.handle_context_caching(llm_request)
|
|
|
|
|
|
|
|
|
|
assert result is not None
|
2025-10-09 21:19:00 -07:00
|
|
|
# Should create new cache when fingerprints match
|
2025-09-19 13:16:17 -07:00
|
|
|
assert (
|
|
|
|
|
result.cache_name
|
|
|
|
|
== "projects/test/locations/us-central1/cachedContents/new456"
|
|
|
|
|
)
|
|
|
|
|
mock_cleanup.assert_called_once_with(existing_cache.cache_name)
|
|
|
|
|
self.manager.genai_client.aio.caches.create.assert_called_once()
|
|
|
|
|
|
2025-10-09 21:19:00 -07:00
|
|
|
async def test_handle_context_caching_invalid_cache_fingerprint_mismatch(
|
|
|
|
|
self,
|
|
|
|
|
):
|
|
|
|
|
"""Test invalid cache with mismatched fingerprint returns fingerprint-only metadata."""
|
|
|
|
|
# Create request with invalid existing cache
|
|
|
|
|
existing_cache = self.create_cache_metadata(
|
|
|
|
|
invocations_used=15, contents_count=3
|
|
|
|
|
) # Exceeds cache_intervals
|
|
|
|
|
llm_request = self.create_llm_request(
|
|
|
|
|
cache_metadata=existing_cache, contents_count=5
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
with (
|
|
|
|
|
patch.object(self.manager, "_is_cache_valid", return_value=False),
|
|
|
|
|
patch.object(self.manager, "cleanup_cache") as mock_cleanup,
|
|
|
|
|
patch.object(
|
|
|
|
|
self.manager,
|
|
|
|
|
"_generate_cache_fingerprint",
|
|
|
|
|
side_effect=["old_fp", "new_fp"], # Different fingerprints
|
|
|
|
|
),
|
|
|
|
|
):
|
|
|
|
|
|
|
|
|
|
result = await self.manager.handle_context_caching(llm_request)
|
|
|
|
|
|
|
|
|
|
assert result is not None
|
|
|
|
|
# Should return fingerprint-only metadata
|
|
|
|
|
assert result.cache_name is None
|
|
|
|
|
assert result.expire_time is None
|
|
|
|
|
assert result.invocations_used is None
|
|
|
|
|
assert result.created_at is None
|
|
|
|
|
assert result.fingerprint == "new_fp"
|
|
|
|
|
assert result.contents_count == 5 # Total contents count
|
|
|
|
|
mock_cleanup.assert_called_once_with(existing_cache.cache_name)
|
|
|
|
|
self.manager.genai_client.aio.caches.create.assert_not_called()
|
|
|
|
|
|
2025-09-19 13:16:17 -07:00
|
|
|
async def test_is_cache_valid_fingerprint_mismatch(self):
|
|
|
|
|
"""Test cache validation with fingerprint mismatch."""
|
|
|
|
|
cache_metadata = self.create_cache_metadata()
|
|
|
|
|
llm_request = self.create_llm_request(cache_metadata=cache_metadata)
|
|
|
|
|
|
|
|
|
|
with patch.object(
|
|
|
|
|
self.manager,
|
|
|
|
|
"_generate_cache_fingerprint",
|
|
|
|
|
return_value="different_fingerprint",
|
|
|
|
|
):
|
|
|
|
|
result = await self.manager._is_cache_valid(llm_request)
|
|
|
|
|
|
|
|
|
|
assert result is False
|
|
|
|
|
|
|
|
|
|
async def test_is_cache_valid_expired_cache(self):
|
|
|
|
|
"""Test cache validation with expired cache."""
|
|
|
|
|
cache_metadata = self.create_cache_metadata(expired=True)
|
|
|
|
|
llm_request = self.create_llm_request(cache_metadata=cache_metadata)
|
|
|
|
|
|
|
|
|
|
with patch.object(
|
|
|
|
|
self.manager,
|
|
|
|
|
"_generate_cache_fingerprint",
|
|
|
|
|
return_value="test_fingerprint",
|
|
|
|
|
):
|
|
|
|
|
result = await self.manager._is_cache_valid(llm_request)
|
|
|
|
|
|
|
|
|
|
assert result is False
|
|
|
|
|
|
2025-10-09 21:19:00 -07:00
|
|
|
async def test_is_cache_valid_fingerprint_only_metadata(self):
|
|
|
|
|
"""Test cache validation with fingerprint-only metadata (no active cache)."""
|
|
|
|
|
# Create fingerprint-only metadata (cache_name is None)
|
|
|
|
|
cache_metadata = CacheMetadata(
|
|
|
|
|
fingerprint="test_fingerprint",
|
|
|
|
|
contents_count=5,
|
|
|
|
|
)
|
|
|
|
|
llm_request = self.create_llm_request(cache_metadata=cache_metadata)
|
|
|
|
|
|
|
|
|
|
result = await self.manager._is_cache_valid(llm_request)
|
|
|
|
|
|
|
|
|
|
assert (
|
|
|
|
|
result is False
|
|
|
|
|
) # Fingerprint-only metadata is not a valid active cache
|
|
|
|
|
|
2025-09-19 13:16:17 -07:00
|
|
|
async def test_is_cache_valid_cache_intervals_exceeded(self):
|
|
|
|
|
"""Test cache validation with max invocations exceeded."""
|
|
|
|
|
cache_metadata = self.create_cache_metadata(
|
|
|
|
|
invocations_used=15
|
|
|
|
|
) # Exceeds cache_intervals=10
|
|
|
|
|
llm_request = self.create_llm_request(cache_metadata=cache_metadata)
|
|
|
|
|
|
|
|
|
|
with patch.object(
|
|
|
|
|
self.manager,
|
|
|
|
|
"_generate_cache_fingerprint",
|
|
|
|
|
return_value="test_fingerprint",
|
|
|
|
|
):
|
|
|
|
|
result = await self.manager._is_cache_valid(llm_request)
|
|
|
|
|
|
|
|
|
|
assert result is False
|
|
|
|
|
|
|
|
|
|
async def test_is_cache_valid_all_checks_pass(self):
|
|
|
|
|
"""Test cache validation when all checks pass."""
|
|
|
|
|
cache_metadata = self.create_cache_metadata(
|
|
|
|
|
invocations_used=5
|
|
|
|
|
) # Within cache_intervals=10
|
|
|
|
|
llm_request = self.create_llm_request(cache_metadata=cache_metadata)
|
|
|
|
|
|
|
|
|
|
with patch.object(
|
|
|
|
|
self.manager,
|
|
|
|
|
"_generate_cache_fingerprint",
|
|
|
|
|
return_value="test_fingerprint",
|
|
|
|
|
):
|
|
|
|
|
result = await self.manager._is_cache_valid(llm_request)
|
|
|
|
|
|
|
|
|
|
assert result is True
|
|
|
|
|
|
|
|
|
|
async def test_cleanup_cache(self):
|
|
|
|
|
"""Test cache cleanup functionality."""
|
|
|
|
|
cache_name = "projects/test/locations/us-central1/cachedContents/test123"
|
|
|
|
|
|
|
|
|
|
await self.manager.cleanup_cache(cache_name)
|
|
|
|
|
|
|
|
|
|
self.manager.genai_client.aio.caches.delete.assert_called_once_with(
|
|
|
|
|
name=cache_name
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_generate_cache_fingerprint(self):
|
|
|
|
|
"""Test cache fingerprint generation includes tools and tool_config."""
|
|
|
|
|
llm_request = self.create_llm_request()
|
|
|
|
|
cache_contents_count = 2 # Cache all but last content
|
|
|
|
|
|
|
|
|
|
fingerprint1 = self.manager._generate_cache_fingerprint(
|
|
|
|
|
llm_request, cache_contents_count
|
|
|
|
|
)
|
|
|
|
|
fingerprint2 = self.manager._generate_cache_fingerprint(
|
|
|
|
|
llm_request, cache_contents_count
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Same request should generate same fingerprint
|
|
|
|
|
assert fingerprint1 == fingerprint2
|
|
|
|
|
assert isinstance(fingerprint1, str)
|
|
|
|
|
assert len(fingerprint1) > 0
|
|
|
|
|
|
|
|
|
|
# Test that tool_config and tools are included in fingerprint
|
|
|
|
|
# Create request without tools/tool_config
|
|
|
|
|
llm_request_no_tools = LlmRequest(
|
|
|
|
|
model="gemini-2.0-flash",
|
|
|
|
|
contents=[types.Content(role="user", parts=[types.Part(text="Test")])],
|
|
|
|
|
config=types.GenerateContentConfig(
|
|
|
|
|
system_instruction="Test instruction"
|
|
|
|
|
),
|
|
|
|
|
cache_config=self.cache_config,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
fingerprint_no_tools = self.manager._generate_cache_fingerprint(
|
|
|
|
|
llm_request_no_tools, cache_contents_count
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Should be different from request with tools
|
|
|
|
|
assert fingerprint1 != fingerprint_no_tools
|
|
|
|
|
|
|
|
|
|
def test_generate_cache_fingerprint_different_requests(self):
|
|
|
|
|
"""Test that different requests generate different fingerprints."""
|
|
|
|
|
llm_request1 = self.create_llm_request()
|
|
|
|
|
|
|
|
|
|
llm_request2 = LlmRequest(
|
|
|
|
|
model="gemini-2.0-flash",
|
|
|
|
|
contents=[
|
|
|
|
|
types.Content(
|
|
|
|
|
role="user", parts=[types.Part(text="Different message")]
|
|
|
|
|
)
|
|
|
|
|
],
|
|
|
|
|
config=types.GenerateContentConfig(
|
|
|
|
|
system_instruction="Different instruction"
|
|
|
|
|
),
|
|
|
|
|
cache_config=self.cache_config,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
cache_contents_count = 2
|
|
|
|
|
fingerprint1 = self.manager._generate_cache_fingerprint(
|
|
|
|
|
llm_request1, cache_contents_count
|
|
|
|
|
)
|
|
|
|
|
fingerprint2 = self.manager._generate_cache_fingerprint(
|
|
|
|
|
llm_request2, cache_contents_count
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert fingerprint1 != fingerprint2
|
|
|
|
|
|
|
|
|
|
def test_generate_cache_fingerprint_tool_config_variations(self):
|
|
|
|
|
"""Test that different tool configs generate different fingerprints."""
|
|
|
|
|
# Request with AUTO mode
|
|
|
|
|
llm_request_auto = self.create_llm_request()
|
|
|
|
|
|
|
|
|
|
# Request with NONE mode
|
|
|
|
|
tool_config_none = types.ToolConfig(
|
|
|
|
|
function_calling_config=types.FunctionCallingConfig(mode="NONE")
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
llm_request_none = LlmRequest(
|
|
|
|
|
model="gemini-2.0-flash",
|
|
|
|
|
contents=[types.Content(role="user", parts=[types.Part(text="Test")])],
|
|
|
|
|
config=types.GenerateContentConfig(
|
|
|
|
|
system_instruction="Test instruction",
|
|
|
|
|
tools=llm_request_auto.config.tools,
|
|
|
|
|
tool_config=tool_config_none,
|
|
|
|
|
),
|
|
|
|
|
cache_config=self.cache_config,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
cache_contents_count = 2
|
|
|
|
|
fingerprint_auto = self.manager._generate_cache_fingerprint(
|
|
|
|
|
llm_request_auto, cache_contents_count
|
|
|
|
|
)
|
|
|
|
|
fingerprint_none = self.manager._generate_cache_fingerprint(
|
|
|
|
|
llm_request_none, cache_contents_count
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert fingerprint_auto != fingerprint_none
|
|
|
|
|
|
|
|
|
|
async def test_populate_cache_metadata_in_response_no_invocations_increment(
|
|
|
|
|
self,
|
|
|
|
|
):
|
|
|
|
|
"""Test that populate_cache_metadata_in_response doesn't increment invocations_used."""
|
|
|
|
|
# Create mock response with usage metadata
|
|
|
|
|
usage_metadata = MagicMock()
|
|
|
|
|
usage_metadata.cached_content_token_count = 800
|
|
|
|
|
usage_metadata.prompt_token_count = 1000
|
|
|
|
|
|
|
|
|
|
llm_response = MagicMock(spec=LlmResponse)
|
|
|
|
|
llm_response.usage_metadata = usage_metadata
|
|
|
|
|
|
|
|
|
|
cache_metadata = self.create_cache_metadata(invocations_used=3)
|
|
|
|
|
|
|
|
|
|
self.manager.populate_cache_metadata_in_response(
|
|
|
|
|
llm_response, cache_metadata
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Verify response metadata preserves the original invocations_used (no increment)
|
|
|
|
|
updated_metadata = llm_response.cache_metadata
|
|
|
|
|
assert (
|
|
|
|
|
updated_metadata.invocations_used == 3
|
|
|
|
|
) # Should preserve original value
|
|
|
|
|
assert updated_metadata.cache_name == cache_metadata.cache_name
|
|
|
|
|
assert updated_metadata.fingerprint == cache_metadata.fingerprint
|
|
|
|
|
assert updated_metadata.expire_time == cache_metadata.expire_time
|
|
|
|
|
assert updated_metadata.created_at == cache_metadata.created_at
|
|
|
|
|
|
|
|
|
|
async def test_populate_cache_metadata_no_usage_metadata(self):
|
|
|
|
|
"""Test populating cache metadata when no usage metadata."""
|
|
|
|
|
llm_response = MagicMock(spec=LlmResponse)
|
|
|
|
|
llm_response.usage_metadata = None
|
|
|
|
|
|
|
|
|
|
cache_metadata = self.create_cache_metadata(invocations_used=3)
|
|
|
|
|
|
|
|
|
|
self.manager.populate_cache_metadata_in_response(
|
|
|
|
|
llm_response, cache_metadata
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Should still create metadata even without usage info
|
|
|
|
|
updated_metadata = llm_response.cache_metadata
|
|
|
|
|
assert (
|
|
|
|
|
updated_metadata.invocations_used == 3
|
|
|
|
|
) # Should preserve original value
|
|
|
|
|
assert updated_metadata.cache_name == cache_metadata.cache_name
|
|
|
|
|
|
|
|
|
|
async def test_create_new_cache_with_proper_ttl(self):
|
|
|
|
|
"""Test that new cache is created with proper TTL."""
|
|
|
|
|
mock_cached_content = AsyncMock()
|
|
|
|
|
mock_cached_content.name = (
|
|
|
|
|
"projects/test/locations/us-central1/cachedContents/test123"
|
|
|
|
|
)
|
|
|
|
|
self.manager.genai_client.aio.caches.create = AsyncMock(
|
|
|
|
|
return_value=mock_cached_content
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
llm_request = self.create_llm_request()
|
|
|
|
|
|
|
|
|
|
cache_contents_count = max(0, len(llm_request.contents) - 1)
|
|
|
|
|
|
|
|
|
|
with patch.object(
|
|
|
|
|
self.manager, "_generate_cache_fingerprint", return_value="test_fp"
|
|
|
|
|
):
|
|
|
|
|
await self.manager._create_gemini_cache(llm_request, cache_contents_count)
|
|
|
|
|
|
|
|
|
|
# Verify cache creation call includes TTL
|
|
|
|
|
create_call = self.manager.genai_client.aio.caches.create.call_args
|
|
|
|
|
assert create_call is not None
|
|
|
|
|
cache_config = create_call[1]["config"]
|
|
|
|
|
assert cache_config.ttl == "1800s" # From cache_config
|
|
|
|
|
|
|
|
|
|
def test_all_but_last_content_caching(self):
|
|
|
|
|
"""Test that cache content counting works correctly."""
|
|
|
|
|
# Test with multiple contents
|
|
|
|
|
llm_request_multi = self.create_llm_request(contents_count=5)
|
|
|
|
|
|
|
|
|
|
# Test cache contents count calculation
|
|
|
|
|
cache_contents_count = max(0, len(llm_request_multi.contents) - 1)
|
|
|
|
|
|
|
|
|
|
assert cache_contents_count == 4 # 5 contents, so cache 4 contents
|
|
|
|
|
|
|
|
|
|
# Test with single content
|
|
|
|
|
llm_request_single = self.create_llm_request(contents_count=1)
|
|
|
|
|
single_cache_contents_count = max(0, len(llm_request_single.contents) - 1)
|
|
|
|
|
|
|
|
|
|
assert single_cache_contents_count == 0 # Single content, cache 0 contents
|
|
|
|
|
|
|
|
|
|
def test_edge_cases(self):
|
|
|
|
|
"""Test various edge cases."""
|
|
|
|
|
# Test with None cache_config
|
|
|
|
|
llm_request_no_config = LlmRequest(
|
|
|
|
|
model="gemini-2.0-flash",
|
|
|
|
|
contents=[types.Content(role="user", parts=[types.Part(text="Test")])],
|
|
|
|
|
config=types.GenerateContentConfig(system_instruction="Test"),
|
|
|
|
|
cache_config=None,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Should handle gracefully
|
|
|
|
|
cache_contents_count = 2
|
|
|
|
|
fingerprint = self.manager._generate_cache_fingerprint(
|
|
|
|
|
llm_request_no_config, cache_contents_count
|
|
|
|
|
)
|
|
|
|
|
assert isinstance(fingerprint, str)
|
|
|
|
|
|
|
|
|
|
# Test with empty contents
|
|
|
|
|
llm_request_empty = LlmRequest(
|
|
|
|
|
model="gemini-2.0-flash",
|
|
|
|
|
contents=[],
|
|
|
|
|
config=types.GenerateContentConfig(system_instruction="Test"),
|
|
|
|
|
cache_config=self.cache_config,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
empty_cache_contents_count = 0
|
|
|
|
|
fingerprint = self.manager._generate_cache_fingerprint(
|
|
|
|
|
llm_request_empty, empty_cache_contents_count
|
|
|
|
|
)
|
|
|
|
|
assert isinstance(fingerprint, str)
|
|
|
|
|
|
|
|
|
|
def test_parameter_types_enforcement(self):
|
|
|
|
|
"""Test that method calls with correct parameter types work properly."""
|
|
|
|
|
# Create proper objects
|
|
|
|
|
usage_metadata = MagicMock()
|
|
|
|
|
usage_metadata.cached_content_token_count = 500
|
|
|
|
|
usage_metadata.prompt_token_count = 1000
|
|
|
|
|
|
|
|
|
|
llm_response = MagicMock(spec=LlmResponse)
|
|
|
|
|
llm_response.usage_metadata = usage_metadata
|
|
|
|
|
|
|
|
|
|
cache_metadata = self.create_cache_metadata(invocations_used=3)
|
|
|
|
|
|
|
|
|
|
# This should work fine (correct types and order)
|
|
|
|
|
self.manager.populate_cache_metadata_in_response(
|
|
|
|
|
llm_response, cache_metadata
|
|
|
|
|
)
|
|
|
|
|
updated_metadata = llm_response.cache_metadata
|
|
|
|
|
assert updated_metadata.invocations_used == 3 # No increment in this method
|
|
|
|
|
|
|
|
|
|
# Document expected types for integration tests
|
|
|
|
|
assert isinstance(cache_metadata, CacheMetadata)
|
|
|
|
|
assert hasattr(
|
|
|
|
|
llm_response, "usage_metadata"
|
|
|
|
|
) # LlmResponse should have this
|
|
|
|
|
assert not hasattr(
|
|
|
|
|
cache_metadata, "usage_metadata"
|
|
|
|
|
) # CacheMetadata should NOT have this
|
2025-10-02 19:21:16 -07:00
|
|
|
|
|
|
|
|
def create_llm_request_with_token_count(
|
|
|
|
|
self, token_count=None, cache_metadata=None
|
|
|
|
|
):
|
|
|
|
|
"""Helper to create LlmRequest with cacheable_contents_token_count."""
|
|
|
|
|
llm_request = self.create_llm_request(cache_metadata=cache_metadata)
|
|
|
|
|
llm_request.cacheable_contents_token_count = token_count
|
|
|
|
|
return llm_request
|
|
|
|
|
|
|
|
|
|
async def test_cache_creation_with_sufficient_token_count(self):
|
2025-10-09 21:19:00 -07:00
|
|
|
"""Test that fingerprint-only metadata is returned even with sufficient tokens."""
|
|
|
|
|
# With new prefix matching logic, no cache is created without existing metadata
|
2025-10-02 19:21:16 -07:00
|
|
|
# Create request with sufficient token count
|
|
|
|
|
llm_request = self.create_llm_request_with_token_count(token_count=2048)
|
|
|
|
|
|
|
|
|
|
with patch.object(
|
|
|
|
|
self.manager, "_generate_cache_fingerprint", return_value="test_fp"
|
|
|
|
|
):
|
|
|
|
|
result = await self.manager.handle_context_caching(llm_request)
|
|
|
|
|
|
2025-10-09 21:19:00 -07:00
|
|
|
# Should return fingerprint-only metadata (no cache creation)
|
2025-10-02 19:21:16 -07:00
|
|
|
assert result is not None
|
2025-10-09 21:19:00 -07:00
|
|
|
assert result.cache_name is None # Fingerprint-only state
|
|
|
|
|
assert result.fingerprint == "test_fp"
|
|
|
|
|
assert result.contents_count == 3
|
|
|
|
|
self.manager.genai_client.aio.caches.create.assert_not_called()
|
2025-10-02 19:21:16 -07:00
|
|
|
|
|
|
|
|
async def test_cache_creation_with_insufficient_token_count(self):
|
2025-10-09 21:19:00 -07:00
|
|
|
"""Test that fingerprint-only metadata is returned even with insufficient tokens."""
|
2025-10-02 19:21:16 -07:00
|
|
|
# Set higher minimum token requirement
|
|
|
|
|
self.manager.cache_config = ContextCacheConfig(
|
|
|
|
|
cache_intervals=10,
|
|
|
|
|
ttl_seconds=1800,
|
|
|
|
|
min_tokens=2048,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Create request with insufficient token count
|
|
|
|
|
llm_request = self.create_llm_request_with_token_count(token_count=1024)
|
|
|
|
|
llm_request.cache_config = self.manager.cache_config
|
|
|
|
|
|
2025-10-09 21:19:00 -07:00
|
|
|
with patch.object(
|
|
|
|
|
self.manager, "_generate_cache_fingerprint", return_value="test_fp"
|
|
|
|
|
):
|
|
|
|
|
result = await self.manager.handle_context_caching(llm_request)
|
2025-10-02 19:21:16 -07:00
|
|
|
|
2025-10-09 21:19:00 -07:00
|
|
|
# Should return fingerprint-only metadata
|
|
|
|
|
assert result is not None
|
|
|
|
|
assert result.cache_name is None
|
|
|
|
|
assert result.fingerprint == "test_fp"
|
2025-10-02 19:21:16 -07:00
|
|
|
self.manager.genai_client.aio.caches.create.assert_not_called()
|
|
|
|
|
|
|
|
|
|
async def test_cache_creation_without_token_count(self):
|
2025-10-09 21:19:00 -07:00
|
|
|
"""Test that fingerprint-only metadata is returned even without token count."""
|
2025-10-02 19:21:16 -07:00
|
|
|
# Create request without token count (initial request)
|
|
|
|
|
llm_request = self.create_llm_request_with_token_count(token_count=None)
|
|
|
|
|
|
2025-10-09 21:19:00 -07:00
|
|
|
with patch.object(
|
|
|
|
|
self.manager, "_generate_cache_fingerprint", return_value="test_fp"
|
|
|
|
|
):
|
|
|
|
|
result = await self.manager.handle_context_caching(llm_request)
|
2025-10-02 19:21:16 -07:00
|
|
|
|
2025-10-09 21:19:00 -07:00
|
|
|
# Should return fingerprint-only metadata
|
|
|
|
|
assert result is not None
|
|
|
|
|
assert result.cache_name is None
|
|
|
|
|
assert result.fingerprint == "test_fp"
|
2025-10-02 19:21:16 -07:00
|
|
|
self.manager.genai_client.aio.caches.create.assert_not_called()
|