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..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 @@ -216,6 +217,34 @@ 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 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]: for chunk in iter: yield chunk