Problem or Use Case
Summary
Add support for Mistral AI's adjustable reasoning capability when api.mistral.ai/v1 is
configured as a custom provider in Hermes Agent. This requires injecting reasoning_effort
as a root-level request field and guarding injection against Mistral model families that do
not support the parameter.
Note: This feature request is specific to Mistral AI's reasoning_effort contract.
The general streaming and field-leakage bugs that were exposed while implementing this
have been filed separately as they affect all strict OpenAI-compatible providers.
Background
Mistral AI offers two approaches to reasoning (see Mistral Reasoning docs):
| Approach |
Models |
How it works |
| Adjustable |
mistral-small-* |
Controlled by a reasoning_effort request parameter (low, medium, high) |
| Native |
magistral-small-*, magistral-medium-* |
Model always reasons; no parameter needed |
When mistral-small-latest is used with reasoning_effort: high in Hermes, the parameter
is currently never sent to the API. Mistral receives no reasoning instruction and returns
a standard (non-reasoning) response.
The parameter must be sent as a root-level field in the Chat Completions request:
{
"model": "mistral-small-latest",
"reasoning_effort": "high",
"messages": [...]
}
This differs from the OpenRouter convention (extra_body.reasoning.effort) and the native
OpenAI o-series convention (reasoning_effort directly on the client kwargs).
Problem
reasoning_effort is never injected for Mistral
File: run_agent.py — _build_api_kwargs()
The method injects reasoning parameters through two existing paths:
- OpenRouter path:
extra_body["reasoning"] = {...} (via _supports_reasoning_extra_body()).
- Ollama path:
extra_body["think"] = false (via Ollama URL detection).
Neither path handles Mistral-style reasoning_effort. The setting is read from config but
silently discarded. Mistral returns a non-reasoning response with no error or warning.
Sending reasoning_effort to unsupported Mistral models causes HTTP 422
Only mistral-small-* accepts reasoning_effort. If the parameter is injected blindly for
all Mistral models:
magistral-small-* / magistral-medium-* — reject it with HTTP 422 (they reason natively,
no parameter is needed or accepted).
- All other Mistral models (
mistral-large-*, mistral-medium-*, codestral, etc.) — reject
it with HTTP 422 (no reasoning support at all).
Proposed Solution
Proposed Implementation
Inject reasoning_effort via extra_body for Mistral, guarded by model family
run_agent.py — _build_api_kwargs(), after the Ollama think=False block:
# Inject reasoning_effort for custom endpoints that use the root-level parameter
# (e.g. Mistral AI). Only send when the model is known to support it.
if (self.provider == "custom" or not self._supports_reasoning_extra_body()) \
and self.reasoning_config and isinstance(self.reasoning_config, dict):
_effort = (self.reasoning_config.get("effort") or "").strip().lower()
_enabled = self.reasoning_config.get("enabled", True)
if _enabled and _effort and _effort != "none":
_model_lower = (self.model or "").lower()
_is_mistral_endpoint = "api.mistral.ai" in self._base_url_lower
if _is_mistral_endpoint:
# Only mistral-small-* supports adjustable reasoning_effort.
# Magistral models reason natively and do not accept this parameter.
# All other Mistral models have no reasoning support.
# See: https://docs.mistral.ai/capabilities/reasoning/
_mistral_supports_effort = _model_lower.startswith("mistral-small")
if not _mistral_supports_effort:
logger.debug(
"Skipping reasoning_effort for Mistral model %r: "
"only mistral-small-* supports adjustable reasoning_effort. "
"Magistral models reason natively; other models will 422.",
self.model,
)
else:
_mistral_supports_effort = True # Non-Mistral custom endpoints: always send
if _mistral_supports_effort:
extra_body["reasoning_effort"] = _effort
extra_body is already merged into api_kwargs["extra_body"] at the end of
_build_api_kwargs(). The OpenAI Python SDK serializes extra_body as root-level JSON
fields — exactly the format Mistral expects.
The check is scoped to api.mistral.ai so non-Mistral custom endpoints (NVIDIA, vLLM,
LiteLLM, etc.) are unaffected.
Extend _supports_reasoning_extra_body() with a Mistral guard
To prevent reasoning_content / reasoning_details from leaking into Mistral message
history (which already causes HTTP 422, see companion bug report), _supports_reasoning_extra_body()
must return False for api.mistral.ai. This is already the case since the method only
returns True for known OpenRouter/Nous Portal/GitHub Models routes — no change needed here,
but it should be explicitly documented:
def _supports_reasoning_extra_body(self) -> bool:
"""Return True when OpenRouter-style reasoning extra_body is safe to send.
Returns False for api.mistral.ai — Mistral uses root-level reasoning_effort
(handled separately) and rejects extra_body.reasoning with extra_forbidden.
"""
if "api.mistral.ai" in self._base_url_lower:
return False # explicit: Mistral uses reasoning_effort, not extra_body.reasoning
...
Mistral Reasoning Model Table
| Model slug |
Reasoning support |
reasoning_effort accepted |
mistral-small-latest (mistral-small-3.1) |
Adjustable |
✅ Yes |
mistral-small-3-1-* versioned aliases |
Adjustable |
✅ Yes |
magistral-small-latest |
Native (always on) |
❌ No (422) |
magistral-medium-latest |
Native (always on) |
❌ No (422) |
mistral-large-latest |
None |
❌ No (422) |
mistral-medium-latest |
None |
❌ No (422) |
codestral-latest |
None |
❌ No (422) |
| All other Mistral models |
None |
❌ No (422) |
Source: https://docs.mistral.ai/capabilities/reasoning/
Verification
Tested locally against mistral-small-latest with reasoning_effort: high:
import run_agent
agent = run_agent.AIAgent(
model="mistral-small-latest",
api_key="<MISTRAL_API_KEY>",
provider="custom",
base_url="https://api.mistral.ai/v1",
reasoning_config={"enabled": True, "effort": "high"},
max_iterations=2,
)
agent.reasoning_callback = lambda x: print("REASONING:", x[:80])
result = agent.run_conversation("Explain the halting problem.")
print("RESPONSE:", result.get("final_response", "")[:120])
Expected output:
REASONING: <Mistral thinking trace, streamed in real time>
RESPONSE: The halting problem is a fundamental result in computability theory...
Also verified that mistral-large-latest and codestral-latest do not receive
reasoning_effort and respond normally.
Files Changed
| File |
Change |
run_agent.py |
Inject reasoning_effort into extra_body for supported Mistral models |
run_agent.py |
Document Mistral exclusion in _supports_reasoning_extra_body() |
Acceptance Criteria
References
Labels
feat provider-compat reasoning mistral
Alternatives Considered
No response
Feature Type
Configuration option
Scope
Small (single file, < 50 lines)
Contribution
Debug Report (optional)
Problem or Use Case
Summary
Add support for Mistral AI's adjustable reasoning capability when
api.mistral.ai/v1isconfigured as a
customprovider in Hermes Agent. This requires injectingreasoning_effortas a root-level request field and guarding injection against Mistral model families that do
not support the parameter.
Background
Mistral AI offers two approaches to reasoning (see Mistral Reasoning docs):
mistral-small-*reasoning_effortrequest parameter (low,medium,high)magistral-small-*,magistral-medium-*When
mistral-small-latestis used withreasoning_effort: highin Hermes, the parameteris currently never sent to the API. Mistral receives no reasoning instruction and returns
a standard (non-reasoning) response.
The parameter must be sent as a root-level field in the Chat Completions request:
{ "model": "mistral-small-latest", "reasoning_effort": "high", "messages": [...] }This differs from the OpenRouter convention (
extra_body.reasoning.effort) and the nativeOpenAI o-series convention (
reasoning_effortdirectly on the client kwargs).Problem
reasoning_effortis never injected for MistralFile:
run_agent.py—_build_api_kwargs()The method injects reasoning parameters through two existing paths:
extra_body["reasoning"] = {...}(via_supports_reasoning_extra_body()).extra_body["think"] = false(via Ollama URL detection).Neither path handles Mistral-style
reasoning_effort. The setting is read from config butsilently discarded. Mistral returns a non-reasoning response with no error or warning.
Sending
reasoning_effortto unsupported Mistral models causes HTTP 422Only
mistral-small-*acceptsreasoning_effort. If the parameter is injected blindly forall Mistral models:
magistral-small-*/magistral-medium-*— reject it with HTTP 422 (they reason natively,no parameter is needed or accepted).
mistral-large-*,mistral-medium-*,codestral, etc.) — rejectit with HTTP 422 (no reasoning support at all).
Proposed Solution
Proposed Implementation
Inject
reasoning_effortviaextra_bodyfor Mistral, guarded by model familyrun_agent.py—_build_api_kwargs(), after the Ollamathink=Falseblock:extra_bodyis already merged intoapi_kwargs["extra_body"]at the end of_build_api_kwargs(). The OpenAI Python SDK serializesextra_bodyas root-level JSONfields — exactly the format Mistral expects.
The check is scoped to
api.mistral.aiso non-Mistral custom endpoints (NVIDIA, vLLM,LiteLLM, etc.) are unaffected.
Extend
_supports_reasoning_extra_body()with a Mistral guardTo prevent
reasoning_content/reasoning_detailsfrom leaking into Mistral messagehistory (which already causes HTTP 422, see companion bug report),
_supports_reasoning_extra_body()must return
Falseforapi.mistral.ai. This is already the case since the method onlyreturns
Truefor known OpenRouter/Nous Portal/GitHub Models routes — no change needed here,but it should be explicitly documented:
Mistral Reasoning Model Table
reasoning_effortacceptedmistral-small-latest(mistral-small-3.1)mistral-small-3-1-*versioned aliasesmagistral-small-latestmagistral-medium-latestmistral-large-latestmistral-medium-latestcodestral-latestSource: https://docs.mistral.ai/capabilities/reasoning/
Verification
Tested locally against
mistral-small-latestwithreasoning_effort: high:Expected output:
Also verified that
mistral-large-latestandcodestral-latestdo not receivereasoning_effortand respond normally.Files Changed
run_agent.pyreasoning_effortintoextra_bodyfor supported Mistral modelsrun_agent.py_supports_reasoning_extra_body()Acceptance Criteria
reasoning_effortis present as a root-level field in the outgoing JSON whenprovider=custom,base_urlisapi.mistral.ai/v1, model ismistral-small-*,and
reasoning_effortis configured and notnone.reasoning_effortis not sent tomagistral-*,mistral-large-*,mistral-medium-*,codestral, or any other non-supporting Mistral model family.python -m pytest tests/ -qpasses with no regressions.References
reasoning_effort): https://docs.mistral.ai/capabilities/reasoning/adjustableLabels
featprovider-compatreasoningmistralAlternatives Considered
No response
Feature Type
Configuration option
Scope
Small (single file, < 50 lines)
Contribution
Debug Report (optional)