fix: keep one reasoning item id across a bridged stream

Write the fallback reasoning item id back to the cache so the
reasoning-done path and the completed snapshot cannot drift apart, and
cover the shared delta id and the snapshot alignment with tests.
This commit is contained in:
mateo-berri 2026-08-22 11:42:59 -07:00
parent 05ee5756e7
commit 6d2b7db2fb
2 changed files with 59 additions and 1 deletions

View file

@ -867,9 +867,10 @@ class LiteLLMCompletionStreamingIterator(ResponsesAPIStreamingIterator):
reasoning_content = "".join(self._accumulated_reasoning_content_parts)
# Ensure we have a valid reasoning_item_id
reasoning_item_id = (
self._cached_reasoning_item_id = (
self._reasoning_item_id or self._cached_reasoning_item_id or f"rs_{uuid.uuid4()}"
)
reasoning_item_id = self._cached_reasoning_item_id
# Create text.done event first with its own sequence number
self._sequence_number += 1

View file

@ -3945,3 +3945,60 @@ class TestStreamingSnapshotItemIds:
for item in _bridged_output_items(completed_event.response, "message"):
assert item.id.startswith("msg_")
assert not item.id.startswith("chatcmpl-")
def _make_reasoning_chunk(self, reasoning_content):
from litellm.types.utils import Delta, ModelResponseStream, StreamingChoices
return ModelResponseStream(
id=BRIDGED_CHAT_COMPLETION_ID,
choices=[
StreamingChoices(
index=0,
delta=Delta(role="assistant", reasoning_content=reasoning_content),
finish_reason=None,
)
],
created=1717000000,
model="claude-sonnet-4-5",
object="chat.completion.chunk",
)
def _reasoning_chat_completion_response(self):
message = Message(role="assistant", content="apple")
message.reasoning_content = "thinking about fruit"
return _bridged_chat_completion_response(
choices=[Choices(index=0, finish_reason="stop", message=message)]
)
def test_reasoning_delta_events_share_one_item_id(self):
"""The old rs_{hash(text)} ID changed with every delta, so a client accumulating
reasoning by item ID saw a new item per chunk."""
iterator = self._make_iterator()
first = iterator._transform_chat_completion_chunk_to_response_api_chunk(
self._make_reasoning_chunk("thinking ")
)
second = iterator._transform_chat_completion_chunk_to_response_api_chunk(
self._make_reasoning_chunk("about fruit")
)
assert first is not None and second is not None
assert first.item_id.startswith("rs_")
assert first.item_id == second.item_id
def test_completed_snapshot_reuses_streamed_reasoning_item_id(self):
iterator = self._make_iterator()
streamed_event = iterator._transform_chat_completion_chunk_to_response_api_chunk(
self._make_reasoning_chunk("thinking about fruit")
)
assert streamed_event is not None
completed_event = iterator._emit_response_completed_event(
self._reasoning_chat_completion_response()
)
assert completed_event is not None
reasoning_items = _bridged_output_items(completed_event.response, "reasoning")
assert len(reasoning_items) == 1
assert reasoning_items[0].id == streamed_event.item_id