diff --git a/integrations/google_genai/src/haystack_integrations/components/generators/google_genai/chat/utils.py b/integrations/google_genai/src/haystack_integrations/components/generators/google_genai/chat/utils.py index 4344f44fc4..c1f7cef91f 100644 --- a/integrations/google_genai/src/haystack_integrations/components/generators/google_genai/chat/utils.py +++ b/integrations/google_genai/src/haystack_integrations/components/generators/google_genai/chat/utils.py @@ -559,10 +559,16 @@ def _convert_google_genai_response_to_chatmessage(response: types.GenerateConten usage.update(_convert_usage_metadata_to_serializable(usage_metadata)) + # Remap finish_reason to "tool_calls" when tool calls are present, since Google GenAI returns + # "STOP" for both normal completions and tool calls (no dedicated FUNCTION_CALL finish reason). + mapped_finish_reason = FINISH_REASON_MAPPING.get(finish_reason or "") + if mapped_finish_reason == "stop" and tool_calls: + mapped_finish_reason = "tool_calls" + # Create meta with reasoning content and thought signatures if available meta: dict[str, Any] = { "model": model, - "finish_reason": FINISH_REASON_MAPPING.get(finish_reason or ""), + "finish_reason": mapped_finish_reason, "usage": usage, } @@ -675,13 +681,19 @@ def _convert_google_chunk_to_streaming_chunk( # Determine the effective content: tool_calls and reasoning take priority. effective_content = "" if tool_calls or reasoning else content + # Remap finish_reason to "tool_calls" when tool calls are present, since Google GenAI returns + # "STOP" for both normal completions and tool calls (no dedicated FUNCTION_CALL finish reason). + mapped_finish_reason = FINISH_REASON_MAPPING.get(finish_reason or "") + if mapped_finish_reason == "stop" and tool_calls: + mapped_finish_reason = "tool_calls" + return StreamingChunk( content=effective_content, tool_calls=tool_calls, component_info=component_info, index=index, start=start, - finish_reason=FINISH_REASON_MAPPING.get(finish_reason or ""), + finish_reason=mapped_finish_reason, meta=meta, reasoning=reasoning, ) diff --git a/integrations/google_genai/tests/test_chat_generator.py b/integrations/google_genai/tests/test_chat_generator.py index c348f8ff97..a66745bc3e 100644 --- a/integrations/google_genai/tests/test_chat_generator.py +++ b/integrations/google_genai/tests/test_chat_generator.py @@ -673,7 +673,7 @@ def test_live_run_with_tools_streaming(self, tools): assert isinstance(tool_message, ChatMessage), "Tool message is not a ChatMessage instance" assert ChatMessage.is_from(tool_message, ChatRole.ASSISTANT), "Tool message is not from the assistant" - assert tool_message.meta["finish_reason"] == "stop" + assert tool_message.meta["finish_reason"] == "tool_calls" tool_call = tool_message.tool_calls[0] assert tool_call.tool_name == "weather" @@ -952,7 +952,7 @@ async def test_live_run_async_with_tools(self, tools): assert len(tool_message.tool_calls) == 1, "Tool message has multiple tool calls" assert tool_message.tool_calls[0].tool_name == "weather" assert tool_message.tool_calls[0].arguments == {"city": "Paris"} - assert tool_message.meta["finish_reason"] == "stop" + assert tool_message.meta["finish_reason"] == "tool_calls" async def test_live_run_async_with_thinking(self): """ diff --git a/integrations/google_genai/tests/test_chat_generator_utils.py b/integrations/google_genai/tests/test_chat_generator_utils.py index 4c8bef027c..52a77f80f5 100644 --- a/integrations/google_genai/tests/test_chat_generator_utils.py +++ b/integrations/google_genai/tests/test_chat_generator_utils.py @@ -291,7 +291,7 @@ def test_convert_google_chunk_to_streaming_chunk_tool_call(self, monkeypatch): assert chunk.tool_calls[0].tool_name == "weather" assert chunk.tool_calls[0].arguments == '{"city": "Paris"}' assert chunk.tool_calls[0].id == "call_123" - assert chunk.finish_reason == "stop" + assert chunk.finish_reason == "tool_calls" assert chunk.index == 0 assert "received_at" in chunk.meta assert chunk.component_info == component_info @@ -337,7 +337,7 @@ def test_convert_google_chunk_to_streaming_chunk_mixed_content(self, monkeypatch assert len(chunk.tool_calls) == 1 assert chunk.tool_calls[0].tool_name == "weather" assert chunk.tool_calls[0].arguments == '{"city": "London"}' - assert chunk.finish_reason == "stop" + assert chunk.finish_reason == "tool_calls" assert chunk.component_info == component_info 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) assert streaming_chunk.content == "" assert streaming_chunk.tool_calls is not None assert len(streaming_chunk.tool_calls) == 6 - assert streaming_chunk.finish_reason == "stop" + assert streaming_chunk.finish_reason == "tool_calls" assert streaming_chunk.index == 2 assert "received_at" in streaming_chunk.meta assert streaming_chunk.meta["model"] == "gemini-2.5-flash"