mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-14 09:03:16 +00:00
fix(responses): preserve mixed reasoning and answer deltas on lts
Partial backport of #46754 to lts/1.108.x. Source merge commit a8113b0dd7. Preserve the reasoning/answer split only, excluding the unrelated Anthropic compaction changes.
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
773b783555
commit
dafe96c05a
2 changed files with 63 additions and 4 deletions
|
|
@ -91,6 +91,11 @@ def _delta_has_signed_thinking_block(delta: object) -> bool:
|
|||
return any(isinstance(b, dict) and (b.get("signature") or b.get("data")) for b in blocks)
|
||||
|
||||
|
||||
def _delta_carries_reasoning_and_answer(delta: ChatCompletionDelta) -> bool:
|
||||
has_reasoning: Final = bool(getattr(delta, "reasoning_content", None)) or _delta_has_signed_thinking_block(delta)
|
||||
return has_reasoning and bool(delta.content or delta.tool_calls)
|
||||
|
||||
|
||||
def _delta_carries_output(delta: ChatCompletionDelta) -> bool:
|
||||
fields: Final = (
|
||||
delta.content,
|
||||
|
|
@ -553,6 +558,26 @@ class LiteLLMCompletionStreamingIterator(ResponsesAPIStreamingIterator):
|
|||
self._buffered_chunk = None
|
||||
return buffered
|
||||
|
||||
def _reasoning_part_first(self, chunk: ModelResponseStream) -> ModelResponseStream:
|
||||
if not chunk.choices or not _delta_carries_reasoning_and_answer(chunk.choices[0].delta):
|
||||
return chunk
|
||||
first_choice: Final = chunk.choices[0]
|
||||
delta: Final = first_choice.delta
|
||||
answer_delta: Final = delta.model_copy(update={"reasoning_content": None, "thinking_blocks": None})
|
||||
self._buffered_chunk = chunk.model_copy(
|
||||
update={"choices": [first_choice.model_copy(update={"delta": answer_delta}), *chunk.choices[1:]]}
|
||||
)
|
||||
reasoning_delta: Final = ChatCompletionDelta(
|
||||
reasoning_content=getattr(delta, "reasoning_content", None),
|
||||
thinking_blocks=delta.thinking_blocks if hasattr(delta, "thinking_blocks") else None,
|
||||
)
|
||||
return chunk.model_copy(
|
||||
update={
|
||||
"provider_specific_fields": None,
|
||||
"choices": [first_choice.model_copy(update={"delta": reasoning_delta, "finish_reason": None})],
|
||||
}
|
||||
)
|
||||
|
||||
def _with_encoded_response_id(self, response: ResponsesAPIResponse) -> ResponsesAPIResponse:
|
||||
return ResponsesAPIRequestUtils.update_responses_api_response_id_with_model_id(
|
||||
responses_api_response=response,
|
||||
|
|
@ -1052,7 +1077,7 @@ class LiteLLMCompletionStreamingIterator(ResponsesAPIStreamingIterator):
|
|||
raise StopAsyncIteration
|
||||
chunk = await self.litellm_custom_stream_wrapper.__anext__()
|
||||
if chunk is not None:
|
||||
chunk = cast(ModelResponseStream, chunk)
|
||||
chunk = self._reasoning_part_first(cast(ModelResponseStream, chunk))
|
||||
for src in (
|
||||
getattr(chunk, "provider_specific_fields", None),
|
||||
getattr(
|
||||
|
|
@ -1151,11 +1176,11 @@ class LiteLLMCompletionStreamingIterator(ResponsesAPIStreamingIterator):
|
|||
try:
|
||||
buffered_chunk = self._take_buffered_chunk()
|
||||
if buffered_chunk is not None:
|
||||
chunk = buffered_chunk
|
||||
chunk = self._reasoning_part_first(buffered_chunk)
|
||||
elif self._upstream_exhausted:
|
||||
raise StopIteration
|
||||
else:
|
||||
chunk = self.litellm_custom_stream_wrapper.__next__()
|
||||
chunk = self._reasoning_part_first(self.litellm_custom_stream_wrapper.__next__())
|
||||
for src in (
|
||||
getattr(chunk, "provider_specific_fields", None),
|
||||
getattr(
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ from typing import Final
|
|||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
from openai.types.responses import ResponseOutputMessage
|
||||
|
||||
from litellm.responses.litellm_completion_transformation.streaming_iterator import (
|
||||
LiteLLMCompletionStreamingIterator,
|
||||
|
|
@ -23,9 +24,12 @@ from litellm.responses.litellm_completion_transformation.streaming_iterator impo
|
|||
from litellm.responses.utils import ResponsesAPIRequestUtils
|
||||
from litellm.types.llms.openai import (
|
||||
BaseLiteLLMOpenAIResponseObject,
|
||||
OutputTextDeltaEvent,
|
||||
ReasoningSummaryTextDeltaEvent,
|
||||
ResponseCompletedEvent,
|
||||
ResponsesAPIStreamEvents,
|
||||
)
|
||||
from litellm.types.responses.main import build_web_search_call
|
||||
from litellm.types.responses.main import GenericResponseOutputItem, build_web_search_call
|
||||
from litellm.types.utils import (
|
||||
Delta,
|
||||
ModelResponse,
|
||||
|
|
@ -1016,6 +1020,36 @@ async def _collect_events(
|
|||
return [event async for event in iterator]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("sync_mode", (True, False))
|
||||
@pytest.mark.parametrize("prefix", ("", "First "))
|
||||
async def test_mixed_reasoning_chunk_keeps_the_first_answer_delta(sync_mode: bool, prefix: str) -> None:
|
||||
mixed: Final = ModelResponseStream(
|
||||
id=CHAT_COMPLETION_ID,
|
||||
created=1748575031,
|
||||
model="claude-haiku-4-5",
|
||||
choices=[StreamingChoices(index=0, delta=Delta(reasoning_content="multiply", content="1"))],
|
||||
)
|
||||
chunks: Final = (
|
||||
*((_reasoning_chunk(prefix),) if prefix else ()),
|
||||
mixed,
|
||||
_chunk("9 × 29 = **551**", finish_reason="stop"),
|
||||
)
|
||||
events: Final = await _collect_events(_build_iterator(chunks), sync_mode)
|
||||
text: Final = "".join(event.delta for event in events if isinstance(event, OutputTextDeltaEvent))
|
||||
reasoning: Final = "".join(event.delta for event in events if isinstance(event, ReasoningSummaryTextDeltaEvent))
|
||||
completed: Final = next(event for event in events if isinstance(event, ResponseCompletedEvent))
|
||||
message_texts: Final = (
|
||||
"".join(part.text for part in item.content if part.type == "output_text")
|
||||
for item in completed.response.output
|
||||
if isinstance(item, (ResponseOutputMessage, GenericResponseOutputItem)) and item.type == "message"
|
||||
)
|
||||
completed_text: Final = "".join(message_texts)
|
||||
|
||||
assert text == completed_text == "19 × 29 = **551**"
|
||||
assert reasoning == f"{prefix}multiply"
|
||||
|
||||
|
||||
def _is_message_item(event: BaseLiteLLMOpenAIResponseObject) -> bool:
|
||||
return getattr(getattr(event, "item", None), "type", None) == "message"
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue