From a4ba0461c7a440bf6df00832c822ac88636d7b4f Mon Sep 17 00:00:00 2001 From: frednijsvrt <109167337+frednijsvrt@users.noreply.github.com> Date: Tue, 2 Sep 2025 13:28:54 +0100 Subject: [PATCH 1/4] Fix bug related to Azure OpenAI model streaming response --- pydantic_ai_slim/pydantic_ai/models/openai.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydantic_ai_slim/pydantic_ai/models/openai.py b/pydantic_ai_slim/pydantic_ai/models/openai.py index 7ff5b2f7f9..550c311d68 100644 --- a/pydantic_ai_slim/pydantic_ai/models/openai.py +++ b/pydantic_ai_slim/pydantic_ai/models/openai.py @@ -1173,7 +1173,7 @@ async def _get_event_iterator(self) -> AsyncIterator[ModelResponseStreamEvent]: # Handle the text part of the response content = choice.delta.content - if content is not None: + if (delta := choice.delta) is not None and (content := delta.content) is not None: maybe_event = self._parts_manager.handle_text_delta( vendor_part_id='content', content=content, From f5bc8e956b9e2d288aac7f3a569f03905eb448b2 Mon Sep 17 00:00:00 2001 From: frednijs Date: Wed, 3 Sep 2025 10:04:50 +0200 Subject: [PATCH 2/4] apply requested change --- pydantic_ai_slim/pydantic_ai/models/openai.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pydantic_ai_slim/pydantic_ai/models/openai.py b/pydantic_ai_slim/pydantic_ai/models/openai.py index 09225941c5..653412a38a 100644 --- a/pydantic_ai_slim/pydantic_ai/models/openai.py +++ b/pydantic_ai_slim/pydantic_ai/models/openai.py @@ -1172,8 +1172,7 @@ async def _get_event_iterator(self) -> AsyncIterator[ModelResponseStreamEvent]: continue # Handle the text part of the response - content = choice.delta.content - if (delta := choice.delta) is not None and (content := delta.content) is not None: + if (delta := choice.delta) is not None and (content := delta.content) is not None: # pyright: ignore[reportUnnecessaryComparison] maybe_event = self._parts_manager.handle_text_delta( vendor_part_id='content', content=content, From 74215450dfe54873437cf7beebf444753a96bd0f Mon Sep 17 00:00:00 2001 From: frednijs Date: Wed, 3 Sep 2025 10:54:18 +0200 Subject: [PATCH 3/4] continue to next chunk on None delta altogether + add test --- pydantic_ai_slim/pydantic_ai/models/openai.py | 6 +++- tests/models/test_openai.py | 31 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/pydantic_ai_slim/pydantic_ai/models/openai.py b/pydantic_ai_slim/pydantic_ai/models/openai.py index 653412a38a..c056bc0dbd 100644 --- a/pydantic_ai_slim/pydantic_ai/models/openai.py +++ b/pydantic_ai_slim/pydantic_ai/models/openai.py @@ -1171,8 +1171,12 @@ async def _get_event_iterator(self) -> AsyncIterator[ModelResponseStreamEvent]: except IndexError: continue + if choice.delta is None: # pyright: ignore[reportUnnecessaryComparison] + continue + # Handle the text part of the response - if (delta := choice.delta) is not None and (content := delta.content) is not None: # pyright: ignore[reportUnnecessaryComparison] + content = choice.delta.content + if content is not None: maybe_event = self._parts_manager.handle_text_delta( vendor_part_id='content', content=content, diff --git a/tests/models/test_openai.py b/tests/models/test_openai.py index 804f4d5614..9a40f09d3a 100644 --- a/tests/models/test_openai.py +++ b/tests/models/test_openai.py @@ -603,6 +603,37 @@ async def test_no_delta(allow_model_requests: None): assert result.usage() == snapshot(RunUsage(requests=1, input_tokens=6, output_tokens=3)) +def none_delta_chunk(finish_reason: FinishReason | None = None) -> chat.ChatCompletionChunk: + choice = ChunkChoice(index=0, delta=ChoiceDelta()) + # When using Azure OpenAI and an async content filter is enabled, the openai SDK can return None deltas. + choice.delta = None # pyright: ignore[reportAttributeAccessIssue] + return chat.ChatCompletionChunk( + id='x', + choices=[choice], + created=1704067200, # 2024-01-01 + model='gpt-4o', + object='chat.completion.chunk', + usage=CompletionUsage(completion_tokens=1, prompt_tokens=2, total_tokens=3), + ) + + +async def test_none_delta(allow_model_requests: None): + stream = [ + none_delta_chunk(), + text_chunk('hello '), + text_chunk('world'), + ] + mock_client = MockOpenAI.create_mock_stream(stream) + m = OpenAIChatModel('gpt-4o', provider=OpenAIProvider(openai_client=mock_client)) + agent = Agent(m) + + async with agent.run_stream('') as result: + assert not result.is_complete + assert [c async for c in result.stream_text(debounce_by=None)] == snapshot(['hello ', 'hello world']) + assert result.is_complete + assert result.usage() == snapshot(RunUsage(requests=1, input_tokens=6, output_tokens=3)) + + @pytest.mark.filterwarnings('ignore:Set the `system_prompt_role` in the `OpenAIModelProfile` instead.') @pytest.mark.parametrize('system_prompt_role', ['system', 'developer', 'user', None]) async def test_system_prompt_role( From e426bd9e0864dd67b40aeacf1095af1367f58f03 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 3 Sep 2025 08:32:14 -0600 Subject: [PATCH 4/4] Update pydantic_ai_slim/pydantic_ai/models/openai.py --- pydantic_ai_slim/pydantic_ai/models/openai.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pydantic_ai_slim/pydantic_ai/models/openai.py b/pydantic_ai_slim/pydantic_ai/models/openai.py index c056bc0dbd..48c2bfb4be 100644 --- a/pydantic_ai_slim/pydantic_ai/models/openai.py +++ b/pydantic_ai_slim/pydantic_ai/models/openai.py @@ -1171,6 +1171,7 @@ async def _get_event_iterator(self) -> AsyncIterator[ModelResponseStreamEvent]: except IndexError: continue + # When using Azure OpenAI and an async content filter is enabled, the openai SDK can return None deltas. if choice.delta is None: # pyright: ignore[reportUnnecessaryComparison] continue