This commit is contained in:
Shubham Saha 2026-10-28 20:50:36 +08:00 • committed by GitHub
commit 355497e65c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 47 additions and 4 deletions

View file

@ -1439,16 +1439,17 @@ def _field_of(value: object, name: str) -> object:
return getattr(value, name, None)
def _delta_reasoning_items(delta: object) -> Sequence[object]:
items: Final = _field_of(delta, "reasoning_items")
def _delta_reasoning_items(delta: object, field: str = "reasoning_items") -> Sequence[object]:
items: Final = _field_of(delta, field)
return cast(Sequence[object], items) if isinstance(items, list) else () # cast-ok: items forwarded verbatim
def _chunk_reasoning_items(chunk: object) -> Sequence[object]:
def _chunk_reasoning_items(chunk: object, field: str = "reasoning_items") -> Sequence[object]:
choices: Final = _field_of(chunk, "choices")
if not isinstance(choices, list) or not choices:
return ()
return _delta_reasoning_items(_field_of(cast(Sequence[object], choices)[0], "delta")) # cast-ok: untyped chunk
delta: Final = _field_of(cast(Sequence[object], choices)[0], "delta") # cast-ok: untyped chunk
return _delta_reasoning_items(delta, field)
_NON_TEXT_DELTA_FIELDS: Final = (
@ -1456,6 +1457,7 @@ _NON_TEXT_DELTA_FIELDS: Final = (
"function_call",
"reasoning_content",
"reasoning_items",
"reasoning_details",
"thinking_blocks",
"annotations",
"audio",
@ -1760,6 +1762,19 @@ def stream_chunk_builder(
_choice = response.choices[0]
_choice.message.provider_specific_fields = combined_provider_fields
# Keep every streamed reasoning_details entry in arrival order, as the
# non-streaming response does. Do not merge by index: some providers
# send whole entries that all have index 0.
reasoning_details: Final = tuple(
chain.from_iterable(_chunk_reasoning_items(chunk, "reasoning_details") for chunk in typed_chunks)
)
if reasoning_details:
message: Final = response.choices[0].message
message.provider_specific_fields = {
**(message.provider_specific_fields or {}),
"reasoning_details": list(reasoning_details),
}
completion_output = get_content_from_model_response(response)
reasoning_tokens: Final = processor.count_reasoning_tokens(response)

View file

@ -1103,6 +1103,34 @@ def test_stream_chunk_builder_tolerates_trailing_chunk_without_choices():
assert response.choices[0].message.content == "Hello world"
def test_stream_chunk_builder_keeps_reasoning_details_from_every_chunk():
"""A streamed `reasoning_details` list must rebuild into the same
`provider_specific_fields["reasoning_details"]` that a non-streaming
response carries: every entry, in arrival order, not merged by `index`
(Vercel AI Gateway sends whole entries that all have `index: 0`)."""
text_entry: Final = {"type": "reasoning.text", "text": "step one", "index": 0}
encrypted_entry: Final = {"type": "reasoning.encrypted", "data": "ENC-TWO", "index": 0}
chunks: Final = [
ModelResponseStream(
model="m",
choices=[
StreamingChoices(index=0, delta=Delta(role="assistant", content="Hi", reasoning_details=[text_entry]))
],
),
ModelResponseStream(
model="m", choices=[StreamingChoices(index=0, delta=Delta(reasoning_details=[encrypted_entry]))]
),
ModelResponseStream(model="m", choices=[StreamingChoices(index=0, delta=Delta(content=" there"))]),
]
response = stream_chunk_builder(chunks=chunks)
assert response is not None
message = response.choices[0].message
assert message.content == "Hi there"
assert message.provider_specific_fields == {"reasoning_details": [text_entry, encrypted_entry]}
def test_anthropic_speed_and_geo_survive_stream_assembly():
"""Anthropic prices fast mode and non-global regions with a multiplier read off
``usage.speed`` / ``usage.inference_geo``. Dropping them while reassembling a stream