From 0bd6575f92c1b0fe46cf5f5c76956e29e3738bba Mon Sep 17 00:00:00 2001 From: Jahanzaib Iqbal Date: Wed, 1 Jul 2026 15:57:21 +0500 Subject: [PATCH] fix: guard against None chunks in OpenAI streaming client Fixes AttributeError when the streaming response yields a None chunk before the None check is performed. Added early continue guard to skip None chunks in the streaming loop, placed before any attribute access on the chunk object. Also removes a redundant `maybe_model = chunk.model` assignment on line 949 (original) that duplicated the assignment already made earlier in the loop body. Fixes #7130 --- .../models/openai/_openai_client.py | 6 +- .../tests/models/test_openai_model_client.py | 66 +++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/python/packages/autogen-ext/src/autogen_ext/models/openai/_openai_client.py b/python/packages/autogen-ext/src/autogen_ext/models/openai/_openai_client.py index a80e912534ab..d191ad9baa67 100644 --- a/python/packages/autogen-ext/src/autogen_ext/models/openai/_openai_client.py +++ b/python/packages/autogen-ext/src/autogen_ext/models/openai/_openai_client.py @@ -906,6 +906,11 @@ async def create_stream( # Process the stream of chunks. async for chunk in chunks: + # Guard against None chunks which can be yielded by some endpoints. + # https://github.com/microsoft/autogen/issues/7130 + if chunk is None: + continue + if first_chunk: first_chunk = False # Emit the start event. @@ -946,7 +951,6 @@ async def create_stream( # for liteLLM chunk usage, do the following hack keeping the pervious chunk.stop_reason (if set). # set the stop_reason for the usage chunk to the prior stop_reason stop_reason = choice.finish_reason if chunk.usage is None and stop_reason is None else stop_reason - maybe_model = chunk.model reasoning_content: str | None = None if choice.delta.model_extra is not None and "reasoning_content" in choice.delta.model_extra: diff --git a/python/packages/autogen-ext/tests/models/test_openai_model_client.py b/python/packages/autogen-ext/tests/models/test_openai_model_client.py index ba79795d1ed7..da1418174a07 100644 --- a/python/packages/autogen-ext/tests/models/test_openai_model_client.py +++ b/python/packages/autogen-ext/tests/models/test_openai_model_client.py @@ -423,6 +423,72 @@ async def test_openai_chat_completion_client_create_stream_cancel(monkeypatch: p pass +@pytest.mark.asyncio +async def test_openai_chat_completion_client_create_stream_none_chunk(monkeypatch: pytest.MonkeyPatch) -> None: + """Regression test for https://github.com/microsoft/autogen/issues/7130. + + Ensures that None chunks yielded by some streaming endpoints do not cause + an AttributeError when the loop accesses chunk.model before a None guard. + """ + + async def _mock_create_stream_with_none_chunks( + *args: Any, **kwargs: Any + ) -> AsyncGenerator[ChatCompletionChunk | None, None]: # type: ignore[misc] + model = resolve_model(kwargs.get("model", "gpt-4.1-nano")) + # Yield a None sentinel chunk first (simulates certain streaming endpoints). + yield None # type: ignore[misc] + yield ChatCompletionChunk( + id="id", + choices=[ + ChunkChoice( + finish_reason=None, + index=0, + delta=ChoiceDelta(content="Hello", role="assistant"), + ) + ], + created=0, + model=model, + object="chat.completion.chunk", + usage=None, + ) + # Yield another None between real chunks. + yield None # type: ignore[misc] + yield ChatCompletionChunk( + id="id", + choices=[ + ChunkChoice( + finish_reason="stop", + index=0, + delta=ChoiceDelta(content=None, role="assistant"), + ) + ], + created=0, + model=model, + object="chat.completion.chunk", + usage=None, + ) + + async def _mock_create_with_none_chunks( + *args: Any, **kwargs: Any + ) -> ChatCompletion | AsyncGenerator[ChatCompletionChunk | None, None]: # type: ignore[misc] + if kwargs.get("stream", False): + return _mock_create_stream_with_none_chunks(*args, **kwargs) + return await _mock_create(*args, **kwargs) + + monkeypatch.setattr(AsyncCompletions, "create", _mock_create_with_none_chunks) + client = OpenAIChatCompletionClient(model="gpt-4o", api_key="api_key") + chunks: List[str | CreateResult] = [] + # Should not raise AttributeError when None chunks are in the stream. + async for chunk in client.create_stream( + messages=[UserMessage(content="Hello", source="user")], + ): + chunks.append(chunk) + + assert chunks[0] == "Hello" + assert isinstance(chunks[-1], CreateResult) + assert chunks[-1].content == "Hello" + + @pytest.mark.asyncio async def test_openai_chat_completion_client_count_tokens(monkeypatch: pytest.MonkeyPatch) -> None: client = OpenAIChatCompletionClient(model="gpt-4o", api_key="api_key")