{"repo":"NousResearch/hermes-agent","public_id":"17426","display_label":"Gemini thinking_config malformed request causing 400 errors","summary":"Multiple issues and fixes related to incorrectly structured or inappropriately injected thinking_config in Gemini API requests, resulting in 400 Bad Request errors. Specific cases include double-nesting in extra_body, injection for unsupported models like Gemma and gemini-3-flash-preview, and OpenAI-compat path mismatches.","subsystem":"agent","counts":{"members":11,"open_issues":1,"open_issues_distinct":1,"open_prs":2,"closed_issues":3,"merged_prs":1,"attempt_prs":6},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":17904},"resolution":{"state":"partially_resolved","by":"pr-17904"},"freshness":{"state":"verified","last_verified_at":"2026-04-30T11:29:04+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.1469090909090909},"fix_edges":[{"issue_number":17426,"pr_number":17904,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true}],"members":[{"kind":"issue","number":17426,"title":"[Bug]: Model calls to google provider return 400 error with Invalid JSON payload on thinking_config","url":null,"state":"closed"},{"kind":"issue","number":25123,"title":"fix: Fallback to gemini-3-flash-preview fails with 400 (thinking_config)","url":null,"state":"open"},{"kind":"issue","number":26161,"title":"[Bug]: Gemini API Failed with 400 Bad Request","url":null,"state":"closed"},{"kind":"issue","number":59283,"title":"Gemini OpenAI-compat: extra_body.google.thinking_config double-nested (400, surfaced as generic empty response)","url":null,"state":"closed"},{"kind":"pr","number":17441,"title":"fix(transport): omit thinking_config for Gemma on the gemini provider (#17426)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"990eccdf6","number":0},{"kind":"commit","value":"5a61c116e","number":0},{"kind":"commit","value":"5a61c116e","number":0},{"kind":"pr","value":"#17334","number":17334},{"kind":"pr","value":"#17334","number":17334},{"kind":"pr","value":"#17334","number":17334},{"kind":"pr","value":"#17334","number":17334},{"kind":"pr","value":"#17569","number":17569},{"kind":"pr","value":"#17386","number":17386},{"kind":"pr","value":"#17348","number":17348}],"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":17904,"title":"fix(transport): omit thinking_config for Gemma on the gemini provider (#17426)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["agent/transports/chat_completions.py","tests/agent/transports/test_chat_completions.py"],"fix_hunks":[{"file":"agent/transports/chat_completions.py","hunks":["@@ -24,6 +24,18 @@ def _build_gemini_thinking_config(model: str, reasoning_config: dict | None) ->\n     if reasoning_config is None or not isinstance(reasoning_config, dict):\n         return None\n \n+    normalized_model = (model or \"\").strip().lower()\n+    if normalized_model.startswith(\"google/\"):\n+        normalized_model = normalized_model.split(\"/\", 1)[1]\n+\n+    # ``thinking_config`` is a Gemini-only request parameter. The same\n+    # ``gemini`` provider also serves Gemma (and historically PaLM/Bard);\n+    # those reject the field with HTTP 400 \"Unknown name 'thinking_config':\n+    # Cannot find field\" — including the polite ``{\"includeThoughts\": False}``\n+    # form. Omit the field entirely on non-Gemini models. (#17426)\n+    if not normalized_model.startswith(\"gemini\"):\n+        return None\n+\n     if reasoning_config.get(\"enabled\") is False:\n         # Gemini can hide thought parts even when internal thinking still\n         # happens; omit thinkingLevel to avoid model-specific validation quirks.","@@ -34,9 +46,6 @@ def _build_gemini_thinking_config(model: str, reasoning_config: dict | None) ->\n         return {\"includeThoughts\": False}\n \n     thinking_config: Dict[str, Any] = {\"includeThoughts\": True}\n-    normalized_model = (model or \"\").strip().lower()\n-    if normalized_model.startswith(\"google/\"):\n-        normalized_model = normalized_model.split(\"/\", 1)[1]\n \n     # Gemini 2.5 accepts thinkingBudget; don't guess a budget from Hermes'\n     # coarse effort levels. ``includeThoughts`` alone is enough to surface"]},{"file":"tests/agent/transports/test_chat_completions.py","hunks":["@@ -244,6 +244,46 @@ def test_gemini_flash_minimal_clamps_to_low(self, transport):\n             \"thinking_level\": \"low\",\n         }\n \n+    def test_gemma_does_not_receive_thinking_config(self, transport):\n+        # The `gemini` provider also serves Gemma (e.g. `gemma-4-31b-it`),\n+        # but Gemma rejects `thinking_config` with HTTP 400 (#17426). Even\n+        # when Hermes has reasoning enabled, the field must be omitted for\n+        # non-Gemini models on this provider.\n+        msgs = [{\"role\": \"user\", \"content\": \"Hi\"}]\n+        kw = transport.build_kwargs(\n+            model=\"gemma-4-31b-it\",\n+            messages=msgs,\n+            provider_name=\"gemini\",\n+            reasoning_config={\"enabled\": True, \"effort\": \"high\"},\n+        )\n+        assert \"thinking_config\" not in kw.get(\"extra_body\", {})\n+\n+    def test_gemma_disabled_reasoning_still_omits_thinking_config(self, transport):\n+        # The `Unknown name 'thinking_config': Cannot find field` rejection\n+        # fires even on `{\"includeThoughts\": False}` — the entire field must\n+        # be absent, not just disabled. (#17426)\n+        msgs = [{\"role\": \"user\", \"content\": \"Hi\"}]\n+        kw = transport.build_kwargs(\n+            model=\"gemma-4-31b-it\",\n+            messages=msgs,\n+            provider_name=\"gemini\",\n+            reasoning_config={\"enabled\": False},\n+        )\n+        assert \"thinking_config\" not in kw.get(\"extra_body\", {})\n+\n+    def test_google_prefixed_gemma_also_omits_thinking_config(self, transport):\n+        # OpenRouter-style `google/gemma-...` IDs hit the same provider path\n+        # and must also omit `thinking_config`. The existing `google/`\n+        # prefix-stripping must not accidentally classify Gemma as Gemini.\n+        msgs = [{\"role\": \"user\", \"content\": \"Hi\"}]\n+        kw = transport.build_kwargs(\n+            model=\"google/gemma-4-31b-it\",\n+            messages=msgs,\n+            provider_name=\"gemini\",\n+            reasoning_config={\"enabled\": True, \"effort\": \"medium\"},\n+        )\n+        assert \"thinking_config\" not in kw.get(\"extra_body\", {})\n+\n     def test_max_tokens_with_fn(self, transport):\n         msgs = [{\"role\": \"user\", \"content\": \"Hi\"}]\n         kw = transport.build_kwargs(\n"]}]},{"kind":"pr","number":25124,"title":"fix: exclude gemini-3-flash-preview from thinking_config injection","url":null,"state":"open","close_reason":{"refs":[{"kind":"pr","value":"#25123","number":25123},{"kind":"pr","value":"#25123","number":25123}],"category":"wontfix_bydesign","implication":"wontfix"},"merged":false},{"kind":"pr","number":32353,"title":"fix(agent): exclude gemini-3-flash-preview from thinkingLevel injection (#25123)","url":null,"state":"closed","close_reason":{"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":59502,"title":"fix(gemini): remove double-nested extra_body in thinking_config builder","url":null,"state":"closed","close_reason":{"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":59504,"title":"fix(agent): merge extra_body into request debug dumps so they match t…","url":null,"state":"open","close_reason":{"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":59699,"title":"fix(gemini): fix double-nested thinking_config in extra_body (#59283)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#59502","number":59502},{"kind":"pr","value":"#59283","number":59283},{"kind":"pr","value":"#59502","number":59502},{"kind":"pr","value":"#59475","number":59475},{"kind":"pr","value":"#59283","number":59283}],"category":"duplicate_of","implication":"duplicate"},"merged":false}],"trust":{"default_tier":"verified","includes_suggested":false}}