fix(models): keep every parallel tool call streamed with the finish reason - #7200
Open
AtulJoshi1206 wants to merge 1 commit into
Open
AtulJoshi1206 wants to merge 1 commit into
AtulJoshi1206 wants to merge 1 commit into
Conversation
…eason _model_response_to_chunk repeats a chunk's finish_reason on every tool call that chunk carries, and the finalizer ran inside that per-chunk loop. On a chunk holding several parallel calls it therefore finalized after the first one and _reset_stream_buffers cleared the rest, so the agent ran one tool of N and the model was never told about the others. Streaming and non-streaming disagreed on the same payload. This is the ordinary shape on LiteLLM's Gemini route, which stamps tool_calls on the chunk carrying the calls. Only a chunk with no further tool-call delta ends the segment now; every other terminal reason falls through to the end-of-stream finalizer, which replays last_finish_reason. The text branch gains an explicit `not function_calls` so it cannot claim a reason the tool branch used to catch and wipe the buffered calls.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Please ensure you have read the contribution guide before creating a pull request.
Link to Issue or Description of Change
No existing issue; described below following the bug-report structure.
Describe the Bug:
When a provider streams several parallel tool calls on the chunk that also carries
finish_reason,LiteLlm.generate_content_asynckeeps only the last one. The others are dropped silently: no error, no log, no partial response._model_response_to_chunkyields oneFunctionChunkper tool call in a chunk and stamps that chunk'sfinish_reasonon every one of those yields. The finalizer sits inside that same per-chunk loop:So on a chunk carrying N calls, the first call satisfies the condition, the segment is finalized, and
_reset_stream_buffers()clearsfunction_calls. The second call then enters an empty accumulator and finalizes again, overwritingaggregated_llm_response_with_tool_call. Only the final call survives.Impact:
The agent executes one of N tools and the model is never told the rest were requested, so a parallel-tool turn silently does part of its work. The non-streaming path on the same payload returns every call, so
stream=Trueandstream=Falsedisagree.This is the normal shape on LiteLLM's Gemini/Vertex route rather than an exotic one. In the installed litellm,
VertexGeminiConfig._check_finish_reasonreturns"tool_calls"for any message carrying tool calls,_create_streaming_choicestamps it on the very chunk that carries them, and_apply_stream_candidatesrewrites"stop"to"tool_calls"on tool-call chunks. Gemini emits allfunctionCallparts of a candidate in a single chunk.Steps to Reproduce:
On current
main(665ec9835), stream oneModelResponseStreamwithfinish_reason="tool_calls"and two tool calls:Observed Behavior:
get_weatheris gone.Expected Behavior:
Both calls present on the final response, matching what the non-streaming path returns for the same payload.
Solution:
A finish reason only ends the tool-call segment when it arrives on a chunk that carries no further tool-call delta, that is when
chunk is None. Every other terminal reason falls through to the end-of-stream finalizer that already exists below, which replayslast_finish_reason, so the reportedfinish_reasonis unchanged.Two details worth flagging for review:
test_generate_content_async_stream_reason_does_not_carry_over. Thechunk is Noneguard is what preserves that test's intent."length"reason arriving on a tool-call chunk no longer lands there, so the textelifbelow could have claimed it and wiped the buffered calls via_reset_stream_buffers(). That branch therefore gains an explicitnot function_calls, which keeps the original precedence of tool calls over buffered text.Testing Plan
Unit Tests:
Added
test_generate_content_async_stream_keeps_parallel_tool_calls, which asserts both calls and their arguments survive. Verified it fails on an unmodifiedlite_llm.pyand passes with the change.Manual End-to-End (E2E) Tests:
Driven through
LiteLlm.generate_content_asyncwith a stubbedacompletion, since a live parallel-tool turn needs provider credentials. Three shapes were checked, all failing before and passing after:finish_reasonChecklist
Additional context
Streaming branch only. Partial yields, usage and grounding attachment, the MAX_TOKENS and MALFORMED_FUNCTION_CALL branches, and the text path are untouched. A single-call stream produces an identical response, finalized one loop iteration later by the end-of-stream finalizer.
🤖 Generated with Claude Code