fix(responses): complete chat bridge reasoning lifecycle - #32776
Conversation
_get_delta_string_from_streaming_choices indexes choices[0] without checking for an empty list. Providers such as DeepSeek emit a terminal streaming chunk with "choices": [] (finish/usage chunk), which raises IndexError and kills the /v1/responses stream mid-response when bridging to a chat-completions backend. Every other access in this file already guards with `chunk.choices and ...`; this applies the same guard here.
|
Closing in favor of a clean branch that contains only the chat bridge reasoning lifecycle patches, separate from #32519. |
Greptile SummaryThis PR fixes three concrete failure modes in the chat-completions → Responses API streaming bridge: an
Confidence Score: 3/5The crash fixes are correct and well-tested, but the reasoning→text path now emits two output items at the same index=0, and the new reasoning_summary_part.added event is missing fields that strict clients require. The three targeted crash bugs are fixed correctly. However, the new code that emits a message OutputItemAddedEvent after a reasoning item hardcodes output_index=0 for both — the same slot the reasoning item already claimed. All downstream message events also hardcode output_index=0. Additionally, the new response.reasoning_summary_part.added event is built without the output_index and part fields that the .done counterpart and OpenAI spec require. streaming_iterator.py — specifically _ensure_output_item_for_chunk (output_index for the new message item) and the response.reasoning_summary_part.added construction.
|
| Filename | Overview |
|---|---|
| litellm/responses/litellm_completion_transformation/streaming_iterator.py | Fixes three crash-level bugs in the chat-completions→Responses API bridge (empty choices IndexError, missing reasoning_summary_part.added, text deltas after reasoning), but introduces an output_index=0 collision between the new message item and the preceding reasoning item, and the newly emitted reasoning_summary_part.added event is missing the required output_index and part fields. |
| tests/test_litellm/responses/litellm_completion_transformation/test_litellm_completion_responses.py | Adds two new test cases covering the reasoning→text lifecycle and stable item ID/summary_index, and updates an existing test to reflect the extra reasoning_summary_part.added event; changes are legitimate and improve coverage without weakening existing assertions. |
| tests/test_litellm/responses/test_streaming_empty_choices.py | New mock-only test file verifying the empty-choices guard; no real network calls, straightforward coverage of the IndexError regression. |
Comments Outside Diff (1)
-
litellm/responses/litellm_completion_transformation/streaming_iterator.py, line 785-793 (link)Message item
output_indexcollides with the reasoning itemWhen a reasoning chunk arrives first, the reasoning
OutputItemAddedEventis emitted atoutput_index=0. When the subsequent text chunk then reaches the "Default: message" branch, the messageOutputItemAddedEventis also emitted atoutput_index=0. Every downstream event for that message item —ContentPartAddedEvent,OutputTextDeltaEvent,ContentPartDoneEvent,OutputTextDoneEvent, andOutputItemDoneEvent— also hardcodeoutput_index=0. Any client that reconstructsresponse.outputby position will have the message item silently overwrite the reasoning item in slot 0.Line 90 even documents the original contract:
_next_tool_output_index: int = 1 # output_index=0 reserved for the message item. Tool calls correctly start at index 1, but the newly emitted message item still claims the same slot as the reasoning item. When reasoning precedes text, the messageOutputItemAddedEventshould carryoutput_index=1(and all its associated delta/done events should match), consistent with how tools are offset beyond the message slot.
Reviews (1): Last reviewed commit: "test(responses): cover reasoning stream ..." | Re-trigger Greptile
| if not self._sent_reasoning_summary_part_added_event: | ||
| self._sent_reasoning_summary_part_added_event = True | ||
| self._pending_response_events.append( | ||
| BaseLiteLLMOpenAIResponseObject( | ||
| type="response.reasoning_summary_part.added", | ||
| item_id=self._cached_reasoning_item_id, | ||
| summary_index=0, | ||
| ) | ||
| ) |
There was a problem hiding this comment.
response.reasoning_summary_part.added missing output_index and part fields
The response.reasoning_summary_part.done event (whose docstring appears at line ~522) includes output_index, summary_index, and a part object ({"type": "summary_text", "text": "..."}). The new response.reasoning_summary_part.added event emitted here carries only type, item_id, and summary_index=0. Clients that strictly follow the OpenAI Responses API streaming contract (e.g. OpenCode) will attempt to deserialize the missing output_index and part fields and may reject or crash on the malformed event, leaving the reasoning lifecycle incomplete despite this PR's intent.
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Summary
Fixes Responses API streaming when the chat-completions bridge receives provider chunks that contain reasoning deltas followed by text deltas, and when terminal usage chunks have
choices: [].This combines:
response.reasoning_summary_part.addedbefore reasoning deltasProblem
Strict Responses API clients such as Vercel AI SDK / OpenCode track streaming state by
item_id,summary_index, andcontent_index. The current chat-completions -> Responses bridge can emit:response.reasoning_summary_text.deltawithout a matchingresponse.reasoning_summary_part.addedchoices: [], causingIndexErroronchoices[0]These shapes surface as errors like:
reasoning part <id>:0 not foundtext part <id> not foundIndexError: list index out of rangeChanges
_get_delta_string_from_streaming_choices()for emptychoicesresponse.reasoning_summary_part.addedfor reasoning itemsresponse.reasoning_summary_text.deltasummary_index=0on reasoning deltasTests
Note: running ruff on the full existing
test_litellm_completion_responses.pyfile reports pre-existing unused imports / variables outside this patch scope.