Skip to content

[Feature]: Native reasoning_effort support for Mistral AI (api.mistral.ai/v1) endpoint #11243

Description

@wyan-r

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

  • reasoning_effort is present as a root-level field in the outgoing JSON when
    provider=custom, base_url is api.mistral.ai/v1, model is mistral-small-*,
    and reasoning_effort is configured and not none.
  • reasoning_effort is not sent to magistral-*, mistral-large-*,
    mistral-medium-*, codestral, or any other non-supporting Mistral model family.
  • Non-Mistral custom endpoints (NVIDIA, vLLM, etc.) are unaffected by this change.
  • python -m pytest tests/ -q passes with no regressions.

References

Labels

feat provider-compat reasoning mistral

Alternatives Considered

No response

Feature Type

Configuration option

Scope

Small (single file, < 50 lines)

Contribution

  • I'd like to implement this myself and submit a PR

Debug Report (optional)

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Medium — degraded but workaround existsarea/configConfig system, migrations, profilescomp/agentCore agent runtime: loop, agent_init, prompt builder, context-compression, responses endpointneeds-decisionAwaiting maintainer decision before any implementationtype/featureNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions