`gpt-oss` resolves `supports_thinking=False` on heroku, nebius, ollama and ovhcloud — `harmony_model_profile` delegates reasoning to a table that excludes gpt-oss

Author: LHMQ878Created Jul 29, 2026Updated Sep 18, 2026
Labelsbugthinkingopenaipydanty:bugprovider-parityp:4-lowneeds-maintainer-actionhas-workaround

Initial Checks

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:

https://github.com/pydantic/pydantic-ai/blob/main/pydantic_ai_slim/pydantic_ai/profiles/harmony.py#L7-L15

python
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:

https://github.com/pydantic/pydantic-ai/blob/main/pydantic_ai_slim/pydantic_ai/profiles/openai.py#L152-L156

python
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.

python
>>> 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

python
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:

https://github.com/pydantic/pydantic-ai/blob/main/pydantic_ai_slim/pydantic_ai/models/__init__.py#L419-L424

python
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:

python
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, Windows