# NousResearch/hermes-agent · reasoning_effort routing bugs across multiple providers

## Open now

- 1 issue, 0 PRs open on GitHub.
- #55276: reasoning_effort / thinking_budget silently dropped for custom and zai providers

Multiple fixes and reports around reasoning_effort (and related parameters) being silently dropped, incorrectly forwarded, or sent to unsupported APIs across Ollama, custom, vLLM, and other OpenAI-compatible providers. Includes at least one case where effort levels produced inverted behavior.

## TL;DR

- 4 member(s): 1 open issue(s), 0 closed issue(s), 0 open PR(s), 1 merged PR(s), 2 PR(s) closed without merging.
- A fix was merged to main in PR #58156. Verify it addresses your case.
- **Recommended:** read PR #58156 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `unresolved`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.50`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Open related issues

- #55276: reasoning_effort / thinking_budget silently dropped for custom and zai providers

## Prior & competing PRs

- **#55428: fix: pass through reasoning_effort for custom/vLLM/Ollama providers (#55276)**. closed without merge · fix tier `suggested`
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `67df958dbe06bb10ca16b8686c76baf0de3bac03`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#58156: fix(custom-provider): forward reasoning_effort at the live profile path (GLM-5.2/ARK)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `agent/transports/chat_completions.py`, `hermes_constants.py`, `plugins/model-providers/custom/__init__.py`, `scripts/release.py`, `tests/plugins/model_providers/test_custom_profile.py`, `tests/test_hermes_constants.py`
  - fix hunks:
  - `agent/transports/chat_completions.py`

    ```diff
    @@ -423,7 +423,10 @@ def build_kwargs(
                     if gh_reasoning is not None:
                         extra_body["reasoning"] = gh_reasoning
                 else:
    -                extra_body["reasoning"] = {"enabled": True, "effort": "medium"}
    +                _effort = "medium"
    +                if reasoning_config and isinstance(reasoning_config, dict):
    +                    _effort = reasoning_config.get("effort", "medium") or "medium"
    +                extra_body["reasoning"] = {"enabled": True, "effort": _effort}
     
             if provider_name == "gemini":
                 raw_thinking_config = _build_gemini_thinking_config(model, reasoning_config)
    ```
  - `hermes_constants.py`

    ```diff
    @@ -791,13 +791,13 @@ def apply_subprocess_home_env(env: dict[str, str]) -> None:
             env["HOME"] = home
     
     
    -VALID_REASONING_EFFORTS = ("minimal", "low", "medium", "high", "xhigh")
    +VALID_REASONING_EFFORTS = ("minimal", "low", "medium", "high", "xhigh", "max")
     
     
     def parse_reasoning_effort(effort) -> dict | None:
         """Parse a reasoning effort level into a config dict.
     
    -    Valid levels: "none", "minimal", "low", "medium", "high", "xhigh".
    +    Valid levels: "none", "minimal", "low", "medium", "high", "xhigh", "max".
         Returns None when the input is empty or unrecognized (caller uses default).
         Returns {"enabled": False} for "none" (aliases: "false", "disabled", and
         YAML boolean False — users write ``reasoning_effort: false``/``off``/``no``
    ```
  - `plugins/model-providers/custom/__init__.py`

    ```diff
    @@ -1,9 +1,13 @@
     """Custom / Ollama (local) provider profile.
     
     Covers any endpoint registered as provider="custom", including local
    -Ollama instances. Key quirks:
    +Ollama instances and OpenAI-compatible reasoning endpoints (GLM-5.2 on
    +Volcengine ARK, vLLM, llama.cpp). Key quirks:
       - ollama_num_ctx → extra_body.options.num_ctx (local context window)
       - reasoning_config disabled → extra_body.think = False
    +  - reasoning_config enabled + effort → top-level reasoning_effort
    +    (the native OpenAI-compatible format GLM/ARK expect; unset omits it
    +    so the endpoint's server default applies)
     """
     
     from typing import Any
    ```

    ```diff
    @@ -23,6 +27,7 @@ def build_api_kwargs_extras(
             **ctx: Any,
         ) -> tuple[dict[str, Any], dict[str, Any]]:
             extra_body: dict[str, Any] = {}
    +        top_level: dict[str, Any] = {}
     
             # Ollama context window
             if ollama_num_ctx:
    ```

    ```diff
    @@ -30,14 +35,29 @@ def build_api_kwargs_extras(
                 options["num_ctx"] = ollama_num_ctx
                 extra_body["options"] = options
     
    -        # Disable thinking when reasoning is turned off
    +        # Reasoning / thinking control for custom OpenAI-compatible endpoints
    +        # (GLM-5.2 on Volcengine ARK, vLLM, Ollama, llama.cpp, …).
    +        #
    +        #   - disabled  → extra_body.think = False (Ollama's thinking-off flag)
    +        #   - enabled + effort set → TOP-LEVEL reasoning_effort string, the
    +        #     format GLM-5.2/ARK and other OpenAI-compatible reasoning APIs
    +        #     expect (GLM documents "high" and "max"; "max" is its default).
    +        #   - enabled + no effort  → omit both, so the endpoint applies its own
    +        #     server-side default (do NOT force a level the user didn't pick).
    +        #
    +        # We deliberately do NOT emit ``think=True`` on enable: it is an
    +        # Ollama-only flag and thinking is already server-default-on for these
    +        # backends, so forcing it risks a 400 on GLM/vLLM endpoints that don't
    +        # recognize it. Mirrors the DeepSeek/Zai profile precedent.
             if reasoning_config and isinstance(reasoning_config, dict):
                 _effort = (reasoning_config.get("effort") or "").strip().lower()
                 _enabled = reasoning_config.get("enabled", True)
                 if _effort == "none" or _enabled is False:
                     extra_body["think"] = False
    +            elif _effort:
    +                top_level["reasoning_effort"] = _effort
     
    -        return extra_body, {}
    +        return extra_body, top_level
     
         def fetch_models(
             self,
    ```
  - `scripts/release.py`

    ```diff
    @@ -45,6 +45,7 @@
     
     # Auto-extracted from noreply emails + manual overrides
     AUTHOR_MAP = {
    +    "huanshan5195@users.noreply.github.com": "huanshan5195",  # PR #57601 salvage (custom-provider: emit reasoning_effort at the live CustomProfile path so GLM-5.2/ARK/vLLM/Ollama endpoints receive it; + "max" reasoning level)
         "[redacted:email]": "infinitycrew39",  # PR #56431 salvage (honor live vLLM context limits on local endpoints)
         "[redacted:email]": "CocaKova",  # PR #57692 salvage (cron: run jobs under the profile secret scope so get_secret does not fail-close with UnscopedSecretError under profile isolation)
         "[redacted:email]": "trismegistus-wanderer",  # PR #31856 salvage (gateway: defer idle-TTL agent-cache eviction until the session store says the session actually expired, so the expiry watcher can still fire MemoryProvider.on_session_end with the live transcript; #11205)
    ```
  - `tests/plugins/model_providers/test_custom_profile.py`

    ```diff
    @@ -0,0 +1,118 @@
    +"""Unit tests for the custom provider profile's reasoning wiring.
    +
    +``provider=custom`` covers any OpenAI-compatible endpoint the user points
    +Hermes at — local Ollama, vLLM, llama.cpp, and hosted reasoning APIs like
    +GLM-5.2 on Volcengine ARK. Before #57601's salvage, ``CustomProfile`` emitted
    +nothing when reasoning was *enabled*, so a configured ``reasoning_effort``
    +was silently dropped for every custom endpoint.
    +
    +These tests pin the wire-shape contract:
    +  - disabled            → extra_body.think = False
    +  - enabled + effort    → top-level reasoning_effort (native OpenAI-compat
    +                          format GLM/ARK expect), passed through verbatim
    +                          including ``max``/``xhigh``
    +  - enabled + no effort → nothing emitted (endpoint's server default applies)
    +  - ollama_num_ctx      → extra_body.options.num_ctx, orthogonal to reasoning
    +"""
    +
    +from __future__ import annotations
    +
    +import pytest
    +
    +
    [redacted:email]
    +def custom_profile():
    +    """Resolve the registered custom profile via the global registry.
    +
    +    Importing ``model_tools`` triggers plugin discovery, which registers the
    +    ``custom`` profile. Going through ``get_provider_profile`` keeps the test
    +    honest — if the registered class is ever downgraded to a plain
    +    ``ProviderProfile``, the assertions below collapse.
    +    """
    +    import model_tools  # noqa: F401
    +    import providers
    +
    +    profile = providers.get_provider_profile("custom")
    +    assert profile is not None, "custom provider profile must be registered"
    +    return profile
    +
    +
    +class TestCustomReasoningWireShape:
    +    """``build_api_kwargs_extras`` produces the correct wire format."""
    +
    +    def test_no_reasoning_config_emits_nothing(self, custom_profile):
    +        """Unset reasoning → omit everything so the endpoint's default applies."""
    +        eb, tl = custom_profile.build_api_kwargs_extras(
    +            reasoning_config=None, model="glm-5.2"
    +        )
    +        assert eb == {}
    +        assert tl == {}
    +
    +    def test_disabled_sends_think_false(self, custom_profile):
    +        """enabled=False → extra_body.think = False (Ollama thinking-off flag)."""
    +        eb, tl = custom_profile.build_api_kwargs_extras(
    +            reasoning_config={"enabled": False}, model="glm-5.2"
    +        )
    +        assert eb == {"think": False}
    +        assert tl == {}
    +
    +    def test_effort_none_sends_think_false(self, custom_profile):
    +        """effort='none' is the disable alias → think=False, no effort."""
    +        eb, tl = custom_profile.build_api_kwargs_extras(
    +            reasoning_config={"enabled": True, "effort": "none"}, model="glm-5.2"
    +        )
    +        assert eb == {"think": False}
    +        assert tl == {}
    +
    +    @pytest.mark.parametrize(
    +        "effort", ["minimal", "low", "medium", "high", "xhigh", "max"]
    +    )
    +    def test_enabled_effort_goes_top_level(self, custom_profile, effort):
    +        """enabled + effort → TOP-LEVEL reasoning_effort, passed through verbatim.
    +
    +        GLM-5.2/ARK and OpenAI-compatible reasoning APIs read reasoning_effort
    +        as a top-level string, not nested in extra_body. ``max`` is GLM's
    +        native deep-reasoning level and must survive.
    +        """
    +        eb, tl = custom_profile.build_api_kwargs_extras(
    +            reasoning_config={"enabled": True, "effort": effort}, model="glm-5.2"
    +        )
    +        assert tl == {"reasoning_effort": effort}
    +        assert "reasoning_effort" not in eb
    +        assert "think" not in eb
    +
    +    def test_enabled_without_effort_emits_nothing(self, custom_profile):
    +        """enabled but no effort → omit; do NOT force a level the user didn't pick."""
    +        eb, tl = custom_profile.build_api_kwargs_extras(
    +            reasoning_config={"enabled": True}, model="glm-5.2"
    +        )
    +        assert eb == {}
    +        assert tl == {}
    +
    +    def test_does_not_force_think_true_on_enable(self, custom_profile):
    +        """We must never send think=True on enable — it's Ollama-only and
    +        would 400 on GLM/vLLM endpoints that don't recognize it."""
    +        eb, _ = custom_profile.build_api_kwargs_extras(
    +            reasoning_config={"enabled": True, "effort": "high"}, model="glm-5.2"
    +        )
    +        assert eb.get("think") is not True
    +
    +
    +class TestCustomReasoningWithNumCtx:
    +    """Ollama num_ctx and reasoning are independent and compose."""
    +
    +    def test_num_ctx_alone(self, custom_profile):
    +        eb, tl = custom_profile.build_api_kwargs_extras(
    +            reasoning_config=None, ollama_num_ctx=8192, model="qwen3"
    +        )
    +        assert eb == {"options": {"num_ctx": 8192}}
    +        assert tl == {}
    +
    +    def test_num_ctx_with_effort(self, custom_profile):
    +        eb, tl = custom_profile.build_api_kwargs_extras(
    +            reasoning_config={"enabled": True, "effort": "high"},
    +            ollama_num_ctx=8192,
    +            model="qwen3",
    +        )
    +        assert eb == {"options": {"num_ctx": 8192}}
    +        assert tl == {"reasoning_effort": "high"}
    ```
  - `tests/test_hermes_constants.py`

    ```diff
    @@ -473,7 +473,7 @@ def test_case_and_whitespace_normalized(self, raw, expected_effort):
     
         @pytest.mark.parametrize(
             "value",
    -        ["bogus", "very-high", "max", "0", "off", "true", "default"],
    +        ["bogus", "very-high", "0", "off", "true", "default"],
         )
         def test_unknown_levels_return_none(self, value):
             """Unrecognized strings fall back to the caller default (None)."""
    ```

    ```diff
    @@ -482,11 +482,11 @@ def test_unknown_levels_return_none(self, value):
         def test_known_supported_levels_are_documented(self):
             """Guard against silently dropping a documented level.
     
    -        The docstring promises "minimal", "low", "medium", "high", "xhigh".
    -        If someone removes one from VALID_REASONING_EFFORTS without updating
    -        the docstring, this test will fail and force the call out.
    +        The docstring promises "minimal", "low", "medium", "high", "xhigh",
    +        "max". If someone removes one from VALID_REASONING_EFFORTS without
    +        updating the docstring, this test will fail and force the call out.
             """
    -        documented = {"minimal", "low", "medium", "high", "xhigh"}
    +        documented = {"minimal", "low", "medium", "high", "xhigh", "max"}
             assert documented.issubset(set(VALID_REASONING_EFFORTS))
     
     
    ```
- **#55280: fix(custom-provider): pass through reasoning_effort to extra_body for OpenAI-compatible backends**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `67df958dbe06bb10ca16b8686c76baf0de3bac03`, `#58156`

## Fix status

- Merged to main in PR #58156 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-07-04`.
- Subsystem: `agent`
