Skip to content

fix(responses): complete chat bridge reasoning lifecycle - #32776

Closed
mxl wants to merge 6 commits into
BerriAI:litellm_oss_stagingfrom
mxl:mxl-dev
Closed

fix(responses): complete chat bridge reasoning lifecycle#32776
mxl wants to merge 6 commits into
BerriAI:litellm_oss_stagingfrom
mxl:mxl-dev

Conversation

@mxl

@mxl mxl commented Jul 10, 2026

Copy link
Copy Markdown

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:

Problem

Strict Responses API clients such as Vercel AI SDK / OpenCode track streaming state by item_id, summary_index, and content_index. The current chat-completions -> Responses bridge can emit:

  • response.reasoning_summary_text.delta without a matching response.reasoning_summary_part.added
  • text deltas after reasoning without opening a separate message output item
  • terminal chunks with choices: [], causing IndexError on choices[0]

These shapes surface as errors like:

  • reasoning part <id>:0 not found
  • text part <id> not found
  • IndexError: list index out of range

Changes

  • Guard _get_delta_string_from_streaming_choices() for empty choices
  • Track reasoning and message output item emission separately
  • Emit response.reasoning_summary_part.added for reasoning items
  • Reuse the cached reasoning item id for response.reasoning_summary_text.delta
  • Include summary_index=0 on reasoning deltas
  • Add unit coverage for empty choices and reasoning -> text lifecycle

Tests

uv run pytest tests/test_litellm/responses/test_streaming_empty_choices.py tests/test_litellm/responses/litellm_completion_transformation/test_litellm_completion_responses.py::TestEnsureOutputItemContentPartAdded -q
# 8 passed

uv run ruff check litellm/responses/litellm_completion_transformation/streaming_iterator.py tests/test_litellm/responses/test_streaming_empty_choices.py
# All checks passed

Note: running ruff on the full existing test_litellm_completion_responses.py file reports pre-existing unused imports / variables outside this patch scope.

ansulev and others added 6 commits July 8, 2026 19:58
_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.
@CLAassistant

CLAassistant commented Jul 10, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@mxl

mxl commented Jul 10, 2026

Copy link
Copy Markdown
Author

Closing in favor of a clean branch that contains only the chat bridge reasoning lifecycle patches, separate from #32519.

@mxl mxl closed this Jul 10, 2026
@greptile-apps

greptile-apps Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes three concrete failure modes in the chat-completions → Responses API streaming bridge: an IndexError on terminal chunks with choices: [], missing response.reasoning_summary_part.added before reasoning deltas, and text deltas after reasoning arriving without a registered message output item.

  • Empty-choices guard (_get_delta_string_from_streaming_choices): a one-line early return prevents the IndexError caused by providers like DeepSeek that emit a terminal usage chunk with choices: [].
  • Reasoning→text lifecycle: sent_output_item_added_event is now scoped to the reasoning path, and a new sent_message_output_item_added_event flag lets the message item be emitted independently after a reasoning item; response.reasoning_summary_part.added is enqueued immediately after the reasoning output_item.added, and reasoning delta events now carry a stable item ID and summary_index=0.
  • Stable reasoning item ID: replaces the per-delta f\"rs_{hash(...)}\" with the cached UUID assigned at the start of the reasoning item's lifecycle.

Confidence Score: 3/5

The 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.

Important Files Changed

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)

  1. litellm/responses/litellm_completion_transformation/streaming_iterator.py, line 785-793 (link)

    P1 Message item output_index collides with the reasoning item

    When a reasoning chunk arrives first, the reasoning OutputItemAddedEvent is emitted at output_index=0. When the subsequent text chunk then reaches the "Default: message" branch, the message OutputItemAddedEvent is also emitted at output_index=0. Every downstream event for that message item — ContentPartAddedEvent, OutputTextDeltaEvent, ContentPartDoneEvent, OutputTextDoneEvent, and OutputItemDoneEvent — also hardcode output_index=0. Any client that reconstructs response.output by 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 message OutputItemAddedEvent should carry output_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

Comment on lines +768 to +776
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,
)
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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

codecov Bot commented Jul 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.75000% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...lm_completion_transformation/streaming_iterator.py 93.75% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants