From bb8c02d4676ed8fb7c513606f3be8a8c91177da0 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 11 May 2026 20:00:28 +0000 Subject: [PATCH] fix(vertex_ai/agent_engine): never surface finish_reason on function-call chunks Setting finish_reason='tool_calls' on intermediate function-call chunks also closes the downstream stream wrapper (handle_openai_chat_completion_chunk treats any non-None finish_reason as is_finished=True), dropping later chunks including the final text reply. Only surface finish_reason when the chunk has user-facing text. This also keeps the priority order between finish_reason and delta_kwargs consistent (both gate on text). --- .../vertex_ai/agent_engine/sse_iterator.py | 18 ++++++++---------- .../agent_engine/test_transformation.py | 4 ++-- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/litellm/llms/vertex_ai/agent_engine/sse_iterator.py b/litellm/llms/vertex_ai/agent_engine/sse_iterator.py index ba93336e75a..b731827fcf5 100644 --- a/litellm/llms/vertex_ai/agent_engine/sse_iterator.py +++ b/litellm/llms/vertex_ai/agent_engine/sse_iterator.py @@ -90,21 +90,19 @@ class VertexAgentEngineResponseIterator(BaseModelResponseIterator): end-of-response signal. We therefore only surface ``finish_reason`` when the chunk has - user-facing content (text or tool_calls). Otherwise downstream stream - handling closes the stream after the first inner action and the actual - response is dropped (see issue #19121). + user-facing text content. Function-call and thought-only chunks must + keep ``finish_reason=None`` so the downstream stream wrapper does not + close the stream after the first inner action and drop the actual + response (see issue #19121). """ text, tool_calls = self._extract_parts_from_chunk(chunk) finish_reason: Optional[str] = None raw_finish_reason = chunk.get("finish_reason") - if raw_finish_reason: - if tool_calls: - finish_reason = "tool_calls" - elif text is not None: - finish_reason = ( - "stop" if raw_finish_reason == "STOP" else raw_finish_reason.lower() - ) + if raw_finish_reason and text is not None: + finish_reason = ( + "stop" if raw_finish_reason == "STOP" else raw_finish_reason.lower() + ) usage = None usage_metadata = chunk.get("usage_metadata") or {} diff --git a/tests/litellm/llms/vertex_ai/agent_engine/test_transformation.py b/tests/litellm/llms/vertex_ai/agent_engine/test_transformation.py index 26a6d28c4a3..2b8ef9cb569 100644 --- a/tests/litellm/llms/vertex_ai/agent_engine/test_transformation.py +++ b/tests/litellm/llms/vertex_ai/agent_engine/test_transformation.py @@ -155,7 +155,7 @@ class TestVertexAgentEngineChunkParser: result = self._iterator().chunk_parser(chunk) - assert result.choices[0].finish_reason == "tool_calls" + assert result.choices[0].finish_reason is None tool_calls = result.choices[0].delta.tool_calls assert tool_calls is not None and len(tool_calls) == 1 assert tool_calls[0]["function"]["name"] == "transfer_to_agent" @@ -207,7 +207,7 @@ class TestVertexAgentEngineChunkParser: result = self._iterator().chunk_parser(chunk) - assert result.choices[0].finish_reason == "tool_calls" + assert result.choices[0].finish_reason is None tool_calls = result.choices[0].delta.tool_calls assert tool_calls is not None and len(tool_calls) == 1 assert tool_calls[0]["function"]["name"] == "list_cases"