Description
Problem
With the releases of Mistral Medium 3.5 and Mistral Small 4, Mistral now has non-magistral models that support thinking via the reasoning_effort API parameter. The current pydantic-ai Mistral integration has two gaps:
1. reasoning_effort is never forwarded to the Mistral API
When model_request_parameters.thinking is set, the Mistral completion methods (_completions_create / _stream_completions_create) don't pass the corresponding reasoning_effort parameter to the Mistral client. The Mistral API supports reasoning_effort (values: "high" or "none") to control chain-of-thought behavior, but currently there's no mapping from pydantic-ai's unified thinking setting.
2. Model profile only recognizes magistral* as thinking-capable
In pydantic_ai_slim/pydantic_ai/profiles/mistral.py:
def mistral_model_profile(model_name: str) -> ModelProfile | None:
is_magistral = model_name.startswith('magistral')
if is_magistral:
return ModelProfile(supports_thinking=True, thinking_always_enabled=True)
return None
This misses two models that support thinking via reasoning_effort (opt-in, not always-on like magistral):
mistral-medium-3-5 — Mistral Medium 3.5
mistral-small-latest — Mistral Small 4 (v26.03), which also exposes configurable reasoning_effort
Unlike magistral models, thinking is not always enabled for these models — it's controlled explicitly by the reasoning_effort parameter.
Additionally, the alias mistral-medium-latest creates ambiguity: it currently resolves to mistral-medium-3-5 on the public API, but on private/self-hosted Mistral deployments it may point to an older version (e.g. mistral-medium-2505) that does not support thinking. The same concern applies to mistral-small-latest on private servers. The profile logic cannot assume these -latest aliases always support thinking.
Proposed Solution
In pydantic_ai_slim/pydantic_ai/profiles/mistral.py:
Add mistral-medium-3-5 and mistral-small-2603 (the versioned name behind mistral-small-latest) as thinking-capable models, with thinking_always_enabled=False since reasoning is opt-in via reasoning_effort:
_ADJUSTABLE_REASONING_MODELS = {'mistral-medium-3-5', 'mistral-small-2603'}
def mistral_model_profile(model_name: str) -> ModelProfile | None:
is_magistral = model_name.startswith('magistral')
if is_magistral:
return ModelProfile(supports_thinking=True, thinking_always_enabled=True)
if model_name in _ADJUSTABLE_REASONING_MODELS:
return ModelProfile(supports_thinking=True, thinking_always_enabled=False)
return None
In pydantic_ai_slim/pydantic_ai/models/mistral.py:
Map the unified thinking parameter to Mistral's reasoning_effort in both _completions_create and _stream_completions_create. When model_request_parameters.thinking is truthy, pass reasoning_effort="high" to self.client.chat.complete_async() / self.client.chat.stream_async().
For example in _completions_create:
async def _completions_create(
self,
messages: list[ModelMessage],
model_settings: MistralModelSettings,
model_request_parameters: ModelRequestParameters,
) -> MistralChatCompletionResponse:
reasoning_effort = self._resolve_reasoning_effort(model_request_parameters)
with _map_api_errors(self.model_name):
response = await self.client.chat.complete_async(
model=str(self._model_name),
messages=await self._map_messages(messages, model_request_parameters),
# ... existing params ...
reasoning_effort=reasoning_effort if reasoning_effort else UNSET,
http_headers={'User-Agent': get_user_agent()},
)
assert response, 'An unexpected empty response from Mistral.'
return response
With a helper to map pydantic-ai's thinking levels to Mistral's binary reasoning_effort:
_THINKING_TO_REASONING_EFFORT: dict[str, str] = {
'high': 'high',
'xhigh': 'high',
'medium': 'high',
'low': 'none',
'minimal': 'none',
}
def _resolve_reasoning_effort(self, model_request_parameters: ModelRequestParameters) -> str | None:
thinking = model_request_parameters.thinking
if thinking is None or thinking is False:
return None
if thinking is True:
return 'high'
return self._THINKING_TO_REASONING_EFFORT.get(thinking, 'high')
Key Considerations
magistral* models: thinking_always_enabled=True — no change needed, they always reason.
mistral-medium-3-5 and mistral-small-2603: thinking_always_enabled=False — thinking is opt-in via reasoning_effort.
mistral-medium-latest and mistral-small-latest: Should we add them to _ADJUSTABLE_REASONING_MODELS, ? On private/self-hosted Mistral servers, these aliases may resolve to older non-thinking versions.
References
Description
Problem
With the releases of Mistral Medium 3.5 and Mistral Small 4, Mistral now has non-magistral models that support thinking via the
reasoning_effortAPI parameter. The current pydantic-ai Mistral integration has two gaps:1.
reasoning_effortis never forwarded to the Mistral APIWhen
model_request_parameters.thinkingis set, the Mistral completion methods (_completions_create/_stream_completions_create) don't pass the correspondingreasoning_effortparameter to the Mistral client. The Mistral API supportsreasoning_effort(values:"high"or"none") to control chain-of-thought behavior, but currently there's no mapping from pydantic-ai's unifiedthinkingsetting.2. Model profile only recognizes
magistral*as thinking-capableIn
pydantic_ai_slim/pydantic_ai/profiles/mistral.py:This misses two models that support thinking via
reasoning_effort(opt-in, not always-on likemagistral):mistral-medium-3-5— Mistral Medium 3.5mistral-small-latest— Mistral Small 4 (v26.03), which also exposes configurablereasoning_effortUnlike
magistralmodels, thinking is not always enabled for these models — it's controlled explicitly by thereasoning_effortparameter.Additionally, the alias
mistral-medium-latestcreates ambiguity: it currently resolves tomistral-medium-3-5on the public API, but on private/self-hosted Mistral deployments it may point to an older version (e.g.mistral-medium-2505) that does not support thinking. The same concern applies tomistral-small-lateston private servers. The profile logic cannot assume these-latestaliases always support thinking.Proposed Solution
In
pydantic_ai_slim/pydantic_ai/profiles/mistral.py:Add
mistral-medium-3-5andmistral-small-2603(the versioned name behindmistral-small-latest) as thinking-capable models, withthinking_always_enabled=Falsesince reasoning is opt-in viareasoning_effort:In
pydantic_ai_slim/pydantic_ai/models/mistral.py:Map the unified
thinkingparameter to Mistral'sreasoning_effortin both_completions_createand_stream_completions_create. Whenmodel_request_parameters.thinkingis truthy, passreasoning_effort="high"toself.client.chat.complete_async()/self.client.chat.stream_async().For example in
_completions_create:With a helper to map pydantic-ai's thinking levels to Mistral's binary reasoning_effort:
Key Considerations
magistral*models:thinking_always_enabled=True— no change needed, they always reason.mistral-medium-3-5andmistral-small-2603:thinking_always_enabled=False— thinking is opt-in viareasoning_effort.mistral-medium-latestandmistral-small-latest: Should we add them to _ADJUSTABLE_REASONING_MODELS, ? On private/self-hosted Mistral servers, these aliases may resolve to older non-thinking versions.References
mistral-small-latestandmistral-medium-3-5as "adjustable reasoning" models alongsidemagistral*reasoning_effortwith values"high"|"none"onmistral-medium-3-5,mistral-small-latestandmagistralmodels