Skip to content

fix(streaming): ensure remaining body is consumed after [DONE] in Str… - #3581

Open
vrs-darkness wants to merge 23 commits into
openai:mainfrom
vrs-darkness:fix/drain-stream-after-done-cleanup
Open

fix(streaming): ensure remaining body is consumed after [DONE] in Str…#3581
vrs-darkness wants to merge 23 commits into
openai:mainfrom
vrs-darkness:fix/drain-stream-after-done-cleanup

Conversation

@vrs-darkness

Copy link
Copy Markdown

fix(streaming): best-effort drain after [DONE] for connection reuse

  • I understand that this repository is auto-generated and my pull request may not be merged

Changes being requested

When using a connection-pooled OpenAI client, each streaming request was adding ~300ms of overhead. SSE streams break on data: [DONE] without fully reading the remaining response body; closing that partially-read response drops the connection from the pool, so the next request pays for a new TCP/TLS handshake.

After [DONE], drain the remaining iterator with consume_sync_iterator / consume_async_iterator before close so the body can be fully consumed and the pooled connection reused.

Drain is best-effort: httpx.HTTPError during cleanup is swallowed so [DONE] stays terminal for callers even if a proxy leaves trailing heartbeats, the socket errors, or trailing bytes are truncated/malformed.

…eam and AsyncStream

- Added best-effort draining of remaining body bytes after receiving [DONE] to allow connection reuse.
- Implemented error handling to prevent stream failures due to transport errors during draining.
- Introduced tests to validate the behavior for both synchronous and asynchronous streams.
@vrs-darkness
vrs-darkness requested a review from a team as a code owner August 5, 2026 22:22

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d4b47e7bb6

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/openai/_streaming.py Outdated
Comment thread src/openai/_streaming.py Outdated
…and AsyncStream

- Updated error handling in both Stream and AsyncStream classes to catch UnicodeError in addition to HTTPError during the draining of the stream after receiving [DONE].
- Added a new test to ensure that malformed trailing bytes after [DONE] do not cause failures in already-complete streams for both synchronous and asynchronous scenarios.
@vrs-darkness

Copy link
Copy Markdown
Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Swish!

Reviewed commit: d59c409388

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@chatgpt-codex-connector

Copy link
Copy Markdown

Security review completed. No security issues were found in this pull request.

Reviewed commit: d59c409388

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@vrs-darkness

vrs-darkness commented Aug 5, 2026

Copy link
Copy Markdown
Author

@jbeckwith-oai please review PR whenever possible. It is currently a patch in my service would be great if you could check the PR and take it for a release which i could use directly.
Thanks!!

@vrs-darkness

Copy link
Copy Markdown
Author

Hey Maintainers, friendly ping for reviewing the PR. Thanks!

@ting-hong-shieh ting-hong-shieh left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validated current head bc04f6d9 against its base 28888f9c and as a clean merge onto current main at d9029e3a (merge tree 7dc9e517). I still see one behavioral blocker and two readiness blockers.

The earlier bounded-drain concern still applies to this head. I ran the sync and async paths against the same in-process byte stream: one JSON event, [DONE], then a heartbeat held behind an explicit release event.

Snapshot Entered the trailing body Returned before release
Base 28888f9c no yes
Head bc04f6d9 yes no
Current-main merge tree 7dc9e517 yes no

Both changed snapshots return the same {"foo": true} chunk after the harness releases the body, and both close the response. Until that release, however, a completed stream waits for the trailing iterator. In a real transport this can delay completion until EOF or the read timeout; an unbounded custom transport can wait indefinitely.

The exact head's focused tests pass (26 passed), but its checked-in files do not currently pass the repository's pinned Ruff 0.14.7 gates: ruff check reports I001 for tests/test_streaming.py, and ruff format --check would reformat that file.

There is also a semantic main-branch integration failure that Git reports as a clean merge. main migrated these paths to httpx2, while the added drain blocks and tests still reference httpx. On merge tree 7dc9e517, the focused suite reports 20 passed, 6 failed with NameError: httpx is not defined; pinned Pyright 1.1.399 reports the same undefined name in both stream implementations and the new tests.

Please keep the post-[DONE] connection-reuse attempt bounded so [DONE] remains terminal for callers, and add a regression whose trailing iterator stays open. The branch also needs to be updated for the httpx2 migration and run through the repository's format/lint gates.

Evidence boundary: the behavioral snapshot uses only custom httpx byte streams and explicit synchronization events. It does not call an API, use credentials, or depend on an external server. The current-main result is from a synthetic clean merge tree, not a commit on this branch.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 73dae37905

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/openai/_streaming.py Outdated
Comment thread tests/test_streaming.py Outdated
…or bounded iterator handling

- Updated the streaming logic to use `drain_sync_iterator` and `drain_async_iterator` instead of `consume_sync_iterator` and `consume_async_iterator`.
- Enhanced comments to clarify the purpose of bounded draining after stream termination signals.
- Added new drain functions to handle a limited number of items from iterators, preventing indefinite blocking.

This change improves resource management and connection reuse after stream completion.
@vrs-darkness

Copy link
Copy Markdown
Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 114ec5741f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/openai/_streaming.py Outdated
- Modified the streaming logic to utilize `iter_bytes` and `aiter_bytes` for both synchronous and asynchronous streams.
- Enhanced comments to clarify the purpose of draining raw bytes after stream termination signals, ensuring proper resource management and preventing unbounded waits.

This change improves the handling of byte streams in the streaming classes.
@chatgpt-codex-connector

Copy link
Copy Markdown

Security review completed. No security issues were found in this pull request.

Reviewed commit: 114ec5741f

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@vrs-darkness

Copy link
Copy Markdown
Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Security review completed. No security issues were found in this pull request.

Reviewed commit: 8a8d02b20e

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@vrs-darkness

Copy link
Copy Markdown
Author

@ting-hong-shieh
Thanks for the detailed review and catching that critical issue.
All three blockers fixed:

Behavioral blocker (bounded drain):

  • Problem: Event iterator drain was ineffective on transports with heartbeat comments
  • Fix: Now drain raw byte iterator instead—byte_iterator = response.iter_bytes() (or aiter_bytes() for async), pass to
    decoder, then drain_sync_iterator(byte_iterator) after [DONE]
  • Result: Bound of 16 chunks actually terminates promptly; [DONE] is truly terminal for callers

Regression test:

  • test_drain_is_bounded_and_doesnt_block_indefinitely (sync + async variants)
  • Custom infinite iterators that yield indefinitely after [DONE]
  • Verifies stream completes and closes without blocking on unbounded transports

httpx2 migration:

  • Fixed all exception handling: httpx.HTTPError → httpx2.HTTPError, httpx.RemoteProtocolError → httpx2.RemoteProtocolError
  • Fixed all Response instantiation: httpx.Response → httpx2.Response
  • Pyright: 0 undefined names, 0 errors

CI verification:

  • ✅ ruff check / ruff format (fixed I001 import sorting)
  • ✅ pyright 0 errors
  • ✅ mypy 0 errors
  • ✅ 28 streaming tests pass

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8a8d02b20e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/openai/_streaming.py Outdated
Comment thread src/openai/_streaming.py Outdated

Copy link
Copy Markdown

Thanks for the detailed update. I revalidated exact head 8a8d02b2 against current base/main d9029e3a.

The httpx2 integration and static-readiness blockers are fixed: all 28 tests in tests/test_streaming.py pass, as do Ruff 0.14.7 lint and format checks, Pyright 1.1.399 with zero errors, and git diff --check.

Two behavioral blockers still reproduce.

First, the same sync/async held-open-body fixture emits one JSON event and [DONE], then withholds the next byte chunk behind an explicit release event:

Revision Entered the trailing body Returned before release
Base/main d9029e3a no yes
Head 8a8d02b2 yes no

After release, both paths return the same {"foo": true} chunk and close the response. The 16-item cap limits the number of completed reads, but the first next(byte_iterator) / __anext__() can still wait for the next byte or the read timeout. The current blocking-after-DONE thread describes this same remaining condition.

Second, the custom-stream hook changes behavior on the same input. A sync and async Stream subclass overrides _iter_events() to yield {"source": "override"}, while the response body contains {"source": "wire"}:

Revision _iter_events() calls Returned source
Base/main d9029e3a 1 override
Head 8a8d02b2 0 wire

The direct decoder construction therefore bypasses the subclass override in both paths. The current custom-stream thread covers this regression.

Both fresh threads are currently marked resolved on the unchanged head, but the conditions above remain reproducible. I have not added duplicate inline comments; my changes-requested review remains applicable until these two behaviors are addressed.

Evidence boundary: both snapshots use in-process httpx2 byte streams and explicit synchronization events. They do not call an API, use credentials, or depend on an external server.

- Updated `drain_sync_iterator` and `drain_async_iterator` to implement timeout-based draining of remaining items from iterators, improving connection reuse after stream termination.
- Enhanced comments to clarify the purpose of bounded draining and ensure proper resource management without indefinite blocking.

This change optimizes the handling of byte streams in both synchronous and asynchronous contexts.
- Added a newline in the `drain_sync_iterator` function to improve code readability and maintain consistency in formatting.

This change enhances the overall clarity of the code without altering functionality.
- Introduced `_byte_iterator` attributes in both `Stream` and `AsyncStream` classes to store byte iterators, enhancing the efficiency of event iteration.
- Updated `_iter_events` methods to initialize `_byte_iterator` only when necessary, optimizing resource usage.
- Adjusted `__stream__` methods to utilize the new `_iter_events` methods for better clarity and performance.

This change refines the handling of byte streams, ensuring more efficient iteration and resource management.
- Removed unnecessary try-except blocks in `drain_sync_iterator` and `drain_async_iterator` functions to simplify error handling.
- Enhanced the logic to ensure that exceptions are handled more cleanly, improving code readability and maintainability.

This change refines the iterator draining process, ensuring clearer error management while maintaining functionality.
- Simplified comments in `Stream` and `AsyncStream` classes to clarify the purpose of draining remaining bytes for connection reuse.
- Adjusted the timeout handling in `drain_async_iterator` to use the current running event loop, enhancing clarity and consistency.

This change improves the readability of the code while maintaining the functionality of the iterator draining process.
- Updated `drain_sync_iterator` and `drain_async_iterator` functions to accept optional iterators, preventing indefinite blocking when a None value is passed.
- Modified test cases to reflect changes in iterator behavior, ensuring that slow iterators trigger timeouts as expected.

This change improves the robustness of iterator draining by handling None values gracefully and ensuring proper timeout behavior during draining.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b94d8a3a2a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/openai/_utils/_streams.py Outdated
…ding

- Modified `drain_sync_iterator` to run in a background thread, preventing blocking on `iterator.__next__()`.
- Improved error handling within the draining process to ensure graceful termination on exceptions.

This change optimizes the synchronous iterator draining process, enhancing responsiveness and robustness during iteration.
- Eliminated the unused `time` import from `_streams.py` to enhance code cleanliness and maintainability.

This change contributes to a more streamlined codebase by removing unnecessary dependencies.
@vrs-darkness

Copy link
Copy Markdown
Author

Hi @ting-hong-shieh, thanks for the detailed review! Both blockers you raised have been addressed in the latest commits:

  1. Sync blocking past timeout — drain_sync_iterator now runs in a background threading.Thread with join(timeout=0.05), so
    the caller is guaranteed to return within 50ms regardless of how long next() blocks (34248fa).
  2. Custom _iter_events override bypassed — stream was updated in f27ac40 to call self._iter_events() again, so
    subclass overrides are respected on current HEAD.

Would appreciate if you could re-verify on the latest HEAD. Thanks!

- Replaced asyncio-based timeout handling in `drain_async_iterator` with anyio's `move_on_after` for improved compatibility with both asyncio and Trio.
- Streamlined the draining logic to utilize async iteration directly, enhancing code clarity and efficiency.

This change enhances the flexibility of the async iterator draining process, ensuring better performance across different async backends.

@ting-hong-shieh ting-hong-shieh left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I revalidated exact head c1dc31b1 against base/main d9029e3a.

This revision fixes the earlier custom _iter_events() regression and both async timeout cases. The asyncio held-open-read and 10 ms heartbeat snapshots finish within 200 ms, and the same [DONE] fixture now succeeds under trio.run(), returning {"foo": true} and closing both the response and body.

One behavioral blocker remains in the synchronous background-thread implementation; I left the exact reproduction inline. One static gate also remains: Ruff 0.14.7 lint passes, but ruff format --check would reformat tests/test_streaming.py by adding a blank line after each local import in the two slow iterators.

Other exact-head checks pass: all 28 tests in tests/test_streaming.py, Pyright 1.1.399 with zero errors, MyPy 1.17 across 1,540 source files, and git diff --check.

Evidence boundary: the behavioral snapshots use only in-process httpx2 byte streams and explicit synchronization. They do not call an API, use credentials, or depend on an external server.

Comment thread src/openai/_utils/_streams.py Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c1dc31b190

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/openai/_utils/_streams.py Outdated
Comment thread src/openai/_streaming.py Outdated
- Updated `drain_sync_iterator` and `drain_async_iterator` to accept a `response` parameter, allowing for graceful closure of responses if draining times out.
- Improved error handling during response closure to prevent unhandled exceptions, ensuring robust behavior in both synchronous and asynchronous contexts.

This change enhances the reliability of iterator draining by ensuring that blocked reads are properly interrupted, improving overall stream management.
…] event

- Introduced a `_done_seen` attribute in both `Stream` and `AsyncStream` classes to track the completion of the stream.
- Updated the draining logic to only execute if the [DONE] event has been seen, preventing unnecessary consumption of data on early exits.
- Enhanced comments to clarify the purpose of the changes in the draining process.

This change optimizes resource management during stream termination, ensuring that abandoned data is not consumed unnecessarily.
…ning

- Removed unnecessary line breaks in the function signatures of `drain_sync_iterator` and `drain_async_iterator` to improve code readability.
- This change enhances the clarity of the code without altering the functionality of the iterator draining process.
…draining

- Added a join operation for the background thread in `drain_sync_iterator` to ensure it exits gracefully after an interrupt.
- This change enhances the robustness of the synchronous iterator draining process by providing adequate time for thread termination, improving overall resource management.
@vrs-darkness

Copy link
Copy Markdown
Author

Hi @ting-hong-shieh, all your blockers are now addressed on the latest HEAD:

  1. Zombie thread after timeout — after response.close() interrupts the blocked next(), a second thread.join(timeout=0.05)
    is now called to ensure the thread is fully exited before returning (5055298)
  2. Drain on early cancellation — added _done_seen flag in both Stream and AsyncStream; drain only fires if [DONE] was
    actually seen, early exits skip it entirely (ea219c4)

Would appreciate a re-review on the latest HEAD. Thanks again for the thorough feedback!

@ting-hong-shieh ting-hong-shieh left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I revalidated exact head 5055298f after both worker-lifetime threads were resolved.

This revision closes the response after the first timeout and then joins the drain thread for another 50 ms. That bounds the caller’s additional wait, but it still does not guarantee worker termination. With the same held-open custom httpx2.SyncByteStream, the caller returns with response_closed_before_release=true and drain_still_blocked_after_return=true; the iterator exits only after the fixture releases it during cleanup. A custom next() that is not interrupted by response.close() can therefore still leave one daemon per completed stream.

Please keep this behavior open until a regression observes that the drain worker has terminated before fixture cleanup. Since a generic synchronous iterator has no cancellation primitive, the implementation may need to avoid starting an un-cancellable per-stream reader.

The rest of the revision is clean on this head. The async held-open and continuous-heartbeat cases, custom sync/async _iter_events() dispatch, and Trio snapshot pass. All 28 streaming tests, Ruff 0.14.7 lint and format, Pyright 1.1.399, MyPy 1.17 across 1,540 source files, and git diff --check also pass.

Evidence boundary: the snapshots use only in-process httpx2 byte streams and explicit synchronization. They do not call an API, use credentials, or depend on an external server.

@vrs-darkness

Copy link
Copy Markdown
Author

Hi @ting-hong-shieh, you're right that the thread approach can't guarantee termination for custom transports. The
alternatives don't work cleanly either — signals are main-thread only, multiprocessing is too heavy, and httpx doesn't
expose raw socket access for non-blocking checks.

The simple timeout loop alternative isn't great either — it can overshoot the deadline by the full duration of one blocking
next() call, which on a slow server could be the entire read timeout.

Given these constraints, the only honest fix for the sync path is to drop the drain entirely and revert to a plain
response.close(), accepting that sync won't get connection reuse. The async path with anyio.move_on_after is properly
bounded and unaffected.

Do you advise this approach — keeping the async drain as-is and simplifying sync back to plain response.close()?

@ting-hong-shieh

ting-hong-shieh commented Aug 14, 2026

Copy link
Copy Markdown

Yes — I’d take that trade-off.

For the sync path, close the response directly after [DONE] and remove the background drain. A guaranteed return with no orphaned worker is more important than best-effort connection reuse when a custom iterator can block indefinitely.

I would keep the async drain, with one wording caveat: move_on_after bounds cancellation-cooperative async streams; it cannot guarantee termination for an arbitrary custom iterator that blocks the event loop or suppresses cancellation.

Please split the current parameterized expectations accordingly:

  • sync: do not require the trailing body to be exhausted; verify that the result returns promptly, the response is closed, and no drain thread is left behind;
  • async: retain the successful trailing-drain/reuse case and the cooperative timeout/close case;
  • both: retain the post-[DONE] error-preservation coverage.

That shape addresses the exact 5055298f reproduction without claiming a guarantee the transport abstraction cannot provide. Ping me after the update and I’ll rerun the snapshots and suite against the new exact head.

…ponse handling

- Removed the `_done_seen` attribute from `Stream` and `AsyncStream` classes, streamlining the logic for handling the [DONE] event during iteration.
- Updated the draining process to always close the response after the stream ends, ensuring proper resource management and connection reuse.
- Adjusted comments for clarity regarding the purpose of draining remaining bytes.

This change improves the efficiency and reliability of the streaming process by simplifying the code and enhancing response closure behavior.
@vrs-darkness

Copy link
Copy Markdown
Author

Hi @ting-hong-shieh, updated on the latest HEAD 58d222d:

  • Sync path: removed background drain entirely, reverted to plain response.close() — no orphaned workers possible
  • Async path: kept anyio.move_on_after drain, updated docstring to clarify it bounds cancellation-cooperative streams only
    and cannot guarantee termination for custom iterators that block the event loop or suppress cancellation
  • Tests: split sync/async — sync only asserts result correct + response closed, async retains the full drain/exhaustion
    assertion

Ready for your snapshots. Thanks!

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 58d222d89b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/openai/_streaming.py Outdated
- Removed the `_byte_iterator` attribute from the `Stream` class, streamlining the iteration logic.
- Updated the `_iter_events` method to directly yield from the response's byte iterator, enhancing code clarity.
- Added a comment to ensure the response is closed properly, improving resource management.

This change simplifies the streaming process by reducing unnecessary state management and ensuring proper closure of resources.

@ting-hong-shieh ting-hong-shieh left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I revalidated exact head 990ed6fb against base/main d9029e3a.

The synchronous change fixes the worker-lifetime blocker. With the same held-open SyncByteStream, 5055298f entered the trailing body and returned with one live Thread-2 (_drain); 990ed6fb does not enter the trailing body, returns with no new live thread, and closes both the response and body.

One regression remains in the async path. This revision removes _done_seen from AsyncStream as well as Stream, so its finally block drains even when the caller exits before [DONE]. The same in-process early-exit fixture produces:

Revision Entered trailing body Cancelled trailing read Close time
5055298f no no 0.0 ms
990ed6fb yes yes 52.3 ms

Both revisions return the same first event and close the response. The difference is that the new head resumes and consumes the abandoned body until move_on_after cancels it. Please retain _done_seen for AsyncStream and guard only the async drain with it; the sync path should remain a direct response.close().

The now-resolved bot suggestion to restore sync draining should not cause the background reader to return. Losing sync connection reuse is the deliberate trade-off that removes an un-cancellable per-stream worker.

All other exact-head checks pass: 28 streaming tests, Ruff 0.14.7 lint and format, Pyright 1.1.399 with zero errors, MyPy 1.17 across 1,540 source files, git diff --check, custom sync/async _iter_events() dispatch, held-open and heartbeat async deadlines, and the Trio cleanup snapshot.

Evidence boundary: the snapshots use only in-process httpx2 byte streams and explicit synchronization. They do not call an API, use credentials, or depend on an external server.

…ream

- Introduced a `_done_seen` attribute in the `AsyncStream` class to track whether the [DONE] event has been encountered during streaming.
- Updated the draining logic to only execute if `_done_seen` is true, preventing unnecessary consumption of trailing data when exiting early.
- Added a new test to verify that early exits without seeing [DONE] do not trigger the drain function, ensuring efficient resource management.

This change enhances the reliability of the streaming process by ensuring that only relevant data is consumed during termination.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants