From 7d5cb8ebb287ea4d9daec840d15b5210dfafa037 Mon Sep 17 00:00:00 2001 From: Cesar Garcia <128240629+Chesars@users.noreply.github.com> Date: Thu, 20 Nov 2025 00:09:37 -0300 Subject: [PATCH] fix(gemini): Add reasoning_content to streaming responses with tools (#16854) Fixes #16805 When using Gemini models (2.5/3.0) with streaming + tools enabled, the reasoning_content field was missing from stream chunks, even though thinking_blocks were present in non-streaming responses. Changes: - Convert thinking_blocks to reasoning_content for streaming responses - Extract "thinking" field from each thinking_block - Concatenate multiple thinking parts with newlines - Assign to reasoning_content in chat_completion_message for streaming Testing: - Added test_streaming_chunk_with_tool_calls_includes_reasoning_content - Test verifies reasoning_content appears with tool calls in streaming - All 39 existing Gemini tests pass --- .../vertex_and_google_ai_studio_gemini.py | 13 +++++ ...test_vertex_and_google_ai_studio_gemini.py | 52 +++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py b/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py index d83096b26b..72cd8116f9 100644 --- a/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py +++ b/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py @@ -1673,6 +1673,19 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): if thinking_blocks is not None: chat_completion_message["thinking_blocks"] = thinking_blocks # type: ignore + # Convert thinking_blocks to reasoning_content for streaming + # This ensures reasoning_content is available in streaming responses + if isinstance(model_response, ModelResponseStream) and reasoning_content is None: + reasoning_content_parts = [] + for block in thinking_blocks: + thinking_text = block.get("thinking") + if thinking_text: + reasoning_content_parts.append(thinking_text) + + if reasoning_content_parts: + reasoning_content = "\n".join(reasoning_content_parts) + chat_completion_message["reasoning_content"] = reasoning_content + if isinstance(model_response, ModelResponseStream): choice = VertexGeminiConfig._create_streaming_choice( chat_completion_message=chat_completion_message, diff --git a/tests/test_litellm/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio_gemini.py b/tests/test_litellm/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio_gemini.py index 2e0e9b4d01..5de6bbbced 100644 --- a/tests/test_litellm/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio_gemini.py +++ b/tests/test_litellm/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio_gemini.py @@ -390,6 +390,58 @@ def test_streaming_chunk_includes_reasoning_content(): ) +def test_streaming_chunk_with_tool_calls_includes_reasoning_content(): + """ + Test for issue #16805: Ensure that when Gemini returns a streaming chunk with + tool calls AND thoughtSignature, the reasoning_content is included in the delta. + + Previously, thinking_blocks were only added to non-streaming responses, causing + reasoning_content to be missing in streaming mode when tools were enabled. + """ + from litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini import ( + ModelResponseIterator, + ) + + litellm_logging = MagicMock() + + chunk = { + "candidates": [ + { + "content": { + "parts": [ + { + "functionCall": { + "name": "get_current_time", + "args": {"timezone": "America/New_York"}, + }, + "thoughtSignature": "EsEDCr4DAdHtim...", # Base64 signature + } + ] + }, + "finishReason": "STOP", + } + ], + "usageMetadata": { + "promptTokenCount": 68, + "candidatesTokenCount": 120, + "totalTokenCount": 188, + }, + } + + iterator = ModelResponseIterator( + streaming_response=[], sync_stream=True, logging_obj=litellm_logging + ) + streaming_chunk = iterator.chunk_parser(chunk) + + # Verify that reasoning_content is present in the streaming delta + assert streaming_chunk.choices[0].delta.reasoning_content is not None + + # Verify tool calls are also present + assert streaming_chunk.choices[0].delta.tool_calls is not None + assert len(streaming_chunk.choices[0].delta.tool_calls) == 1 + assert streaming_chunk.choices[0].delta.tool_calls[0].function.name == "get_current_time" + + def test_check_finish_reason(): finish_reason_mappings = VertexGeminiConfig.get_finish_reason_mapping() for k, v in finish_reason_mappings.items():