Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Changelog

## [Unreleased]
### Fixed
- `stream_batch` now materializes each chunk's content into the shared response proto before yielding, so every response (not just the first) shows accumulated content when read during streaming or in any read order.

## [v1.19.0](https://github.com/xai-org/xai-sdk-python/releases/tag/v1.19.0) - 2026-08-18
### Added
Expand Down
4 changes: 4 additions & 0 deletions src/xai_sdk/aio/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,10 @@ async def stream_batch(self, n: int) -> AsyncIterator[tuple[Sequence[Response],
first_chunk_received = True

responses[0].process_chunk(chunk)
# All chunk deltas for every output index accumulate in the first
# response's buffers; materialize them into the shared proto so
# every response sees current content regardless of read order.
responses[0]._sync_buffers_to_proto()
yield responses, [Chunk(chunk, i) for i in range(n)]

span.set_attributes(self._make_span_response_attributes(responses))
Expand Down
4 changes: 4 additions & 0 deletions src/xai_sdk/sync/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,10 @@ def stream_batch(self, n: int) -> Iterator[tuple[Sequence[Response], Sequence[Ch
first_chunk_received = True

responses[0].process_chunk(chunk)
# All chunk deltas for every output index accumulate in the first
# response's buffers; materialize them into the shared proto so
# every response sees current content regardless of read order.
responses[0]._sync_buffers_to_proto()
yield responses, [Chunk(chunk, i) for i in range(n)]

span.set_attributes(self._make_span_response_attributes(responses))
Expand Down
21 changes: 21 additions & 0 deletions tests/aio/chat_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,27 @@ async def test_streaming_batch(client):
assert last_response[1].content == "Hello, this is a test response!"


@pytest.mark.asyncio(loop_scope="session")
async def test_streaming_batch_each_response_populated_during_stream(client):
"""Reading a later response's content during the stream must show accumulated content.

Regression: stream_batch accumulated all deltas in the first response's
buffers and only materialized them into the shared proto when response[0]
was read (or the telemetry span closed). A caller streaming per-option
content from response[1] alone got an empty string for the whole stream.
"""
chat = client.chat.create("grok-3-latest")
chat.append(user("test message"))
stream = chat.stream_batch(2)

observed = []
async for responses, _ in stream:
observed.append(responses[1].content)

assert observed
assert observed[-1] == "Hello, this is a test response!"


@pytest.mark.asyncio(loop_scope="session")
async def test_deferred(client):
chat = client.chat.create("grok-3-latest")
Expand Down
20 changes: 20 additions & 0 deletions tests/sync/chat_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,26 @@ def test_streaming_batch(client: Client):
assert last_response[1].content == "Hello, this is a test response!"


def test_streaming_batch_each_response_populated_during_stream(client: Client):
"""Reading a later response's content during the stream must show accumulated content.

Regression: stream_batch accumulated all deltas in the first response's
buffers and only materialized them into the shared proto when response[0]
was read (or the telemetry span closed). A caller streaming per-option
content from response[1] alone got an empty string for the whole stream.
"""
chat = client.chat.create("grok-3-latest")
chat.append(user("test message"))
stream = chat.stream_batch(2)

observed = []
for responses, _ in stream:
observed.append(responses[1].content)

assert observed
assert observed[-1] == "Hello, this is a test response!"


def test_function_calling(client: Client):
chat = client.chat.create(
"grok-3-latest",
Expand Down