From b45ab30fe7685c1bb8e4922ed06487489d1f56cb Mon Sep 17 00:00:00 2001 From: latent-9 <296084221+latent-9@users.noreply.github.com> Date: Sat, 22 Aug 2026 13:29:08 +1200 Subject: [PATCH] fix(chat): expose stream_batch content for every response stream_batch(n) wraps all n options in Response objects sharing one proto, but only responses[0].process_chunk is called for each chunk. Content is buffered per output index in the first response and only materialized into the shared proto when responses[0].content is read or the telemetry span closes. Reading responses[i].content for i > 0 during streaming returns an empty string for the whole stream. Materialize the first response's buffers into the shared proto after each chunk so every response reflects streamed content regardless of read order. --- CHANGELOG.md | 2 ++ src/xai_sdk/aio/chat.py | 4 ++++ src/xai_sdk/sync/chat.py | 4 ++++ tests/aio/chat_test.py | 21 +++++++++++++++++++++ tests/sync/chat_test.py | 20 ++++++++++++++++++++ 5 files changed, 51 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b427441..1f11543 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/xai_sdk/aio/chat.py b/src/xai_sdk/aio/chat.py index c4a7f78..6334930 100644 --- a/src/xai_sdk/aio/chat.py +++ b/src/xai_sdk/aio/chat.py @@ -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)) diff --git a/src/xai_sdk/sync/chat.py b/src/xai_sdk/sync/chat.py index d758525..e032d5c 100644 --- a/src/xai_sdk/sync/chat.py +++ b/src/xai_sdk/sync/chat.py @@ -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)) diff --git a/tests/aio/chat_test.py b/tests/aio/chat_test.py index e5acaf5..1d8dd2f 100644 --- a/tests/aio/chat_test.py +++ b/tests/aio/chat_test.py @@ -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") diff --git a/tests/sync/chat_test.py b/tests/sync/chat_test.py index c2e24d2..1075408 100644 --- a/tests/sync/chat_test.py +++ b/tests/sync/chat_test.py @@ -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",