{"repo":"NousResearch/hermes-agent","public_id":"55276","display_label":"reasoning_effort routing bugs across multiple providers","summary":"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.","subsystem":"agent","counts":{"members":4,"open_issues":1,"open_issues_distinct":1,"open_prs":0,"closed_issues":0,"merged_prs":1,"attempt_prs":2},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":58156,"merged_at":"2026-07-04T08:49:45+00:00"},"resolution":{"state":"unresolved","by":null},"freshness":{"state":"verified","last_verified_at":"2026-07-04T08:49:45+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.5},"fix_edges":[{"issue_number":55276,"pr_number":58156,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-07-04T08:49:45+00:00"}],"members":[{"kind":"issue","number":55276,"title":"reasoning_effort / thinking_budget silently dropped for custom and zai providers","url":null,"state":"open"},{"kind":"pr","number":55280,"title":"fix(custom-provider): pass through reasoning_effort to extra_body for OpenAI-compatible backends","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"67df958dbe06bb10ca16b8686c76baf0de3bac03","number":0},{"kind":"pr","value":"#58156","number":58156}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":55428,"title":"fix: pass through reasoning_effort for custom/vLLM/Ollama providers (#55276)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"67df958dbe06bb10ca16b8686c76baf0de3bac03","number":0}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":58156,"title":"fix(custom-provider): forward reasoning_effort at the live profile path (GLM-5.2/ARK)","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["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":[{"file":"agent/transports/chat_completions.py","hunks":["@@ -423,7 +423,10 @@ def build_kwargs(\n                 if gh_reasoning is not None:\n                     extra_body[\"reasoning\"] = gh_reasoning\n             else:\n-                extra_body[\"reasoning\"] = {\"enabled\": True, \"effort\": \"medium\"}\n+                _effort = \"medium\"\n+                if reasoning_config and isinstance(reasoning_config, dict):\n+                    _effort = reasoning_config.get(\"effort\", \"medium\") or \"medium\"\n+                extra_body[\"reasoning\"] = {\"enabled\": True, \"effort\": _effort}\n \n         if provider_name == \"gemini\":\n             raw_thinking_config = _build_gemini_thinking_config(model, reasoning_config)"]},{"file":"hermes_constants.py","hunks":["@@ -791,13 +791,13 @@ def apply_subprocess_home_env(env: dict[str, str]) -> None:\n         env[\"HOME\"] = home\n \n \n-VALID_REASONING_EFFORTS = (\"minimal\", \"low\", \"medium\", \"high\", \"xhigh\")\n+VALID_REASONING_EFFORTS = (\"minimal\", \"low\", \"medium\", \"high\", \"xhigh\", \"max\")\n \n \n def parse_reasoning_effort(effort) -> dict | None:\n     \"\"\"Parse a reasoning effort level into a config dict.\n \n-    Valid levels: \"none\", \"minimal\", \"low\", \"medium\", \"high\", \"xhigh\".\n+    Valid levels: \"none\", \"minimal\", \"low\", \"medium\", \"high\", \"xhigh\", \"max\".\n     Returns None when the input is empty or unrecognized (caller uses default).\n     Returns {\"enabled\": False} for \"none\" (aliases: \"false\", \"disabled\", and\n     YAML boolean False — users write ``reasoning_effort: false``/``off``/``no``"]},{"file":"plugins/model-providers/custom/__init__.py","hunks":["@@ -1,9 +1,13 @@\n \"\"\"Custom / Ollama (local) provider profile.\n \n Covers any endpoint registered as provider=\"custom\", including local\n-Ollama instances. Key quirks:\n+Ollama instances and OpenAI-compatible reasoning endpoints (GLM-5.2 on\n+Volcengine ARK, vLLM, llama.cpp). Key quirks:\n   - ollama_num_ctx → extra_body.options.num_ctx (local context window)\n   - reasoning_config disabled → extra_body.think = False\n+  - reasoning_config enabled + effort → top-level reasoning_effort\n+    (the native OpenAI-compatible format GLM/ARK expect; unset omits it\n+    so the endpoint's server default applies)\n \"\"\"\n \n from typing import Any","@@ -23,6 +27,7 @@ def build_api_kwargs_extras(\n         **ctx: Any,\n     ) -> tuple[dict[str, Any], dict[str, Any]]:\n         extra_body: dict[str, Any] = {}\n+        top_level: dict[str, Any] = {}\n \n         # Ollama context window\n         if ollama_num_ctx:","@@ -30,14 +35,29 @@ def build_api_kwargs_extras(\n             options[\"num_ctx\"] = ollama_num_ctx\n             extra_body[\"options\"] = options\n \n-        # Disable thinking when reasoning is turned off\n+        # Reasoning / thinking control for custom OpenAI-compatible endpoints\n+        # (GLM-5.2 on Volcengine ARK, vLLM, Ollama, llama.cpp, …).\n+        #\n+        #   - disabled  → extra_body.think = False (Ollama's thinking-off flag)\n+        #   - enabled + effort set → TOP-LEVEL reasoning_effort string, the\n+        #     format GLM-5.2/ARK and other OpenAI-compatible reasoning APIs\n+        #     expect (GLM documents \"high\" and \"max\"; \"max\" is its default).\n+        #   - enabled + no effort  → omit both, so the endpoint applies its own\n+        #     server-side default (do NOT force a level the user didn't pick).\n+        #\n+        # We deliberately do NOT emit ``think=True`` on enable: it is an\n+        # Ollama-only flag and thinking is already server-default-on for these\n+        # backends, so forcing it risks a 400 on GLM/vLLM endpoints that don't\n+        # recognize it. Mirrors the DeepSeek/Zai profile precedent.\n         if reasoning_config and isinstance(reasoning_config, dict):\n             _effort = (reasoning_config.get(\"effort\") or \"\").strip().lower()\n             _enabled = reasoning_config.get(\"enabled\", True)\n             if _effort == \"none\" or _enabled is False:\n                 extra_body[\"think\"] = False\n+            elif _effort:\n+                top_level[\"reasoning_effort\"] = _effort\n \n-        return extra_body, {}\n+        return extra_body, top_level\n \n     def fetch_models(\n         self,"]},{"file":"scripts/release.py","hunks":["@@ -45,6 +45,7 @@\n \n # Auto-extracted from noreply emails + manual overrides\n AUTHOR_MAP = {\n+    \"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)\n     \"[redacted:email]\": \"infinitycrew39\",  # PR #56431 salvage (honor live vLLM context limits on local endpoints)\n     \"[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)\n     \"[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)"]},{"file":"tests/plugins/model_providers/test_custom_profile.py","hunks":["@@ -0,0 +1,118 @@\n+\"\"\"Unit tests for the custom provider profile's reasoning wiring.\n+\n+``provider=custom`` covers any OpenAI-compatible endpoint the user points\n+Hermes at — local Ollama, vLLM, llama.cpp, and hosted reasoning APIs like\n+GLM-5.2 on Volcengine ARK. Before #57601's salvage, ``CustomProfile`` emitted\n+nothing when reasoning was *enabled*, so a configured ``reasoning_effort``\n+was silently dropped for every custom endpoint.\n+\n+These tests pin the wire-shape contract:\n+  - disabled            → extra_body.think = False\n+  - enabled + effort    → top-level reasoning_effort (native OpenAI-compat\n+                          format GLM/ARK expect), passed through verbatim\n+                          including ``max``/``xhigh``\n+  - enabled + no effort → nothing emitted (endpoint's server default applies)\n+  - ollama_num_ctx      → extra_body.options.num_ctx, orthogonal to reasoning\n+\"\"\"\n+\n+from __future__ import annotations\n+\n+import pytest\n+\n+\n[redacted:email]\n+def custom_profile():\n+    \"\"\"Resolve the registered custom profile via the global registry.\n+\n+    Importing ``model_tools`` triggers plugin discovery, which registers the\n+    ``custom`` profile. Going through ``get_provider_profile`` keeps the test\n+    honest — if the registered class is ever downgraded to a plain\n+    ``ProviderProfile``, the assertions below collapse.\n+    \"\"\"\n+    import model_tools  # noqa: F401\n+    import providers\n+\n+    profile = providers.get_provider_profile(\"custom\")\n+    assert profile is not None, \"custom provider profile must be registered\"\n+    return profile\n+\n+\n+class TestCustomReasoningWireShape:\n+    \"\"\"``build_api_kwargs_extras`` produces the correct wire format.\"\"\"\n+\n+    def test_no_reasoning_config_emits_nothing(self, custom_profile):\n+        \"\"\"Unset reasoning → omit everything so the endpoint's default applies.\"\"\"\n+        eb, tl = custom_profile.build_api_kwargs_extras(\n+            reasoning_config=None, model=\"glm-5.2\"\n+        )\n+        assert eb == {}\n+        assert tl == {}\n+\n+    def test_disabled_sends_think_false(self, custom_profile):\n+        \"\"\"enabled=False → extra_body.think = False (Ollama thinking-off flag).\"\"\"\n+        eb, tl = custom_profile.build_api_kwargs_extras(\n+            reasoning_config={\"enabled\": False}, model=\"glm-5.2\"\n+        )\n+        assert eb == {\"think\": False}\n+        assert tl == {}\n+\n+    def test_effort_none_sends_think_false(self, custom_profile):\n+        \"\"\"effort='none' is the disable alias → think=False, no effort.\"\"\"\n+        eb, tl = custom_profile.build_api_kwargs_extras(\n+            reasoning_config={\"enabled\": True, \"effort\": \"none\"}, model=\"glm-5.2\"\n+        )\n+        assert eb == {\"think\": False}\n+        assert tl == {}\n+\n+    @pytest.mark.parametrize(\n+        \"effort\", [\"minimal\", \"low\", \"medium\", \"high\", \"xhigh\", \"max\"]\n+    )\n+    def test_enabled_effort_goes_top_level(self, custom_profile, effort):\n+        \"\"\"enabled + effort → TOP-LEVEL reasoning_effort, passed through verbatim.\n+\n+        GLM-5.2/ARK and OpenAI-compatible reasoning APIs read reasoning_effort\n+        as a top-level string, not nested in extra_body. ``max`` is GLM's\n+        native deep-reasoning level and must survive.\n+        \"\"\"\n+        eb, tl = custom_profile.build_api_kwargs_extras(\n+            reasoning_config={\"enabled\": True, \"effort\": effort}, model=\"glm-5.2\"\n+        )\n+        assert tl == {\"reasoning_effort\": effort}\n+        assert \"reasoning_effort\" not in eb\n+        assert \"think\" not in eb\n+\n+    def test_enabled_without_effort_emits_nothing(self, custom_profile):\n+        \"\"\"enabled but no effort → omit; do NOT force a level the user didn't pick.\"\"\"\n+        eb, tl = custom_profile.build_api_kwargs_extras(\n+            reasoning_config={\"enabled\": True}, model=\"glm-5.2\"\n+        )\n+        assert eb == {}\n+        assert tl == {}\n+\n+    def test_does_not_force_think_true_on_enable(self, custom_profile):\n+        \"\"\"We must never send think=True on enable — it's Ollama-only and\n+        would 400 on GLM/vLLM endpoints that don't recognize it.\"\"\"\n+        eb, _ = custom_profile.build_api_kwargs_extras(\n+            reasoning_config={\"enabled\": True, \"effort\": \"high\"}, model=\"glm-5.2\"\n+        )\n+        assert eb.get(\"think\") is not True\n+\n+\n+class TestCustomReasoningWithNumCtx:\n+    \"\"\"Ollama num_ctx and reasoning are independent and compose.\"\"\"\n+\n+    def test_num_ctx_alone(self, custom_profile):\n+        eb, tl = custom_profile.build_api_kwargs_extras(\n+            reasoning_config=None, ollama_num_ctx=8192, model=\"qwen3\"\n+        )\n+        assert eb == {\"options\": {\"num_ctx\": 8192}}\n+        assert tl == {}\n+\n+    def test_num_ctx_with_effort(self, custom_profile):\n+        eb, tl = custom_profile.build_api_kwargs_extras(\n+            reasoning_config={\"enabled\": True, \"effort\": \"high\"},\n+            ollama_num_ctx=8192,\n+            model=\"qwen3\",\n+        )\n+        assert eb == {\"options\": {\"num_ctx\": 8192}}\n+        assert tl == {\"reasoning_effort\": \"high\"}"]},{"file":"tests/test_hermes_constants.py","hunks":["@@ -473,7 +473,7 @@ def test_case_and_whitespace_normalized(self, raw, expected_effort):\n \n     @pytest.mark.parametrize(\n         \"value\",\n-        [\"bogus\", \"very-high\", \"max\", \"0\", \"off\", \"true\", \"default\"],\n+        [\"bogus\", \"very-high\", \"0\", \"off\", \"true\", \"default\"],\n     )\n     def test_unknown_levels_return_none(self, value):\n         \"\"\"Unrecognized strings fall back to the caller default (None).\"\"\"","@@ -482,11 +482,11 @@ def test_unknown_levels_return_none(self, value):\n     def test_known_supported_levels_are_documented(self):\n         \"\"\"Guard against silently dropping a documented level.\n \n-        The docstring promises \"minimal\", \"low\", \"medium\", \"high\", \"xhigh\".\n-        If someone removes one from VALID_REASONING_EFFORTS without updating\n-        the docstring, this test will fail and force the call out.\n+        The docstring promises \"minimal\", \"low\", \"medium\", \"high\", \"xhigh\",\n+        \"max\". If someone removes one from VALID_REASONING_EFFORTS without\n+        updating the docstring, this test will fail and force the call out.\n         \"\"\"\n-        documented = {\"minimal\", \"low\", \"medium\", \"high\", \"xhigh\"}\n+        documented = {\"minimal\", \"low\", \"medium\", \"high\", \"xhigh\", \"max\"}\n         assert documented.issubset(set(VALID_REASONING_EFFORTS))\n \n \n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}