mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-12 23:01:41 +00:00
fix(responses): keep the reasoning item open while reasoning interleaves with content
This commit is contained in:
parent
abba4600b0
commit
8c64b7980c
2 changed files with 66 additions and 4 deletions
|
|
@ -174,13 +174,13 @@ class LiteLLMCompletionStreamingIterator(ResponsesAPIStreamingIterator):
|
|||
chunk.model_copy(update=MappingProxyType({"choices": (content_choice,)})),
|
||||
)
|
||||
|
||||
def _queue_reasoning_lifecycle_events(self, chunk: ModelResponseStream) -> None:
|
||||
def _queue_reasoning_lifecycle_events(self, chunk: ModelResponseStream, *, ends_reasoning: bool) -> None:
|
||||
if not self._reasoning_active or self._reasoning_done_emitted:
|
||||
return
|
||||
delta: Final = chunk.choices[0].delta if chunk.choices else None
|
||||
if delta and getattr(delta, "reasoning_content", None):
|
||||
self._accumulated_reasoning_content_parts.append(delta.reasoning_content)
|
||||
if not self._is_reasoning_end(chunk):
|
||||
if not ends_reasoning:
|
||||
return
|
||||
|
||||
reasoning_content: Final = "".join(self._accumulated_reasoning_content_parts)
|
||||
|
|
@ -208,10 +208,22 @@ class LiteLLMCompletionStreamingIterator(ResponsesAPIStreamingIterator):
|
|||
self._reasoning_done_emitted = True
|
||||
self._reasoning_active = False
|
||||
|
||||
def _chunk_ends_reasoning(self, chunk: ModelResponseStream, *, is_split: bool) -> bool:
|
||||
if not chunk.choices:
|
||||
return False
|
||||
# A chunk carrying both reasoning and content only ends reasoning if the turn ends
|
||||
# there too: providers that interleave the two keep emitting reasoning afterwards.
|
||||
if is_split:
|
||||
return chunk.choices[0].finish_reason is not None
|
||||
return bool(self._is_reasoning_end(chunk))
|
||||
|
||||
def _queue_events_for_chunk(self, chunk: ModelResponseStream) -> None:
|
||||
for part in self._split_reasoning_and_content(chunk):
|
||||
parts: Final = self._split_reasoning_and_content(chunk)
|
||||
ends_reasoning: Final = self._chunk_ends_reasoning(chunk, is_split=len(parts) > 1)
|
||||
for part in parts:
|
||||
self._ensure_output_item_for_chunk(part)
|
||||
self._queue_reasoning_lifecycle_events(part)
|
||||
if part is parts[0]:
|
||||
self._queue_reasoning_lifecycle_events(part, ends_reasoning=ends_reasoning)
|
||||
response_api_chunk = self._transform_chat_completion_chunk_to_response_api_chunk(part)
|
||||
if response_api_chunk:
|
||||
self._pending_response_events.append(response_api_chunk)
|
||||
|
|
|
|||
|
|
@ -375,6 +375,56 @@ def test_combined_reasoning_and_content_chunk_emits_both_deltas_sync():
|
|||
assert "".join(text_deltas) == "MARKER_ALPHA begin middle end"
|
||||
|
||||
|
||||
def _interleaved_reasoning_and_text_chunks() -> list[ModelResponseStream]:
|
||||
def make(
|
||||
content: str | None = None,
|
||||
reasoning: str | None = None,
|
||||
finish_reason: str | None = None,
|
||||
) -> ModelResponseStream:
|
||||
return ModelResponseStream(
|
||||
id="chatcmpl-interleaved",
|
||||
created=1234567890,
|
||||
model="test-model",
|
||||
object="chat.completion.chunk",
|
||||
choices=[
|
||||
StreamingChoices(
|
||||
finish_reason=finish_reason,
|
||||
index=0,
|
||||
delta=Delta(content=content, role="assistant", reasoning_content=reasoning),
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
return [
|
||||
make(reasoning="think one ", content="answer one "),
|
||||
make(reasoning="think two ", content="answer two "),
|
||||
make(reasoning="think three", content="answer three"),
|
||||
make(finish_reason="stop"),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_interleaved_reasoning_stays_open_until_the_turn_ends():
|
||||
"""Reasoning that keeps arriving alongside content must not be closed after the first chunk."""
|
||||
iterator = _new_iterator(_interleaved_reasoning_and_text_chunks())
|
||||
|
||||
types = []
|
||||
reasoning_deltas = []
|
||||
summary_done_text = None
|
||||
async for event in iterator:
|
||||
types.append(event.type)
|
||||
if event.type == "response.reasoning_summary_text.delta":
|
||||
reasoning_deltas.append(event.delta)
|
||||
elif event.type == "response.reasoning_summary_text.done":
|
||||
summary_done_text = event.text
|
||||
|
||||
assert "".join(reasoning_deltas) == "think one think two think three"
|
||||
assert summary_done_text == "".join(reasoning_deltas)
|
||||
assert types.count("response.reasoning_summary_text.done") == 1
|
||||
last_reasoning_delta = len(types) - 1 - types[::-1].index("response.reasoning_summary_text.delta")
|
||||
assert last_reasoning_delta < types.index("response.reasoning_summary_text.done")
|
||||
|
||||
|
||||
def test_streaming_chunk_id_raw():
|
||||
"""Test that streaming chunk IDs are raw (not encoded) to match OpenAI format"""
|
||||
chunk = ModelResponseStream(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue