From 54884ed2d52ec1d50578e60d5e3b6b723b9dd417 Mon Sep 17 00:00:00 2001 From: Arthur Landim Costa Date: Tue, 10 Feb 2026 12:51:24 -0300 Subject: [PATCH 1/2] fix: add aclose() to AsyncStream for standard async cleanup `AsyncStream` exposes `close()` but not `aclose()`, which is the standard Python async cleanup method name (used by contextlib, asyncio, and the language spec for async generators). This causes `AttributeError` when callers use the conventional `aclose()` pattern. Two concrete callers in this repo are affected: - `AsyncChatCompletionStream.close()` stores `raw_stream.response` in `self._response` and calls `self._response.aclose()`. When instrumentation libraries (e.g. Langfuse) wrap the raw stream, the `.response` attribute can resolve to the `AsyncStream` itself rather than the underlying `httpx.Response`, hitting the missing method. - Third-party instrumentation (Langfuse `LangfuseResponseGeneratorAsync`) calls `.aclose()` on the response generator which delegates to the wrapped `AsyncStream`. The fix adds `aclose()` as a thin async alias for `close()`, matching the pattern already used by `httpx.Response`, `asyncio.StreamWriter`, and Python async generators. --- src/openai/_streaming.py | 9 +++++++++ tests/test_streaming.py | 26 ++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/src/openai/_streaming.py b/src/openai/_streaming.py index 61a742668a..a7d354e54a 100644 --- a/src/openai/_streaming.py +++ b/src/openai/_streaming.py @@ -223,6 +223,15 @@ async def close(self) -> None: """ await self.response.aclose() + async def aclose(self) -> None: + """Alias for :meth:`close` following the Python async convention. + + Callers such as ``AsyncChatCompletionStream`` and third-party + instrumentation libraries (e.g. Langfuse) use ``aclose()`` as the + standard async cleanup method. + """ + await self.close() + class ServerSentEvent: def __init__( diff --git a/tests/test_streaming.py b/tests/test_streaming.py index 04f8e51abd..5257a32236 100644 --- a/tests/test_streaming.py +++ b/tests/test_streaming.py @@ -216,6 +216,32 @@ def body() -> Iterator[bytes]: assert sse.json() == {"content": "известни"} +@pytest.mark.asyncio +async def test_async_stream_aclose(async_client: AsyncOpenAI) -> None: + """AsyncStream should support aclose() as an alias for close(). + + This is the standard Python async cleanup method name (used by contextlib, + asyncio, and the language spec for async generators). Callers such as + ``AsyncChatCompletionStream.close()`` and Langfuse's + ``LangfuseResponseGeneratorAsync`` invoke ``aclose()`` on the underlying + stream, so its absence causes ``AttributeError`` at cleanup time. + """ + + def body() -> Iterator[bytes]: + yield b"data: [DONE]\n\n" + + stream = AsyncStream( + cast_to=object, + client=async_client, + response=httpx.Response(200, content=to_aiter(body())), + ) + + assert hasattr(stream, "aclose"), "AsyncStream must expose aclose()" + + # aclose() should behave identically to close() + await stream.aclose() + + async def to_aiter(iter: Iterator[bytes]) -> AsyncIterator[bytes]: for chunk in iter: yield chunk From 9e552b381a2865c8abac02921d3515071b928717 Mon Sep 17 00:00:00 2001 From: Arthur Landim Costa Date: Tue, 10 Feb 2026 16:57:29 -0300 Subject: [PATCH 2/2] test: verify aclose() delegates to close() Replace the smoke test with a mock-based assertion that aclose() actually calls close(), validating the behavioral contract. Co-Authored-By: Claude Opus 4.6 --- tests/test_streaming.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/test_streaming.py b/tests/test_streaming.py index 5257a32236..4a566fd5a1 100644 --- a/tests/test_streaming.py +++ b/tests/test_streaming.py @@ -1,6 +1,7 @@ from __future__ import annotations from typing import Iterator, AsyncIterator +from unittest import mock import httpx import pytest @@ -238,8 +239,10 @@ def body() -> Iterator[bytes]: assert hasattr(stream, "aclose"), "AsyncStream must expose aclose()" - # aclose() should behave identically to close() - await stream.aclose() + # aclose() should delegate to close() + with mock.patch.object(stream, "close", wraps=stream.close) as mock_close: + await stream.aclose() + mock_close.assert_called_once() async def to_aiter(iter: Iterator[bytes]) -> AsyncIterator[bytes]: