mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-13 08:52:02 +00:00
fix(streaming): keep bridged reasoning summaries on choice 0 and infer an unreported choice finish reason
This commit is contained in:
parent
091d564928
commit
7169634df2
5 changed files with 75 additions and 9 deletions
|
|
@ -2130,7 +2130,7 @@ class OpenAiResponsesToChatCompletionStreamIterator(BaseModelResponseIterator):
|
|||
return ModelResponseStream(
|
||||
choices=[
|
||||
StreamingChoices(
|
||||
index=cast(int, parsed_chunk.get("summary_index")),
|
||||
index=0,
|
||||
delta=Delta(reasoning_content=content_part),
|
||||
)
|
||||
]
|
||||
|
|
|
|||
|
|
@ -385,8 +385,8 @@ class ChunkProcessor:
|
|||
return "assistant"
|
||||
|
||||
@staticmethod
|
||||
def _finish_reason_of_chunks(chunks: Sequence["_BaseChunk"]) -> object:
|
||||
finish_reason = "stop"
|
||||
def _finish_reason_of_chunks(chunks: Sequence["_BaseChunk"], unreported: str = "stop") -> object:
|
||||
finish_reason = unreported
|
||||
for chunk in chunks:
|
||||
if "choices" in chunk and len(chunk["choices"]) > 0:
|
||||
chunk_finish_reason = None
|
||||
|
|
@ -402,7 +402,9 @@ class ChunkProcessor:
|
|||
def build_base_choice(index: int, chunks: Sequence["_BaseChunk"]) -> Choices:
|
||||
return Choices(
|
||||
index=index,
|
||||
finish_reason=ChunkProcessor._finish_reason_of_chunks(chunks),
|
||||
finish_reason=ChunkProcessor._finish_reason_of_chunks(
|
||||
chunks, unreported="tool_calls" if _carries_tool_calls(chunks) else "stop"
|
||||
),
|
||||
message={"role": ChunkProcessor._get_role_from_chunks(chunks), "content": ""},
|
||||
)
|
||||
|
||||
|
|
@ -1532,6 +1534,13 @@ def _stream_choice_index(choice: object) -> int:
|
|||
return index if isinstance(index, int) else 0
|
||||
|
||||
|
||||
def _carries_tool_calls(chunks: Sequence[_BaseChunk]) -> bool:
|
||||
return any(
|
||||
"tool_calls" in choice["delta"] and choice["delta"]["tool_calls"]
|
||||
for choice in chain.from_iterable(chunk.get("choices") or () for chunk in chunks)
|
||||
)
|
||||
|
||||
|
||||
def _choice_streams(chunks: Sequence[_BaseChunk]) -> tuple[tuple[int, Sequence[_BaseChunk]], ...]:
|
||||
indices: Final = sorted(
|
||||
frozenset(map(_stream_choice_index, chain.from_iterable(chunk.get("choices") or () for chunk in chunks)))
|
||||
|
|
|
|||
|
|
@ -687,11 +687,6 @@ class OpenAIChatCompletionsHandler(BaseTranslation):
|
|||
responses_so_far: Sequence["ModelResponseStream"],
|
||||
litellm_logging_obj: "LiteLLMLoggingObj | None",
|
||||
) -> "ModelResponse":
|
||||
"""``stream_chunk_builder`` folds every choice of a stream into one index-0
|
||||
choice, so the stream is rebuilt one choice index at a time (every chunk
|
||||
kept, its choices narrowed to that index, so usage-only chunks still
|
||||
count) and the rebuilt choices are stitched into one response, each
|
||||
carrying the index the stream gave it."""
|
||||
choice_indices: Final = tuple(
|
||||
sorted(frozenset(choice.index for response in responses_so_far for choice in response.choices))
|
||||
)
|
||||
|
|
|
|||
|
|
@ -418,6 +418,35 @@ def test_openai_responses_chunk_parser_reasoning_summary():
|
|||
assert delta.function_call is None
|
||||
|
||||
|
||||
def test_bridged_reasoning_summary_parts_rebuild_into_the_one_choice_they_streamed_on():
|
||||
from litellm.completion_extras.litellm_responses_transformation.transformation import (
|
||||
OpenAiResponsesToChatCompletionStreamIterator,
|
||||
)
|
||||
|
||||
iterator = OpenAiResponsesToChatCompletionStreamIterator(streaming_response=None, sync_stream=True)
|
||||
events = (
|
||||
{
|
||||
"type": "response.reasoning_summary_text.delta",
|
||||
"delta": "First part.",
|
||||
"output_index": 0,
|
||||
"summary_index": 0,
|
||||
},
|
||||
{
|
||||
"type": "response.reasoning_summary_text.delta",
|
||||
"delta": "Second part.",
|
||||
"output_index": 0,
|
||||
"summary_index": 1,
|
||||
},
|
||||
{"type": "response.output_text.delta", "delta": "Answer", "output_index": 1, "content_index": 0},
|
||||
)
|
||||
|
||||
rebuilt = litellm.stream_chunk_builder(chunks=[iterator.chunk_parser(event) for event in events])
|
||||
|
||||
assert [(choice.index, choice.message.reasoning_content, choice.message.content) for choice in rebuilt.choices] == [
|
||||
(0, "First part.Second part.", "Answer")
|
||||
]
|
||||
|
||||
|
||||
def test_chunk_parser_string_output_text_delta_produces_text():
|
||||
from litellm.completion_extras.litellm_responses_transformation.transformation import (
|
||||
OpenAiResponsesToChatCompletionStreamIterator,
|
||||
|
|
|
|||
|
|
@ -1339,6 +1339,39 @@ def test_stream_chunk_builder_rebuilds_each_choice_of_a_multi_choice_text_stream
|
|||
]
|
||||
|
||||
|
||||
def test_stream_chunk_builder_gives_a_choice_without_a_finish_chunk_the_finish_reason_its_content_implies():
|
||||
def chunk(choice_index: int, delta: Delta, finish_reason: str | None = None) -> ModelResponseStream:
|
||||
return ModelResponseStream(
|
||||
id="chatcmpl-123",
|
||||
object="chat.completion.chunk",
|
||||
created=1234567890,
|
||||
model="gpt-4.1-mini",
|
||||
choices=[StreamingChoices(index=choice_index, delta=delta, finish_reason=finish_reason)],
|
||||
)
|
||||
|
||||
def tool_call(call_id: str) -> list[ChatCompletionDeltaToolCall]:
|
||||
return [
|
||||
ChatCompletionDeltaToolCall(
|
||||
id=call_id, index=0, type="function", function=Function(name="lookup_fruit", arguments="{}")
|
||||
)
|
||||
]
|
||||
|
||||
response = stream_chunk_builder(
|
||||
chunks=[
|
||||
chunk(0, Delta(tool_calls=tool_call("call_a"))),
|
||||
chunk(1, Delta(tool_calls=tool_call("call_b"))),
|
||||
chunk(2, Delta(content="Kiwis are fuzzy.")),
|
||||
chunk(0, Delta(), finish_reason="tool_calls"),
|
||||
]
|
||||
)
|
||||
|
||||
assert [(choice.index, choice.finish_reason) for choice in response.choices] == [
|
||||
(0, "tool_calls"),
|
||||
(1, "tool_calls"),
|
||||
(2, "stop"),
|
||||
]
|
||||
|
||||
|
||||
def test_stream_chunk_builder_keeps_each_choices_function_call_annotations_and_images_on_that_choice():
|
||||
def chunk(choice_index: int, delta: Delta) -> ModelResponseStream:
|
||||
return ModelResponseStream(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue