@@ -304,6 +304,109 @@ async def test_create_cache_gates_on_prefix_not_full_prompt(self):
304304 assert result is None
305305 self .manager .genai_client .aio .caches .create .assert_not_called ()
306306
307+ async def test_completed_turn_grows_cacheable_prefix (self ):
308+ """A completed turn becomes part of the next explicit cache."""
309+ first_user = types .Content (
310+ role = "user" , parts = [types .Part (text = "First question" )]
311+ )
312+ first_model = types .Content (
313+ role = "model" , parts = [types .Part (text = "First answer" )]
314+ )
315+ next_user = types .Content (
316+ role = "user" , parts = [types .Part (text = "Next question" )]
317+ )
318+ first_request = self .create_llm_request (contents_count = 0 )
319+ first_request .contents = [first_user ]
320+
321+ first_metadata = await self .manager .handle_context_caching (first_request )
322+
323+ assert first_metadata is not None
324+ assert first_metadata .contents_count == 0
325+
326+ next_request = self .create_llm_request (
327+ cache_metadata = first_metadata , contents_count = 0
328+ )
329+ next_request .contents = [first_user , first_model , next_user ]
330+ next_request .cacheable_contents_token_count = 30_000
331+ cached_content = AsyncMock ()
332+ cached_content .name = "cachedContents/grown-prefix"
333+ self .manager .genai_client .aio .caches .create = AsyncMock (
334+ return_value = cached_content
335+ )
336+
337+ next_metadata = await self .manager .handle_context_caching (next_request )
338+
339+ assert next_metadata is not None
340+ assert next_metadata .cache_name == "cachedContents/grown-prefix"
341+ assert next_metadata .contents_count == 2
342+ create_config = (
343+ self .manager .genai_client .aio .caches .create .call_args .kwargs ["config" ]
344+ )
345+ assert create_config .contents == [first_user , first_model ]
346+ assert next_request .contents == [next_user ]
347+
348+ async def test_gemini_25_creates_cache_above_2048_token_minimum (self ):
349+ """Gemini 2.5 creates an explicit cache above its 2,048-token floor."""
350+ llm_request = self .create_llm_request (contents_count = 0 )
351+ llm_request .config .system_instruction = "x" * 12_000
352+ llm_request .cacheable_contents_token_count = 3_000
353+ llm_request .cache_metadata = CacheMetadata (
354+ fingerprint = self .manager ._generate_cache_fingerprint (llm_request , 0 ),
355+ contents_count = 0 ,
356+ )
357+ cached_content = AsyncMock ()
358+ cached_content .name = "cachedContents/gemini-25"
359+ self .manager .genai_client .aio .caches .create = AsyncMock (
360+ return_value = cached_content
361+ )
362+
363+ result = await self .manager .handle_context_caching (llm_request )
364+
365+ assert result is not None
366+ assert result .cache_name == "cachedContents/gemini-25"
367+ self .manager .genai_client .aio .caches .create .assert_awaited_once ()
368+
369+ async def test_gemini_3_skips_cache_below_4096_token_minimum (self ):
370+ """Gemini 3 skips an explicit cache below its 4,096-token floor."""
371+ llm_request = self .create_llm_request (contents_count = 0 )
372+ llm_request .model = "gemini-3.1-pro-preview"
373+ llm_request .config .system_instruction = "x" * 12_000
374+ llm_request .cacheable_contents_token_count = 3_000
375+ llm_request .cache_metadata = CacheMetadata (
376+ fingerprint = self .manager ._generate_cache_fingerprint (llm_request , 0 ),
377+ contents_count = 0 ,
378+ )
379+
380+ result = await self .manager .handle_context_caching (llm_request )
381+
382+ assert result is not None
383+ assert result .cache_name is None
384+ self .manager .genai_client .aio .caches .create .assert_not_called ()
385+
386+ async def test_opaque_model_does_not_apply_guessed_token_minimum (self ):
387+ """Opaque tuned-model IDs let the server enforce the cache floor."""
388+ llm_request = self .create_llm_request (contents_count = 0 )
389+ llm_request .model = (
390+ "projects/test/locations/us-central1/endpoints/tuned-model"
391+ )
392+ llm_request .config .system_instruction = "x" * 12_000
393+ llm_request .cacheable_contents_token_count = 3_000
394+ llm_request .cache_metadata = CacheMetadata (
395+ fingerprint = self .manager ._generate_cache_fingerprint (llm_request , 0 ),
396+ contents_count = 0 ,
397+ )
398+ cached_content = AsyncMock ()
399+ cached_content .name = "cachedContents/tuned-model"
400+ self .manager .genai_client .aio .caches .create = AsyncMock (
401+ return_value = cached_content
402+ )
403+
404+ result = await self .manager .handle_context_caching (llm_request )
405+
406+ assert result is not None
407+ assert result .cache_name == "cachedContents/tuned-model"
408+ self .manager .genai_client .aio .caches .create .assert_awaited_once ()
409+
307410 async def test_handle_context_caching_invalid_cache_fingerprint_mismatch (
308411 self ,
309412 ):
@@ -1155,9 +1258,12 @@ async def test_dynamic_instruction_does_not_break_initial_cache_fingerprint(
11551258 assert result_2 .cache_name == (
11561259 "projects/test/locations/us-central1/cachedContents/new789"
11571260 )
1158- assert result_2 .contents_count == 0
1261+ assert result_2 .contents_count == 2
11591262 assert result_2 .invocations_used == 1
1160- self .manager .genai_client .aio .caches .create .assert_called_once ()
1263+ create_config = (
1264+ self .manager .genai_client .aio .caches .create .call_args .kwargs ["config" ]
1265+ )
1266+ assert create_config .contents == [user_msg , model_tool_call ]
11611267
11621268 async def test_create_cache_uses_server_expire_time (self ):
11631269 """The server-reported expiry is authoritative when it is available."""
0 commit comments