mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-07 08:26:10 +00:00
Merge 944d230f43 into 1df25e26cf
This commit is contained in:
commit
96bc2421cd
2 changed files with 111 additions and 0 deletions
|
|
@ -8648,6 +8648,7 @@ def stream_chunk_builder(
|
|||
or delta.get("audio") is not None
|
||||
or delta.get("images") is not None
|
||||
or delta.get("provider_specific_fields") is not None
|
||||
or delta.get("reasoning_items") is not None
|
||||
):
|
||||
is_simple_text_stream = False
|
||||
break
|
||||
|
|
@ -8800,6 +8801,22 @@ def stream_chunk_builder(
|
|||
all_images.extend(chunk["choices"][0]["delta"]["images"])
|
||||
response["choices"][0]["message"]["images"] = all_images
|
||||
|
||||
# Reasoning items carry the provider's encrypted reasoning state, which the
|
||||
# Responses API bridge reads back off the assembled assistant message.
|
||||
reasoning_item_chunks: Final = [ # mutable-ok: local filter over chunks, never escapes this function
|
||||
chunk
|
||||
for chunk in chunks
|
||||
if len(chunk["choices"]) > 0
|
||||
and "reasoning_items" in chunk["choices"][0]["delta"]
|
||||
and chunk["choices"][0]["delta"]["reasoning_items"] is not None
|
||||
]
|
||||
|
||||
if len(reasoning_item_chunks) > 0:
|
||||
all_reasoning_items: Final[list] = [] # mutable-ok: accumulator, extended in the loop below
|
||||
for chunk in reasoning_item_chunks:
|
||||
all_reasoning_items.extend(chunk["choices"][0]["delta"]["reasoning_items"])
|
||||
response["choices"][0]["message"]["reasoning_items"] = all_reasoning_items
|
||||
|
||||
# Combine provider_specific_fields from streaming chunks (e.g., web_search_results, citations)
|
||||
# See: https://github.com/BerriAI/litellm/issues/17737
|
||||
provider_specific_chunks: Final = [
|
||||
|
|
|
|||
|
|
@ -0,0 +1,94 @@
|
|||
"""
|
||||
Tests for stream_chunk_builder reasoning item reassembly.
|
||||
|
||||
Previously, stream_chunk_builder dropped delta.reasoning_items entirely: the
|
||||
simple-text fast path returned before any aggregation ran, and the full path
|
||||
had no reasoning_items branch. The encrypted reasoning state a provider sends
|
||||
back was therefore lost from the assembled message, which is what gets cached
|
||||
and what the Responses API bridge reads to rebuild its input.
|
||||
"""
|
||||
|
||||
from litellm import stream_chunk_builder
|
||||
from litellm.types.utils import Delta, ModelResponseStream, StreamingChoices
|
||||
|
||||
MESSAGES = [{"role": "user", "content": "hi"}]
|
||||
|
||||
|
||||
def _chunk(**delta_kwargs) -> ModelResponseStream:
|
||||
return ModelResponseStream(
|
||||
id="chatcmpl-test",
|
||||
created=1700000000,
|
||||
model="test-model",
|
||||
object="chat.completion.chunk",
|
||||
choices=[
|
||||
StreamingChoices(
|
||||
finish_reason=None,
|
||||
index=0,
|
||||
delta=Delta(**delta_kwargs),
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def test_reasoning_items_survive_a_text_only_stream():
|
||||
"""
|
||||
A stream carrying only content and reasoning_items must keep the reasoning
|
||||
items. This is the fast path, which used to return before aggregating.
|
||||
"""
|
||||
reasoning_item = {
|
||||
"id": "rs_abc123",
|
||||
"type": "reasoning",
|
||||
"encrypted_content": "ENCRYPTED-REASONING-BLOB",
|
||||
"summary": [{"type": "summary_text", "text": "Thinking"}],
|
||||
}
|
||||
|
||||
chunks = [
|
||||
_chunk(content="Hello", role="assistant"),
|
||||
_chunk(content="", reasoning_items=[reasoning_item]),
|
||||
]
|
||||
|
||||
response = stream_chunk_builder(chunks, messages=MESSAGES)
|
||||
|
||||
assert response is not None
|
||||
message = response.choices[0].message
|
||||
assert message.content == "Hello"
|
||||
assert getattr(message, "reasoning_items", None) == [reasoning_item]
|
||||
|
||||
|
||||
def test_reasoning_items_are_merged_across_chunks():
|
||||
"""
|
||||
Reasoning items arriving in more than one chunk are concatenated, matching
|
||||
how annotations and images are merged. thinking_blocks here also forces the
|
||||
full aggregation path rather than the text-only fast path.
|
||||
"""
|
||||
item_a = {"id": "rs_a", "type": "reasoning", "encrypted_content": "BLOB-A"}
|
||||
item_b = {"id": "rs_b", "type": "reasoning", "encrypted_content": "BLOB-B"}
|
||||
|
||||
chunks = [
|
||||
_chunk(
|
||||
content="Part one. ",
|
||||
role="assistant",
|
||||
thinking_blocks=[{"type": "thinking", "thinking": "step 1", "signature": "sig"}],
|
||||
reasoning_items=[item_a],
|
||||
),
|
||||
_chunk(content="Part two.", reasoning_items=[item_b]),
|
||||
]
|
||||
|
||||
response = stream_chunk_builder(chunks, messages=MESSAGES)
|
||||
|
||||
assert response is not None
|
||||
message = response.choices[0].message
|
||||
assert message.content == "Part one. Part two."
|
||||
assert getattr(message, "reasoning_items", None) == [item_a, item_b]
|
||||
|
||||
|
||||
def test_no_reasoning_items_leaves_the_message_alone():
|
||||
"""A stream without reasoning items must not grow a reasoning_items field."""
|
||||
chunks = [_chunk(content="Hello", role="assistant"), _chunk(content=" there")]
|
||||
|
||||
response = stream_chunk_builder(chunks, messages=MESSAGES)
|
||||
|
||||
assert response is not None
|
||||
message = response.choices[0].message
|
||||
assert message.content == "Hello there"
|
||||
assert getattr(message, "reasoning_items", None) is None
|
||||
Loading…
Add table
Reference in a new issue