From 621e54282db7a0621c326e636c6fb70c32ac397e Mon Sep 17 00:00:00 2001 From: Sureel Bhurat Date: Sun, 23 Nov 2025 09:21:37 -0500 Subject: [PATCH 1/2] Fix: Map finish_reason for LiteLLM streaming responses Fixes #3665 Streaming responses from LiteLLM models (Claude, GPT, etc.) were not setting finish_reason on aggregated LlmResponse objects, causing agent runners to not properly recognize completion states. This fix mirrors the finish_reason mapping logic from the non-streaming path (lines 776-784) and applies it to both streaming code paths: - Tool call responses (lines 1340-1368) - Text-only responses (lines 1369-1390) Without this fix, agents using Claude or GPT via LiteLLM would encounter stop conditions that couldn't be properly handled, leading to incomplete responses or unexpected agent behavior. Tested with Claude Sonnet 4.5 and GPT-5 via Azure OpenAI in production multi-agent system with MCP tools. --- src/google/adk/models/lite_llm.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/google/adk/models/lite_llm.py b/src/google/adk/models/lite_llm.py index 9e3698b190..5a6b0b2e91 100644 --- a/src/google/adk/models/lite_llm.py +++ b/src/google/adk/models/lite_llm.py @@ -1348,6 +1348,20 @@ async def generate_content_async( else None, ) ) + # FIX: Map finish_reason to FinishReason enum for streaming responses. + # Previously, streaming responses did not set finish_reason on aggregated + # LlmResponse objects, causing the ADK agent runner to not properly recognize + # completion states. This mirrors the logic from non-streaming path (lines 776-784) + # to ensure consistent behavior across both streaming and non-streaming modes. + # Without this, Claude and other models via LiteLLM would hit stop conditions + # that the agent couldn't properly handle. + if isinstance(finish_reason, types.FinishReason): + aggregated_llm_response_with_tool_call.finish_reason = finish_reason + else: + finish_reason_str = str(finish_reason).lower() + aggregated_llm_response_with_tool_call.finish_reason = _FINISH_REASON_MAPPING.get( + finish_reason_str, types.FinishReason.OTHER + ) text = "" reasoning_parts = [] function_calls.clear() @@ -1362,6 +1376,20 @@ async def generate_content_async( if reasoning_parts else None, ) + # FIX: Map finish_reason to FinishReason enum for streaming text-only responses. + # Previously, streaming responses did not set finish_reason on aggregated + # LlmResponse objects, causing the ADK agent runner to not properly recognize + # completion states. This mirrors the logic from non-streaming path (lines 776-784) + # to ensure consistent behavior across both streaming and non-streaming modes. + # Without this, Claude and other models via LiteLLM would hit stop conditions + # that the agent couldn't properly handle. + if isinstance(finish_reason, types.FinishReason): + aggregated_llm_response.finish_reason = finish_reason + else: + finish_reason_str = str(finish_reason).lower() + aggregated_llm_response.finish_reason = _FINISH_REASON_MAPPING.get( + finish_reason_str, types.FinishReason.OTHER + ) text = "" reasoning_parts = [] From 4998e2d6dff3202c0bac8f5ca9237c835d54749d Mon Sep 17 00:00:00 2001 From: Sureel Bhurat Date: Fri, 19 Dec 2025 12:00:44 +0530 Subject: [PATCH 2/2] style: Run autoformat.sh to fix lint errors --- src/google/adk/models/lite_llm.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/google/adk/models/lite_llm.py b/src/google/adk/models/lite_llm.py index 5a6b0b2e91..87079d60d0 100644 --- a/src/google/adk/models/lite_llm.py +++ b/src/google/adk/models/lite_llm.py @@ -1356,11 +1356,15 @@ async def generate_content_async( # Without this, Claude and other models via LiteLLM would hit stop conditions # that the agent couldn't properly handle. if isinstance(finish_reason, types.FinishReason): - aggregated_llm_response_with_tool_call.finish_reason = finish_reason + aggregated_llm_response_with_tool_call.finish_reason = ( + finish_reason + ) else: finish_reason_str = str(finish_reason).lower() - aggregated_llm_response_with_tool_call.finish_reason = _FINISH_REASON_MAPPING.get( - finish_reason_str, types.FinishReason.OTHER + aggregated_llm_response_with_tool_call.finish_reason = ( + _FINISH_REASON_MAPPING.get( + finish_reason_str, types.FinishReason.OTHER + ) ) text = "" reasoning_parts = [] @@ -1387,8 +1391,10 @@ async def generate_content_async( aggregated_llm_response.finish_reason = finish_reason else: finish_reason_str = str(finish_reason).lower() - aggregated_llm_response.finish_reason = _FINISH_REASON_MAPPING.get( - finish_reason_str, types.FinishReason.OTHER + aggregated_llm_response.finish_reason = ( + _FINISH_REASON_MAPPING.get( + finish_reason_str, types.FinishReason.OTHER + ) ) text = "" reasoning_parts = []