Skip to content

Capture thought signatures from Google models #2305

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

TariqAHassan
Copy link

Overview

Capture the thought signature for Google Models.

Note: This requires casting the bytes Google provides to a base64-encoded string for compatibility with ThoughtPart()'s signature field, and then back to bytes when returning the response to Google.

Closes #2293

# thought_signature=_base64_string_to_bytes(item.signature),
# text=item.content,
# )
# )
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NOTE: We don't send ThinkingPart to the providers yet. If you are unsatisfied with this,

Can we enable it here, or is there a broader rule about this in place?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah we can return the signatures, just not the actual thought text.

@@ -682,7 +682,7 @@ class ThinkingPart:
signature: str | None = None
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose the alternative to base64 encoding/decoding would be do signature: str | bytes | None = None, but I'm not sure what kind of ripple effects that would produce.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's do that

@@ -511,7 +525,7 @@ def _process_response_from_parts(
for part in parts:
if part.text is not None:
if part.thought:
items.append(ThinkingPart(content=part.text))
items.append(ThinkingPart(content=part.text, signature=_bytes_to_base64_string(part.thought_signature)))
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I don't think this is going to work.

The thinking signature in the example (see Appendix 1) comes right after the thinking part.

So, I think support for a signature field would need to be added to ToolCallPart() and TextPart()?

Copy link
Contributor

@DouweM DouweM Jul 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, interesting, I had missed that detail in reading your issue originally.

For consistency with Anthropic, and to not have to modify all the other part classes with this Google-specific detail, I would prefer to store the signature on the most recent thinking part in Pydantic AI representation, and then to include it on the (next) text/tool part when sending it back to Google. Can you see if you can make that work?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that we'll also need to handle the signature when streaming, which is implemented here:

yield self._parts_manager.handle_thinking_delta(vendor_part_id='thinking', content=part.text)

Since the signature will come on a text/tool message, we'll want to check if its set there, and if so call handle_thinking_delta(vendor_part_id='thinking', signature=signature) before we call handle_text_delta or handle_tool_call_delta so that the signature will be added to the most recently seen thinking part.

@@ -511,7 +525,7 @@ def _process_response_from_parts(
for part in parts:
if part.text is not None:
if part.thought:
items.append(ThinkingPart(content=part.text))
items.append(ThinkingPart(content=part.text, signature=_bytes_to_base64_string(part.thought_signature)))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that we'll also need to handle the signature when streaming, which is implemented here:

yield self._parts_manager.handle_thinking_delta(vendor_part_id='thinking', content=part.text)

Since the signature will come on a text/tool message, we'll want to check if its set there, and if so call handle_thinking_delta(vendor_part_id='thinking', signature=signature) before we call handle_text_delta or handle_tool_call_delta so that the signature will be added to the most recently seen thinking part.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Thought Signatures With Google Gemini Models
2 participants