Skip to content

Commit 0242b66

Browse files
authored
Fix streaming gpt-oss using Ollama (#3035)
1 parent dc9d081 commit 0242b66

File tree

4 files changed

+31
-4
lines changed

4 files changed

+31
-4
lines changed

pydantic_ai_slim/pydantic_ai/models/openai.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -526,16 +526,20 @@ def _process_response(self, response: chat.ChatCompletion | str) -> ModelRespons
526526

527527
choice = response.choices[0]
528528
items: list[ModelResponsePart] = []
529+
529530
# The `reasoning_content` field is only present in DeepSeek models.
530531
# https://api-docs.deepseek.com/guides/reasoning_model
531532
if reasoning_content := getattr(choice.message, 'reasoning_content', None):
532533
items.append(ThinkingPart(id='reasoning_content', content=reasoning_content, provider_name=self.system))
533534

534-
# NOTE: We don't currently handle OpenRouter `reasoning_details`:
535-
# - https://openrouter.ai/docs/use-cases/reasoning-tokens#preserving-reasoning-blocks
536-
# NOTE: We don't currently handle OpenRouter/gpt-oss `reasoning`:
535+
# The `reasoning` field is only present in gpt-oss via Ollama and OpenRouter.
537536
# - https://cookbook.openai.com/articles/gpt-oss/handle-raw-cot#chat-completions-api
538537
# - https://openrouter.ai/docs/use-cases/reasoning-tokens#basic-usage-with-reasoning-tokens
538+
if reasoning := getattr(choice.message, 'reasoning', None):
539+
items.append(ThinkingPart(id='reasoning', content=reasoning, provider_name=self.system))
540+
541+
# NOTE: We don't currently handle OpenRouter `reasoning_details`:
542+
# - https://openrouter.ai/docs/use-cases/reasoning-tokens#preserving-reasoning-blocks
539543
# If you need this, please file an issue.
540544

541545
vendor_details: dict[str, Any] = {}
@@ -1492,6 +1496,17 @@ async def _get_event_iterator(self) -> AsyncIterator[ModelResponseStreamEvent]:
14921496
provider_name=self.provider_name,
14931497
)
14941498

1499+
# The `reasoning` field is only present in gpt-oss via Ollama and OpenRouter.
1500+
# - https://cookbook.openai.com/articles/gpt-oss/handle-raw-cot#chat-completions-api
1501+
# - https://openrouter.ai/docs/use-cases/reasoning-tokens#basic-usage-with-reasoning-tokens
1502+
if reasoning := getattr(choice.delta, 'reasoning', None): # pragma: no cover
1503+
yield self._parts_manager.handle_thinking_delta(
1504+
vendor_part_id='reasoning',
1505+
id='reasoning',
1506+
content=reasoning,
1507+
provider_name=self.provider_name,
1508+
)
1509+
14951510
for dtc in choice.delta.tool_calls or []:
14961511
maybe_event = self._parts_manager.handle_tool_call_delta(
14971512
vendor_part_id=dtc.index,

pydantic_ai_slim/pydantic_ai/profiles/harmony.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,6 @@ def harmony_model_profile(model_name: str) -> ModelProfile | None:
1010
See <https://cookbook.openai.com/articles/openai-harmony> for more details.
1111
"""
1212
profile = openai_model_profile(model_name)
13-
return OpenAIModelProfile(openai_supports_tool_choice_required=False).update(profile)
13+
return OpenAIModelProfile(
14+
openai_supports_tool_choice_required=False, ignore_streamed_leading_whitespace=True
15+
).update(profile)

pydantic_ai_slim/pydantic_ai/providers/ollama.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from pydantic_ai.profiles.cohere import cohere_model_profile
1212
from pydantic_ai.profiles.deepseek import deepseek_model_profile
1313
from pydantic_ai.profiles.google import google_model_profile
14+
from pydantic_ai.profiles.harmony import harmony_model_profile
1415
from pydantic_ai.profiles.meta import meta_model_profile
1516
from pydantic_ai.profiles.mistral import mistral_model_profile
1617
from pydantic_ai.profiles.openai import OpenAIJsonSchemaTransformer, OpenAIModelProfile
@@ -50,6 +51,7 @@ def model_profile(self, model_name: str) -> ModelProfile | None:
5051
'deepseek': deepseek_model_profile,
5152
'mistral': mistral_model_profile,
5253
'command': cohere_model_profile,
54+
'gpt-oss': harmony_model_profile,
5355
}
5456

5557
profile = None

tests/providers/test_ollama.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from pydantic_ai.profiles.cohere import cohere_model_profile
1010
from pydantic_ai.profiles.deepseek import deepseek_model_profile
1111
from pydantic_ai.profiles.google import GoogleJsonSchemaTransformer, google_model_profile
12+
from pydantic_ai.profiles.harmony import harmony_model_profile
1213
from pydantic_ai.profiles.meta import meta_model_profile
1314
from pydantic_ai.profiles.mistral import mistral_model_profile
1415
from pydantic_ai.profiles.openai import OpenAIJsonSchemaTransformer
@@ -77,6 +78,7 @@ def test_ollama_provider_model_profile(mocker: MockerFixture):
7778
mistral_model_profile_mock = mocker.patch(f'{ns}.mistral_model_profile', wraps=mistral_model_profile)
7879
qwen_model_profile_mock = mocker.patch(f'{ns}.qwen_model_profile', wraps=qwen_model_profile)
7980
cohere_model_profile_mock = mocker.patch(f'{ns}.cohere_model_profile', wraps=cohere_model_profile)
81+
harmony_model_profile_mock = mocker.patch(f'{ns}.harmony_model_profile', wraps=harmony_model_profile)
8082

8183
meta_profile = provider.model_profile('llama3.2')
8284
meta_model_profile_mock.assert_called_with('llama3.2')
@@ -115,6 +117,12 @@ def test_ollama_provider_model_profile(mocker: MockerFixture):
115117
assert cohere_profile is not None
116118
assert cohere_profile.json_schema_transformer == OpenAIJsonSchemaTransformer
117119

120+
harmony_profile = provider.model_profile('gpt-oss')
121+
harmony_model_profile_mock.assert_called_with('gpt-oss')
122+
assert harmony_profile is not None
123+
assert harmony_profile.json_schema_transformer == OpenAIJsonSchemaTransformer
124+
assert harmony_profile.ignore_streamed_leading_whitespace is True
125+
118126
unknown_profile = provider.model_profile('unknown-model')
119127
assert unknown_profile is not None
120128
assert unknown_profile.json_schema_transformer == OpenAIJsonSchemaTransformer

0 commit comments

Comments
 (0)