NousResearch/hermes-agent

Gemini thinking_config malformed request causing 400 errors

Cluster 17426 · agent. Data as of

3 open items. Fix merged in #17904. Check it covers your case.

The short answer

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

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.

Open issues1

  • Issue#25123fix: Fallback to gemini-3-flash-preview fails with 400 (thinking_config)

Open PRs2

  • PR#25124fix: exclude gemini-3-flash-preview from thinking_config injection
  • PR#59504fix(agent): merge extra_body into request debug dumps so they match t…

Fixes on record

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

  • #17904 fix(transport): omit thinking_config for Gemma on the gemini provider (#17426)

    merged to main

    files: agent/transports/chat_completions.py, tests/agent/transports/test_chat_completions.py

    Show the diff
    • agent/transports/chat_completions.py
      @@ -24,6 +24,18 @@ def _build_gemini_thinking_config(model: str, reasoning_config: dict | None) ->
           if reasoning_config is None or not isinstance(reasoning_config, dict):
               return None
       
      +    normalized_model = (model or "").strip().lower()
      +    if normalized_model.startswith("google/"):
      +        normalized_model = normalized_model.split("/", 1)[1]
      +
      +    # ``thinking_config`` is a Gemini-only request parameter. The same
      +    # ``gemini`` provider also serves Gemma (and historically PaLM/Bard);
      +    # those reject the field with HTTP 400 "Unknown name 'thinking_config':
      +    # Cannot find field" — including the polite ``{"includeThoughts": False}``
      +    # form. Omit the field entirely on non-Gemini models. (#17426)
      +    if not normalized_model.startswith("gemini"):
      +        return None
      +
           if reasoning_config.get("enabled") is False:
               # Gemini can hide thought parts even when internal thinking still
               # happens; omit thinkingLevel to avoid model-specific validation quirks.
      @@ -34,9 +46,6 @@ def _build_gemini_thinking_config(model: str, reasoning_config: dict | None) ->
               return {"includeThoughts": False}
       
           thinking_config: Dict[str, Any] = {"includeThoughts": True}
      -    normalized_model = (model or "").strip().lower()
      -    if normalized_model.startswith("google/"):
      -        normalized_model = normalized_model.split("/", 1)[1]
       
           # Gemini 2.5 accepts thinkingBudget; don't guess a budget from Hermes'
           # coarse effort levels. ``includeThoughts`` alone is enough to surface
    • tests/agent/transports/test_chat_completions.py
      @@ -244,6 +244,46 @@ def test_gemini_flash_minimal_clamps_to_low(self, transport):
                   "thinking_level": "low",
               }
       
      +    def test_gemma_does_not_receive_thinking_config(self, transport):
      +        # The `gemini` provider also serves Gemma (e.g. `gemma-4-31b-it`),
      +        # but Gemma rejects `thinking_config` with HTTP 400 (#17426). Even
      +        # when Hermes has reasoning enabled, the field must be omitted for
      +        # non-Gemini models on this provider.
      +        msgs = [{"role": "user", "content": "Hi"}]
      +        kw = transport.build_kwargs(
      +            model="gemma-4-31b-it",
      +            messages=msgs,
      +            provider_name="gemini",
      +            reasoning_config={"enabled": True, "effort": "high"},
      +        )
      +        assert "thinking_config" not in kw.get("extra_body", {})
      +
      +    def test_gemma_disabled_reasoning_still_omits_thinking_config(self, transport):
      +        # The `Unknown name 'thinking_config': Cannot find field` rejection
      +        # fires even on `{"includeThoughts": False}` — the entire field must
      +        # be absent, not just disabled. (#17426)
      +        msgs = [{"role": "user", "content": "Hi"}]
      +        kw = transport.build_kwargs(
      +            model="gemma-4-31b-it",
      +            messages=msgs,
      +            provider_name="gemini",
      +            reasoning_config={"enabled": False},
      +        )
      +        assert "thinking_config" not in kw.get("extra_body", {})
      +
      +    def test_google_prefixed_gemma_also_omits_thinking_config(self, transport):
      +        # OpenRouter-style `google/gemma-...` IDs hit the same provider path
      +        # and must also omit `thinking_config`. The existing `google/`
      +        # prefix-stripping must not accidentally classify Gemma as Gemini.
      +        msgs = [{"role": "user", "content": "Hi"}]
      +        kw = transport.build_kwargs(
      +            model="google/gemma-4-31b-it",
      +            messages=msgs,
      +            provider_name="gemini",
      +            reasoning_config={"enabled": True, "effort": "medium"},
      +        )
      +        assert "thinking_config" not in kw.get("extra_body", {})
      +
           def test_max_tokens_with_fn(self, transport):
               msgs = [{"role": "user", "content": "Hi"}]
               kw = transport.build_kwargs(

4 PRs closed without merging

From the close comments: done elsewhere 1 · duplicate 1 · superseded by another PR 1 · other 1

  • #17441 fix(transport): omit thinking_config for Gemma on the gemini provider (#17426) · other → 990eccd, 5a61c11, #17334, #17569, #17386, #17348
  • #32353 fix(agent): exclude gemini-3-flash-preview from thinkingLevel injection (#25123) · done elsewhere
  • #59502 fix(gemini): remove double-nested extra_body in thinking_config builder · superseded by another PR
  • #59699 fix(gemini): fix double-nested thinking_config in extra_body (#59283) · duplicate → #59502, #59283, #59475
Closed issues · 3
  • Closed#17426[Bug]: Model calls to google provider return 400 error with Invalid JSON payload on thinking_config
  • Closed#26161[Bug]: Gemini API Failed with 400 Bad Request
  • Closed#59283Gemini OpenAI-compat: extra_body.google.thinking_config double-nested (400, surfaced as generic empty response)
How this cluster was grouped
  • Link evidence (partly structural links): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
  • Members outside that structural core may belong to a different underlying problem.
  • Pairwise link density 0.15: 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
#17904
Closed issues
3
Attempts
4 PRs closed without merging
Fix link checked
30 Apr 2026

For your agent

The same facts, typed.