Skip to content

Commit c73038c

Browse files
committed
fix(models): Drop stale thought signatures
Keep only the newest thought_signature in request contents so prior turns do not send signatures the model no longer uses. Fixes #3693
1 parent 49d4441 commit c73038c

2 files changed

Lines changed: 63 additions & 0 deletions

File tree

src/google/adk/models/google_llm.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,21 @@
6363
"""
6464

6565

66+
def _remove_old_thought_signatures(contents: list[types.Content]) -> None:
67+
"""Keeps only the latest thought signature in Gemini request contents."""
68+
latest_signature_seen = False
69+
for content in reversed(contents):
70+
if not content.parts:
71+
continue
72+
for part in reversed(content.parts):
73+
if part.thought_signature is None:
74+
continue
75+
if latest_signature_seen:
76+
part.thought_signature = None
77+
else:
78+
latest_signature_seen = True
79+
80+
6681
class _ResourceExhaustedError(ClientError):
6782
"""Represents a resources exhausted error received from the Model."""
6883

@@ -195,6 +210,7 @@ async def generate_content_async(
195210
"""
196211
await self._preprocess_request(llm_request)
197212
self._maybe_append_user_content(llm_request)
213+
_remove_old_thought_signatures(llm_request.contents)
198214

199215
# Handle context caching if configured
200216
cache_metadata = None

tests/unittests/models/test_google_llm.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,53 @@ async def mock_coro():
381381
mock_client.aio.models.generate_content.assert_called_once()
382382

383383

384+
@pytest.mark.asyncio
385+
async def test_generate_content_async_keeps_only_latest_thought_signature(
386+
gemini_llm, generate_content_response
387+
):
388+
"""Gemini requests keep only the newest thought signature."""
389+
390+
def _function_call_part(name, signature):
391+
return Part(
392+
function_call=types.FunctionCall(name=name, args={}),
393+
thought_signature=signature,
394+
)
395+
396+
old_part = _function_call_part("first_tool", b"old")
397+
newer_part = _function_call_part("second_tool", b"newer")
398+
latest_part = _function_call_part("third_tool", b"latest")
399+
llm_request = LlmRequest(
400+
model="gemini-2.5-flash",
401+
contents=[
402+
Content(role="model", parts=[old_part]),
403+
Content(role="user", parts=[Part.from_text(text="tool result")]),
404+
Content(role="model", parts=[newer_part, latest_part]),
405+
],
406+
)
407+
408+
with mock.patch.object(gemini_llm, "api_client") as mock_client:
409+
410+
async def mock_coro():
411+
return generate_content_response
412+
413+
mock_client.aio.models.generate_content.return_value = mock_coro()
414+
415+
responses = [
416+
resp
417+
async for resp in gemini_llm.generate_content_async(
418+
llm_request, stream=False
419+
)
420+
]
421+
422+
assert len(responses) == 1
423+
request_contents = mock_client.aio.models.generate_content.call_args.kwargs[
424+
"contents"
425+
]
426+
assert request_contents[0].parts[0].thought_signature is None
427+
assert request_contents[2].parts[0].thought_signature is None
428+
assert request_contents[2].parts[1].thought_signature == b"latest"
429+
430+
384431
@pytest.mark.asyncio
385432
async def test_generate_content_async_stream(gemini_llm, llm_request):
386433
with mock.patch.object(gemini_llm, "api_client") as mock_client:

0 commit comments

Comments
 (0)