`gpt-oss` resolves `supports_thinking=False` on heroku, nebius, ollama and ovhcloud — `harmony_model_profile` delegates reasoning to a table that excludes gpt-oss
Initial Checks
- I'm using the latest version of Pydantic AI
- I've searched for my issue in the issue tracker before opening this issue
Description
Four of the five providers that route gpt-oss through harmony_model_profile resolve supports_thinking=False, so ModelSettings(thinking=...) is silently discarded on them:
| provider | model ID served | supports_thinking |
thinking_always_enabled |
|---|---|---|---|
heroku |
gpt-oss-120b |
❌ False | ❌ False |
nebius |
openai/gpt-oss-120b |
❌ False | ❌ False |
ollama |
gpt-oss:20b |
❌ False | ❌ False |
ovhcloud |
gpt-oss-120b |
❌ False | ❌ False |
cerebras |
gpt-oss-120b |
✅ True | ✅ True |
groq |
openai/gpt-oss-120b |
✅ True | ✅ True |
Cerebras is correct only because it adds its own gateway-layer override (providers/cerebras.py:75-77), and Groq doesn't use harmony_model_profile at all — groq_model_profile has an explicit 'openai/gpt-oss' entry. The four providers that rely on harmony_model_profile alone get nothing.
Root cause
harmony_model_profile delegates the reasoning question to openai_model_profile:
return merge_profile(
openai_model_profile(model_name),
OpenAIModelProfile(openai_supports_tool_choice_required=False, ignore_streamed_leading_whitespace=True),
)But openai_model_profile's prefix table describes the models OpenAI serves on its own Responses API, which doesn't include gpt-oss. A bare gpt-oss-120b matches no prefix and falls through to _NO_REASONING:
return next(
(support for prefix, support in _REASONING_SUPPORT_BY_PREFIX.items() if model_name.startswith(prefix)),
_NO_REASONING,
)There's a wrinkle that makes this easy to miss: the prefixed spelling accidentally works.
>>> openai_model_profile('gpt-oss-120b').get('supports_thinking')
False
>>> openai_model_profile('openai/gpt-oss-120b').get('supports_thinking')
True'openai/gpt-oss-120b'.startswith('o') matches the o-series entry at the end of the table. That's a coincidence, not intent — and it doesn't help any of the four broken providers, because Nebius strips the openai/ prefix before delegating (providers/nebius.py:63-66) and the other three never had it.
Reproduction
import importlib
CASES = [
('cerebras', 'CerebrasProvider', 'gpt-oss-120b'),
('heroku', 'HerokuProvider', 'gpt-oss-120b'),
('nebius', 'NebiusProvider', 'openai/gpt-oss-120b'),
('ollama', 'OllamaProvider', 'gpt-oss:20b'),
('ovhcloud', 'OVHcloudProvider', 'gpt-oss-120b'),
]
for mod, cls, name in CASES:
p = getattr(importlib.import_module(f'pydantic_ai.providers.{mod}'), cls).model_profile(name)
print(f'{mod:10}', {k: p.get(k) for k in ('supports_thinking', 'thinking_always_enabled')})On main:
cerebras {'supports_thinking': True, 'thinking_always_enabled': True}
heroku {'supports_thinking': False, 'thinking_always_enabled': False}
nebius {'supports_thinking': False, 'thinking_always_enabled': False}
ollama {'supports_thinking': False, 'thinking_always_enabled': False}
ovhcloud {'supports_thinking': False, 'thinking_always_enabled': False}Expected behaviour
Reasoning is intrinsic to Harmony, not a per-model trait. From the Harmony guide — the doc harmony_model_profile's own docstring links to:
These are messages that are being used by the model for its chain of thought (CoT).
To control the reasoning you can specify in the system message the reasoning level as low, medium, or high.
By default, the model will do medium level reasoning.
gpt-oss should not be used without using the harmony format, as it will not work correctly
Low/medium/high with medium as the default and no off switch — i.e. supports_thinking=True and thinking_always_enabled=True. This matches what the two correct providers already say in their own comments: Groq's 'openai/gpt-oss' entry is annotated "graded reasoning_effort (low/medium/high), always-on", and Cerebras's override says "gpt-oss reasons unconditionally on Cerebras: disable_reasoning=True is rejected with a 400".
The existing ignore_streamed_leading_whitespace=True in harmony_model_profile follows from the same format — the analysis channel is emitted ahead of final — so the profile already encodes one consequence of Harmony reasoning while omitting the reasoning flags themselves.
Impact
Model.prepare_request drops the setting with no error when neither flag is set:
supports_thinking = self.profile.get('supports_thinking', False)
thinking_always_enabled = self.profile.get('thinking_always_enabled', False)
if supports_thinking or thinking_always_enabled:
if not (thinking_value is False and thinking_always_enabled):
params = replace(params, thinking=thinking_value)So Agent('heroku:gpt-oss-120b', model_settings=ModelSettings(thinking='high')) silently sends no reasoning level, and the model falls back to its medium default.
Why the existing tests don't catch it
tests/profiles/test_resolution_matrix.py exists precisely to catch this class of drift — its docstring says "any change to a provider profile, an upstream profile, or merge_profile() semantics that affects a resolved flag must show up as a diff here." But test_ollama_gpt_oss is the only gpt-oss route it snapshots, and that snapshot currently pins the bug: it has ignore_streamed_leading_whitespace: True and no supports_thinking key at all. The other four Harmony providers (heroku, nebius, ovhcloud, cerebras) have no gpt-oss entry in the matrix, so nothing compares them against each other.
The per-provider tests (tests/providers/test_heroku.py, test_nebius.py, test_ollama.py, test_ovhcloud.py) all assert harmony_model_profile was called with the right name, and some assert ignore_streamed_leading_whitespace is True, but none read the reasoning flags.
Suggested fix
Set the flags in harmony_model_profile, where the format is known, rather than asking openai_model_profile about a model OpenAI doesn't serve:
return merge_profile(
openai_model_profile(model_name),
OpenAIModelProfile(
openai_supports_tool_choice_required=False,
ignore_streamed_leading_whitespace=True,
supports_thinking=True,
thinking_always_enabled=True,
),
)This fixes all four providers at once and leaves Cerebras and Groq unchanged (both already resolve the same values). Non-gpt-oss models on those providers are unaffected — they don't route through harmony_model_profile.
I deliberately did not add a 'gpt-oss' entry to _REASONING_SUPPORT_BY_PREFIX: that table is documented as "verified against the live Responses API", and gpt-oss isn't on it. openai_model_profile returning _NO_REASONING for a bare gpt-oss-120b is arguably correct for what that table describes; the mistake is harmony_model_profile treating it as authoritative.
Branch
Fix plus tests: fix/harmony-gpt-oss-reasoning-flags.
Two parametrized tests in the resolution matrix: one asserting all five Harmony providers agree on supports_thinking / thinking_always_enabled / ignore_streamed_leading_whitespace, plus a Groq case checking the non-Harmony route reaches the same verdict; and a control over five non-gpt-oss models on the same providers asserting they get none of those flags.
Reverting only profiles/harmony.py fails exactly the four broken providers with assert False is True, leaves Cerebras and Groq green, leaves all five controls green, and surfaces the test_ollama_gpt_oss snapshot diff — whose only delta is the two added keys.
Related
Found by an audit that groups models by family across every provider and flags disagreements on flags that are intrinsic to the model rather than the gateway. Same "shared profile advertises less than it should" class as #6822 / #6827 / #6831, but the divergence here is between sibling providers rather than between a profile and the code reading it.
Python, Pydantic AI & LLM client version
pydantic-ai main
Python 3.12, WindowsSource: pydantic/pydantic-ai