{"repo":"NousResearch/hermes-agent","public_id":"23088","display_label":"xAI grok models rejecting reasoning.effort parameter","summary":"These issues and fixes address HTTP 400 errors caused by sending the reasoning.effort parameter to xAI grok models that do not accept it. The work involves conditionally omitting or gating that parameter based on the specific model being used.","subsystem":"agent/transports","counts":{"members":4,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":2},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":23435,"merged_at":"2026-05-10T22:21:31+00:00"},"resolution":{"state":"unresolved","by":null},"freshness":{"state":"verified","last_verified_at":"2026-05-10T22:21:31+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.5},"fix_edges":[{"issue_number":23088,"pr_number":23435,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-05-10T22:21:31+00:00"}],"members":[{"kind":"issue","number":23088,"title":"[Bug]: xAI grok-4-1-fast returns HTTP 400 — \"does not support parameter reasoningEffort\"","url":null,"state":"closed"},{"kind":"pr","number":23106,"title":"fix(transports/codex): gate xAI reasoning.effort per model","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#23088","number":23088},{"kind":"pr","value":"#23435","number":23435}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":23109,"title":"fix(xai): skip reasoning effort for grok 4.1 responses","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#23435","number":23435}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":23435,"title":"fix(xai): omit reasoning.effort for grok models that reject it","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["agent/model_metadata.py","agent/transports/codex.py","tests/agent/transports/test_codex_transport.py"],"fix_hunks":[{"file":"agent/model_metadata.py","hunks":["@@ -244,6 +244,44 @@ def _strip_provider_prefix(model: str) -> str:\n     \"zai-org/GLM-5\": 202752,\n }\n \n+# xAI Grok models that ACCEPT the `reasoning.effort` parameter on\n+# api.x.ai. Verified live against /v1/responses 2026-05-10:\n+#\n+#   ACCEPTS effort:  grok-3-mini, grok-3-mini-fast, grok-4.20-multi-agent-0309,\n+#                    grok-4.3\n+#   REJECTS effort:  grok-3, grok-4, grok-4-0709, grok-4-fast-(non-)reasoning,\n+#                    grok-4-1-fast-(non-)reasoning, grok-4.20-0309-(non-)reasoning,\n+#                    grok-code-fast-1\n+#\n+# REJECTS-side models still reason natively — they just don't expose an\n+# effort dial — so callers should send no `reasoning` key at all rather\n+# than a default `medium` (which 400s with \"Model X does not support\n+# parameter reasoningEffort\").\n+_GROK_EFFORT_CAPABLE_PREFIXES = (\n+    \"grok-3-mini\",\n+    \"grok-4.20-multi-agent\",\n+    \"grok-4.3\",\n+)\n+\n+\n+def grok_supports_reasoning_effort(model: str) -> bool:\n+    \"\"\"Return True when an xAI Grok model accepts ``reasoning.effort``.\n+\n+    Allowlist by substring (matches both bare ``grok-3-mini`` and\n+    aggregator-prefixed ``x-ai/grok-3-mini``). Conservative by design:\n+    if a future Grok model isn't listed, we send no effort dial rather\n+    than 400.\n+    \"\"\"\n+    name = (model or \"\").strip().lower()\n+    if not name:\n+        return False\n+    # Strip common aggregator prefixes (x-ai/, openrouter/x-ai/, xai/, ...)\n+    for sep in (\"/\",):\n+        if sep in name:\n+            name = name.rsplit(sep, 1)[-1]\n+    return any(name.startswith(prefix) for prefix in _GROK_EFFORT_CAPABLE_PREFIXES)\n+\n+\n _CONTEXT_LENGTH_KEYS = (\n     \"context_length\",\n     \"context_window\","]},{"file":"agent/transports/codex.py","hunks":["@@ -104,8 +104,16 @@ def build_kwargs(\n             kwargs[\"prompt_cache_key\"] = session_id\n \n         if reasoning_enabled and is_xai_responses:\n+            from agent.model_metadata import grok_supports_reasoning_effort\n+\n             kwargs[\"include\"] = [\"reasoning.encrypted_content\"]\n-            kwargs[\"reasoning\"] = {\"effort\": reasoning_effort}\n+            # xAI rejects `reasoning.effort` on grok-4 / grok-4-fast / grok-3\n+            # / grok-code-fast / grok-4.20-0309-* with HTTP 400 even though\n+            # those models reason natively. Only send the effort dial when\n+            # the target model is on the allowlist; otherwise send no\n+            # `reasoning` key at all and let the model reason on its own.\n+            if grok_supports_reasoning_effort(model):\n+                kwargs[\"reasoning\"] = {\"effort\": reasoning_effort}\n         elif reasoning_enabled:\n             if is_github_responses:\n                 github_reasoning = params.get(\"github_reasoning_extra\")"]},{"file":"tests/agent/transports/test_codex_transport.py","hunks":["@@ -180,6 +180,119 @@ def test_xai_minimal_effort_clamped(self, transport):\n         # \"minimal\" should be clamped to \"low\" for xAI as well\n         assert kw.get(\"reasoning\", {}).get(\"effort\") == \"low\"\n \n+    # --- Grok reasoning-effort capability allowlist ---\n+    # api.x.ai 400s with \"Model X does not support parameter reasoningEffort\"\n+    # on grok-4 / grok-4-fast / grok-3 / grok-code-fast / grok-4.20-0309-*.\n+    # Those models reason natively but don't expose the dial. The transport\n+    # must omit the `reasoning` key for them while keeping the encrypted\n+    # reasoning content include so we can capture native reasoning tokens.\n+\n+    def test_xai_grok_4_omits_reasoning_effort(self, transport):\n+        \"\"\"grok-4 / grok-4-0709 reject reasoning.effort with HTTP 400.\"\"\"\n+        messages = [{\"role\": \"user\", \"content\": \"Hi\"}]\n+        for model in (\"grok-4\", \"grok-4-0709\"):\n+            kw = transport.build_kwargs(\n+                model=model, messages=messages, tools=[],\n+                is_xai_responses=True,\n+                reasoning_config={\"effort\": \"high\"},\n+            )\n+            assert \"reasoning\" not in kw, (\n+                f\"{model} must not receive a reasoning key (xAI rejects it)\"\n+            )\n+            # Still capture native reasoning tokens\n+            assert \"reasoning.encrypted_content\" in kw.get(\"include\", [])\n+\n+    def test_xai_grok_4_fast_omits_reasoning_effort(self, transport):\n+        \"\"\"grok-4-fast and grok-4-1-fast variants reject reasoning.effort.\"\"\"\n+        messages = [{\"role\": \"user\", \"content\": \"Hi\"}]\n+        for model in (\n+            \"grok-4-fast-reasoning\",\n+            \"grok-4-fast-non-reasoning\",\n+            \"grok-4-1-fast-reasoning\",\n+            \"grok-4-1-fast-non-reasoning\",\n+        ):\n+            kw = transport.build_kwargs(\n+                model=model, messages=messages, tools=[],\n+                is_xai_responses=True,\n+                reasoning_config={\"effort\": \"low\"},\n+            )\n+            assert \"reasoning\" not in kw, (\n+                f\"{model} must not receive a reasoning key (xAI rejects it)\"\n+            )\n+\n+    def test_xai_grok_3_non_mini_omits_reasoning_effort(self, transport):\n+        \"\"\"Plain grok-3 rejects reasoning.effort — only grok-3-mini accepts it.\"\"\"\n+        messages = [{\"role\": \"user\", \"content\": \"Hi\"}]\n+        kw = transport.build_kwargs(\n+            model=\"grok-3\", messages=messages, tools=[],\n+            is_xai_responses=True,\n+            reasoning_config={\"effort\": \"medium\"},\n+        )\n+        assert \"reasoning\" not in kw\n+\n+    def test_xai_grok_3_mini_keeps_reasoning_effort(self, transport):\n+        \"\"\"grok-3-mini and -fast variants do accept the effort dial.\"\"\"\n+        messages = [{\"role\": \"user\", \"content\": \"Hi\"}]\n+        for model in (\"grok-3-mini\", \"grok-3-mini-fast\"):\n+            kw = transport.build_kwargs(\n+                model=model, messages=messages, tools=[],\n+                is_xai_responses=True,\n+                reasoning_config={\"effort\": \"high\"},\n+            )\n+            assert kw.get(\"reasoning\") == {\"effort\": \"high\"}\n+\n+    def test_xai_grok_4_20_0309_variants_omit_reasoning_effort(self, transport):\n+        \"\"\"grok-4.20-0309-(non-)reasoning reject the effort dial.\n+\n+        Counterintuitively, only grok-4.20-multi-agent-0309 accepts it.\n+        \"\"\"\n+        messages = [{\"role\": \"user\", \"content\": \"Hi\"}]\n+        for model in (\"grok-4.20-0309-reasoning\", \"grok-4.20-0309-non-reasoning\"):\n+            kw = transport.build_kwargs(\n+                model=model, messages=messages, tools=[],\n+                is_xai_responses=True,\n+                reasoning_config={\"effort\": \"high\"},\n+            )\n+            assert \"reasoning\" not in kw, f\"{model} must not receive reasoning\"\n+\n+    def test_xai_grok_4_20_multi_agent_keeps_reasoning_effort(self, transport):\n+        \"\"\"grok-4.20-multi-agent-0309 is the one grok-4.20 variant that accepts effort.\"\"\"\n+        messages = [{\"role\": \"user\", \"content\": \"Hi\"}]\n+        kw = transport.build_kwargs(\n+            model=\"grok-4.20-multi-agent-0309\", messages=messages, tools=[],\n+            is_xai_responses=True,\n+            reasoning_config={\"effort\": \"low\"},\n+        )\n+        assert kw.get(\"reasoning\") == {\"effort\": \"low\"}\n+\n+    def test_xai_grok_code_fast_omits_reasoning_effort(self, transport):\n+        \"\"\"grok-code-fast-1 rejects reasoning.effort.\"\"\"\n+        messages = [{\"role\": \"user\", \"content\": \"Hi\"}]\n+        kw = transport.build_kwargs(\n+            model=\"grok-code-fast-1\", messages=messages, tools=[],\n+            is_xai_responses=True,\n+            reasoning_config={\"effort\": \"high\"},\n+        )\n+        assert \"reasoning\" not in kw\n+\n+    def test_xai_aggregator_prefix_stripped(self, transport):\n+        \"\"\"`x-ai/grok-3-mini` (OpenRouter-style slug) still resolves correctly.\"\"\"\n+        messages = [{\"role\": \"user\", \"content\": \"Hi\"}]\n+        # Effort-capable\n+        kw = transport.build_kwargs(\n+            model=\"x-ai/grok-3-mini\", messages=messages, tools=[],\n+            is_xai_responses=True,\n+            reasoning_config={\"effort\": \"high\"},\n+        )\n+        assert kw.get(\"reasoning\") == {\"effort\": \"high\"}\n+        # Effort-incapable\n+        kw = transport.build_kwargs(\n+            model=\"x-ai/grok-4-0709\", messages=messages, tools=[],\n+            is_xai_responses=True,\n+            reasoning_config={\"effort\": \"high\"},\n+        )\n+        assert \"reasoning\" not in kw\n+\n \n class TestCodexValidateResponse:\n \n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}