fix(responses): preserve tool call argument deltas when streaming id is omitted - #20712
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Greptile OverviewGreptile SummaryThis PR updates Confidence Score: 4/5
|
| Filename | Overview |
|---|---|
| litellm/responses/litellm_completion_transformation/streaming_iterator.py | Adds index -> call_id memoization so tool-call argument deltas with missing id can still be routed; main behavior preserved but mapping can be overwritten if providers reuse an index for multiple tool calls. |
| tests/test_litellm/responses/litellm_completion_transformation/test_tool_call_streaming_transformation.py | Adds regression tests covering streaming tool-call deltas where only the first chunk has id and later chunks rely on index, including parallel tool call routing. |
Sequence Diagram
sequenceDiagram
participant Provider as LLM Provider (chat.completion.chunk)
participant It as LiteLLMCompletionStreamingIterator
participant Q as _pending_tool_events
participant Client as Responses API stream consumer
Provider->>It: chunk delta.tool_calls[{index=0,id=call_abc,...args='{"lo'}]
It->>It: _normalize_tool_call_index() => 0
It->>It: _tool_call_id_by_index[0] = 'call_abc'
It->>It: _get_or_assign_tool_output_index('call_abc')
It->>Q: enqueue output_item.added(item.id='call_abc')
It->>Q: enqueue function_call_arguments.delta(delta='{"lo'...)
Provider->>It: chunk delta.tool_calls[{index=0,id=null,args='cation":'}]
It->>It: resolve call_id via _tool_call_id_by_index[0]
It->>Q: enqueue function_call_arguments.delta(delta='cation":'...)
Client->>It: next() / drain events
It-->>Client: output_item.added
It-->>Client: function_call_arguments.delta (multiple)
Note over It: If index is reused for a different call_id later,
Note over It: _tool_call_id_by_index can be overwritten and misroute id=null deltas.
Additional Comments (1)
|
|
Follow-up update for the Greptile edge case is now pushed. What changed:
Regression coverage:
Validation:
Commit: |
There was a problem hiding this comment.
Pull request overview
This PR fixes a Responses API streaming bug where tool-call argument deltas were dropped after the first chunk when subsequent chunks omit id and only provide index (OpenAI-style tool streaming). It adds an index -> call_id resolution mechanism so later id=None deltas are correctly attributed and streamed incrementally.
Changes:
- Track
tool_call.index -> tool_call.idduring streaming and use it to route laterid=Nonetool-call deltas. - Keep existing skip behavior when neither
idnor a previously mappedindexis available. - Add regression tests for single and parallel tool-call streaming where only the first chunk includes
id.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
litellm/responses/litellm_completion_transformation/streaming_iterator.py |
Adds index normalization + per-index call-id mapping to preserve tool argument delta streaming when id is omitted. |
tests/test_litellm/responses/litellm_completion_transformation/test_tool_call_streaming_transformation.py |
Adds regression coverage for id=None tool-call deltas (single + parallel tool calls) using index-based routing. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Greptile OverviewGreptile SummaryPreserves tool call argument deltas when streaming chunks omit the Key improvements:
This fix enables LiteLLM to handle OpenAI-style streaming where only the first chunk contains the tool call Confidence Score: 5/5
|
| Filename | Overview |
|---|---|
| litellm/responses/litellm_completion_transformation/streaming_iterator.py | Adds index-to-call_id mapping for streaming tool calls, enabling proper routing when id is omitted from subsequent chunks while guarding against ambiguous index reuse |
| tests/test_litellm/responses/litellm_completion_transformation/test_tool_call_streaming_transformation.py | Comprehensive tests for id-less tool call deltas: single tool with index fallback, parallel tool calls routing, and ambiguous index reuse detection |
Sequence Diagram
sequenceDiagram
participant Provider as LLM Provider
participant Iterator as LiteLLMCompletionStreamingIterator
participant Mapping as _tool_call_id_by_index
participant Queue as _pending_tool_events
participant Client as API Client
Note over Provider,Client: Streaming Tool Call with id in first chunk
Provider->>Iterator: chunk[0] {index:0, id:"call_abc", args:'{"lo'}
Iterator->>Iterator: _normalize_tool_call_index(tc) → 0
Iterator->>Iterator: Extract call_id = "call_abc"
Iterator->>Mapping: Store mapping[0] = "call_abc"
Iterator->>Queue: Queue OUTPUT_ITEM_ADDED event
Iterator->>Queue: Queue FUNCTION_CALL_ARGUMENTS_DELTA
Iterator->>Client: Emit queued events
Note over Provider,Client: Subsequent chunks without id
Provider->>Iterator: chunk[1] {index:0, args:'cation":'}
Iterator->>Iterator: _normalize_tool_call_index(tc) → 0
Iterator->>Iterator: call_id_raw is None/empty
Iterator->>Mapping: Lookup mapping[0] → "call_abc"
Iterator->>Iterator: Use mapped call_id = "call_abc"
Iterator->>Queue: Queue FUNCTION_CALL_ARGUMENTS_DELTA for call_abc
Iterator->>Client: Emit delta event
Provider->>Iterator: chunk[2] {index:0, args:' "New'}
Iterator->>Mapping: Lookup mapping[0] → "call_abc"
Iterator->>Queue: Queue FUNCTION_CALL_ARGUMENTS_DELTA for call_abc
Iterator->>Client: Emit delta event
Note over Provider,Client: Parallel tool calls scenario
Provider->>Iterator: chunk[0] {index:0, id:"call_a", args:'{"x":'}
Provider->>Iterator: chunk[0] {index:1, id:"call_b", args:'{"y":'}
Iterator->>Mapping: Store mapping[0] = "call_a"
Iterator->>Mapping: Store mapping[1] = "call_b"
Iterator->>Client: Emit OUTPUT_ITEM_ADDED for both
Provider->>Iterator: chunk[1] {index:0, args:'1}'}, {index:1, args:'2}'}
Iterator->>Mapping: Lookup mapping[0] → "call_a"
Iterator->>Mapping: Lookup mapping[1] → "call_b"
Iterator->>Queue: Queue deltas for call_a and call_b
Iterator->>Client: Emit deltas correctly routed
Note over Provider,Client: Ambiguous index reuse detection
Provider->>Iterator: chunk[0] {index:0, id:"call_a"}
Iterator->>Mapping: Store mapping[0] = "call_a"
Provider->>Iterator: chunk[1] {index:0, id:"call_b"}
Iterator->>Mapping: Existing mapping[0] = "call_a" != "call_b"
Iterator->>Iterator: Mark index 0 as ambiguous
Iterator->>Mapping: Update mapping[0] = "call_b"
Provider->>Iterator: chunk[2] {index:0, NO id}
Iterator->>Iterator: Check if index 0 is ambiguous
Iterator->>Iterator: Skip fallback - prevent misrouting
Note over Iterator: Delta is dropped to avoid<br/>routing to wrong call_id
…-delta-index-mapping fix(responses): preserve tool call argument deltas when streaming id is omitted
Relevant issues
Fixes #20711
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
tests/litellm/directory, Adding at least 1 test is a hard requirement - see detailsmake test-unitCI (LiteLLM team)
Branch creation CI run
Link:
CI run for the last commit
Link:
Merge / cherry-pick CI run
Links:
Type
🐛 Bug Fix
✅ Test
Changes
LiteLLMCompletionStreamingIteratorso chunks withid=Nonecan reuse a previously seencall_idfrom the sameindex.idis present, while populatingindex -> call_idmapping for later chunks.idnor a mappedindex.idand subsequent chunks only carryindex.id=Nonedeltas.Validation
poetry run pytest tests/test_litellm/responses/litellm_completion_transformation/test_tool_call_streaming_transformation.py5 passed