NousResearch/hermes-agent

reasoning_effort routing bugs across multiple providers

Cluster 55276 · agent. Data as of

1 open item. Fix merged in #58156 on 4 Jul 2026. Check it covers your case.

The short answer

1open issue
0open PRs
4items grouped
Known?
Yes. 4 related items grouped here, 0 issues already closed.
Fix event
A fix was merged to main in PR #58156. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this unresolved. That is separate from the fix event.
Still open
1 issue and 0 PRs.

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.

Open issues1

  • Issue#55276reasoning_effort / thinking_budget silently dropped for custom and zai providers

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #58156 fix(custom-provider): forward reasoning_effort at the live profile path (GLM-5.2/ARK)

    merged to main

    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

    Show the diff
    • agent/transports/chat_completions.py
      @@ -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
      @@ -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
      @@ -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
      @@ -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:
      @@ -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
      @@ -45,6 +45,7 @@
       
       # Auto-extracted from noreply emails + manual overrides
       AUTHOR_MAP = {
      +    "[email protected]": "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
      @@ -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
      @@ -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)."""
      @@ -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))
       
       

2 PRs closed without merging

From the close comments: done elsewhere 2

  • #55280 fix(custom-provider): pass through reasoning_effort to extra_body for OpenAI-compatible backends · done elsewhere → 67df958, #58156
  • #55428 fix: pass through reasoning_effort for custom/vLLM/Ollama providers (#55276) · done elsewhere → 67df958
How this cluster was grouped
  • Link evidence (structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
  • Pairwise link density 0.50: the share of item 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.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
agent
Merged PRs
#58156
Closed issues
0
Attempts
2 PRs closed without merging
Fix link checked
4 Jul 2026

For your agent

The same facts, typed.