Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -555,7 +555,7 @@ def _convert_google_chunk_to_streaming_chunk(
content = ""
tool_calls: list[ToolCallDelta] = []
finish_reason = None
reasoning_deltas: list[dict[str, str]] = []
reasoning_deltas: list[str] = []
thought_signature_deltas: list[dict[str, Any]] = [] # Track thought signatures in streaming

if chunk.candidates:
Expand Down Expand Up @@ -606,39 +606,39 @@ def _convert_google_chunk_to_streaming_chunk(

# Handle thought parts for Gemini 2.5 series
elif hasattr(part, "thought") and part.thought:
thought_delta = {
"type": "reasoning",
"content": part.text if part.text else "",
}
reasoning_deltas.append(thought_delta)
reasoning_deltas.append(part.text if part.text else "")

# Combine reasoning deltas into a single ReasoningContent
reasoning = ReasoningContent(reasoning_text="".join(reasoning_deltas)) if reasoning_deltas else None

# start is only used by print_streaming_chunk. We try to make a reasonable assumption here but it should not be
# a problem if we change it in the future.
start = index == 0 or len(tool_calls) > 0

# Create meta with reasoning deltas and thought signatures if available
# Create meta with thought signatures if available
meta: dict[str, Any] = {
"received_at": datetime.now(timezone.utc).isoformat(),
"model": model,
"usage": usage,
}

# Add reasoning deltas to meta if available
if reasoning_deltas:
meta["reasoning_deltas"] = reasoning_deltas

# Add thought signature deltas to meta if available (for multi-turn context)
if thought_signature_deltas:
meta["thought_signature_deltas"] = thought_signature_deltas

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

My impression is that we can use the extra field of ReasoningContent (code) to store thought_signature_deltas.
In my opinion, we did something similar in #2849 for Anthropic redacted thinking and thinking signature.

Since all this info is related to reasoning, I'd like to have it grouped into ReasoningContent.

But I haven't tried, and I don't know if this is simple to implement or if it would make the code much more complex. So please try and let me know.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the suggestion! I've moved thought_signature_deltas into ReasoningContent.extra for reasoning chunks, consistent with the Anthropic approach in #2849.

One caveat: StreamingChunk enforces mutual exclusivity between content and reasoning (raises ValueError in __post_init__), so for text/tool-call chunks that also carry thought signatures, the signatures still go in meta. The aggregation logic reads from both sources. This keeps all reasoning-related info grouped into ReasoningContent where possible without breaking the constraint.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Seeing the code, I now realize that Thought Signatures can be included in non-reasoning response parts.

For this reason, I recommend going back to the previous version of your code where thought_signature_deltas are always stored in meta. This would be simpler and consistent.
I'd just add a comment on top of the code line explaining that this data can be part of non-reasoning content parts.

Makes sense?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Makes sense! Reverted in bd3d479thought_signature_deltas are now always stored in meta with a comment explaining that thought signatures can appear in both reasoning and non-reasoning response parts.

# StreamingChunk allows only one of content/tool_calls/reasoning to be set.
# Determine the effective content: tool_calls and reasoning take priority.
effective_content = "" if tool_calls or reasoning else content

return StreamingChunk(
content="" if tool_calls else content, # prioritize tool calls over content when both are present
content=effective_content,
tool_calls=tool_calls,
component_info=component_info,
index=index,
start=start,
finish_reason=FINISH_REASON_MAPPING.get(finish_reason or ""),
meta=meta,
reasoning=reasoning,
)


Expand All @@ -662,13 +662,9 @@ def _aggregate_streaming_chunks_with_reasoning(chunks: list[StreamingChunk]) ->
thoughts_token_count = None

for chunk in chunks:
# Extract reasoning deltas
if chunk.meta and "reasoning_deltas" in chunk.meta:
reasoning_deltas = chunk.meta["reasoning_deltas"]
if isinstance(reasoning_deltas, list):
for delta in reasoning_deltas:
if delta.get("type") == "reasoning":
reasoning_text_parts.append(delta.get("content", ""))
# Extract reasoning from the StreamingChunk.reasoning field
if chunk.reasoning and chunk.reasoning.reasoning_text:
reasoning_text_parts.append(chunk.reasoning.reasoning_text)

# Extract thought signature deltas (for multi-turn context preservation)
if chunk.meta and "thought_signature_deltas" in chunk.meta:
Expand Down
35 changes: 32 additions & 3 deletions integrations/google_genai/tests/test_chat_generator_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,38 @@ def test_convert_google_chunk_to_streaming_chunk_real_example(self, monkeypatch)
assert streaming_chunk.tool_calls[5].id is None
assert streaming_chunk.tool_calls[5].index == 5

def test_convert_google_chunk_to_streaming_chunk_with_thought(self, monkeypatch):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could you try using actual objects from Google API in this test?
See test_convert_google_chunk_to_streaming_chunk_real_example for an example

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good call — updated the test to use actual types.Part, types.Content, types.Candidate, and types.GenerateContentResponse objects, following the pattern in test_convert_google_chunk_to_streaming_chunk_real_example.

"""Test that thought parts populate StreamingChunk.reasoning instead of meta."""
monkeypatch.setenv("GOOGLE_API_KEY", "test-api-key")
component = GoogleGenAIChatGenerator()
component_info = ComponentInfo.from_component(component)

# Simulate a chunk with a thought part (reasoning-only chunk)
mock_thought_part = Mock()
mock_thought_part.text = "Let me think about this..."
mock_thought_part.thought = True
mock_thought_part.function_call = None

mock_content = Mock()
mock_content.parts = [mock_thought_part]
mock_candidate = Mock()
mock_candidate.content = mock_content
mock_candidate.finish_reason = None

mock_chunk = Mock()
mock_chunk.candidates = [mock_candidate]
mock_chunk.usage_metadata = None

streaming_chunk = _convert_google_chunk_to_streaming_chunk(
chunk=mock_chunk, index=0, component_info=component_info, model="gemini-2.5-flash"
)

# Reasoning should be in the reasoning field, not in meta
assert streaming_chunk.reasoning is not None
assert streaming_chunk.reasoning.reasoning_text == "Let me think about this..."
assert "reasoning_deltas" not in streaming_chunk.meta
assert streaming_chunk.content == ""

def test_aggregate_streaming_chunks_with_reasoning(self):
"""Test the _aggregate_streaming_chunks_with_reasoning function for reasoning content aggregation."""

Expand All @@ -515,9 +547,6 @@ def test_aggregate_streaming_chunks_with_reasoning(self):
}
final_chunk.reasoning = ReasoningContent(reasoning_text="I should greet the user politely")

# Add reasoning deltas to the final chunk meta (this is how the real method works)
final_chunk.meta["reasoning_deltas"] = [{"type": "reasoning", "content": "I should greet the user politely"}]

# Test aggregation
result = _aggregate_streaming_chunks_with_reasoning([chunk1, chunk2, final_chunk])

Expand Down