Skip to content

Commit 2cea62a

Browse files
fix(google-genai): remap finish_reason to tool_calls when response contains tool calls (#3102)
1 parent d304239 commit 2cea62a

3 files changed

Lines changed: 19 additions & 7 deletions

File tree

integrations/google_genai/src/haystack_integrations/components/generators/google_genai/chat/utils.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -559,10 +559,16 @@ def _convert_google_genai_response_to_chatmessage(response: types.GenerateConten
559559

560560
usage.update(_convert_usage_metadata_to_serializable(usage_metadata))
561561

562+
# Remap finish_reason to "tool_calls" when tool calls are present, since Google GenAI returns
563+
# "STOP" for both normal completions and tool calls (no dedicated FUNCTION_CALL finish reason).
564+
mapped_finish_reason = FINISH_REASON_MAPPING.get(finish_reason or "")
565+
if mapped_finish_reason == "stop" and tool_calls:
566+
mapped_finish_reason = "tool_calls"
567+
562568
# Create meta with reasoning content and thought signatures if available
563569
meta: dict[str, Any] = {
564570
"model": model,
565-
"finish_reason": FINISH_REASON_MAPPING.get(finish_reason or ""),
571+
"finish_reason": mapped_finish_reason,
566572
"usage": usage,
567573
}
568574

@@ -675,13 +681,19 @@ def _convert_google_chunk_to_streaming_chunk(
675681
# Determine the effective content: tool_calls and reasoning take priority.
676682
effective_content = "" if tool_calls or reasoning else content
677683

684+
# Remap finish_reason to "tool_calls" when tool calls are present, since Google GenAI returns
685+
# "STOP" for both normal completions and tool calls (no dedicated FUNCTION_CALL finish reason).
686+
mapped_finish_reason = FINISH_REASON_MAPPING.get(finish_reason or "")
687+
if mapped_finish_reason == "stop" and tool_calls:
688+
mapped_finish_reason = "tool_calls"
689+
678690
return StreamingChunk(
679691
content=effective_content,
680692
tool_calls=tool_calls,
681693
component_info=component_info,
682694
index=index,
683695
start=start,
684-
finish_reason=FINISH_REASON_MAPPING.get(finish_reason or ""),
696+
finish_reason=mapped_finish_reason,
685697
meta=meta,
686698
reasoning=reasoning,
687699
)

integrations/google_genai/tests/test_chat_generator.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -673,7 +673,7 @@ def test_live_run_with_tools_streaming(self, tools):
673673

674674
assert isinstance(tool_message, ChatMessage), "Tool message is not a ChatMessage instance"
675675
assert ChatMessage.is_from(tool_message, ChatRole.ASSISTANT), "Tool message is not from the assistant"
676-
assert tool_message.meta["finish_reason"] == "stop"
676+
assert tool_message.meta["finish_reason"] == "tool_calls"
677677

678678
tool_call = tool_message.tool_calls[0]
679679
assert tool_call.tool_name == "weather"
@@ -952,7 +952,7 @@ async def test_live_run_async_with_tools(self, tools):
952952
assert len(tool_message.tool_calls) == 1, "Tool message has multiple tool calls"
953953
assert tool_message.tool_calls[0].tool_name == "weather"
954954
assert tool_message.tool_calls[0].arguments == {"city": "Paris"}
955-
assert tool_message.meta["finish_reason"] == "stop"
955+
assert tool_message.meta["finish_reason"] == "tool_calls"
956956

957957
async def test_live_run_async_with_thinking(self):
958958
"""

integrations/google_genai/tests/test_chat_generator_utils.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ def test_convert_google_chunk_to_streaming_chunk_tool_call(self, monkeypatch):
291291
assert chunk.tool_calls[0].tool_name == "weather"
292292
assert chunk.tool_calls[0].arguments == '{"city": "Paris"}'
293293
assert chunk.tool_calls[0].id == "call_123"
294-
assert chunk.finish_reason == "stop"
294+
assert chunk.finish_reason == "tool_calls"
295295
assert chunk.index == 0
296296
assert "received_at" in chunk.meta
297297
assert chunk.component_info == component_info
@@ -337,7 +337,7 @@ def test_convert_google_chunk_to_streaming_chunk_mixed_content(self, monkeypatch
337337
assert len(chunk.tool_calls) == 1
338338
assert chunk.tool_calls[0].tool_name == "weather"
339339
assert chunk.tool_calls[0].arguments == '{"city": "London"}'
340-
assert chunk.finish_reason == "stop"
340+
assert chunk.finish_reason == "tool_calls"
341341
assert chunk.component_info == component_info
342342

343343
def test_convert_google_chunk_to_streaming_chunk_empty_parts(self, monkeypatch):
@@ -513,7 +513,7 @@ def test_convert_google_chunk_to_streaming_chunk_real_example(self, monkeypatch)
513513
assert streaming_chunk.content == ""
514514
assert streaming_chunk.tool_calls is not None
515515
assert len(streaming_chunk.tool_calls) == 6
516-
assert streaming_chunk.finish_reason == "stop"
516+
assert streaming_chunk.finish_reason == "tool_calls"
517517
assert streaming_chunk.index == 2
518518
assert "received_at" in streaming_chunk.meta
519519
assert streaming_chunk.meta["model"] == "gemini-2.5-flash"

0 commit comments

Comments
 (0)