NousResearch/hermes-agent

Gemini and gateway auth header routing fixes

Cluster 3717 · gateway. Data as of

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

The short answer

1open issue
2open PRs
9items grouped
Known?
Yes. 9 related items grouped here, 3 issues already closed.
Fix event
A fix was merged to main in PR #12251. 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 2 PRs.

Multiple bugs and fixes related to authentication headers being incorrectly sent or omitted for Gemini and custom OpenAI-compatible gateway endpoints, causing 400, 403, and 404 errors. Includes related work on API key routing, bad model filtering, and provider config shadowing warnings.

Open issues1

  • Issue#43026Bug: Gemini OpenAI-compatible provider returns HTTP 400/404 via internal HTTP client

Open PRs2

  • PR#20642fix: harden gateway SMS opt-out and Gemini doctor probe
  • PR#43178fix(cli): warn when provider config entries shadow a built-in provider

Fixes on record

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

  • #12251 fix(gemini): hide bad Google models and restore bearer auth

    merged to main

    files: agent/auxiliary_client.py, agent/models_dev.py, hermes_cli/models.py, hermes_cli/setup.py, run_agent.py, tests/hermes_cli/test_gemini_provider.py

    Show the diff
    • agent/auxiliary_client.py
      @@ -782,15 +782,6 @@ def _resolve_api_key_provider() -> Tuple[Optional[OpenAI], Optional[str]]:
                       from hermes_cli.models import copilot_default_headers
       
                       extra["default_headers"] = copilot_default_headers()
      -            elif "generativelanguage.googleapis.com" in base_url.lower():
      -                # Google's OpenAI-compatible endpoint only accepts x-goog-api-key.
      -                # Passing api_key= causes the SDK to inject Authorization: Bearer,
      -                # which Google rejects with HTTP 400 "Multiple authentication
      -                # credentials received". Use a placeholder for api_key and pass
      -                # the real key via x-goog-api-key header instead.
      -                # Fixes: https://github.com/NousResearch/hermes-agent/issues/7893
      -                extra["default_headers"] = {"x-goog-api-key": api_key}
      -                api_key = "[redacted:secret-assignment]"
                   return OpenAI(api_key=[redacted:secret-assignment] base_url=base_url, **extra), model
       
               creds = resolve_api_key_provider_credentials(provider_id)
      @@ -812,15 +803,6 @@ def _resolve_api_key_provider() -> Tuple[Optional[OpenAI], Optional[str]]:
                   from hermes_cli.models import copilot_default_headers
       
                   extra["default_headers"] = copilot_default_headers()
      -        elif "generativelanguage.googleapis.com" in base_url.lower():
      -            # Google's OpenAI-compatible endpoint only accepts x-goog-api-key.
      -            # Passing api_key= causes the SDK to inject Authorization: Bearer,
      -            # which Google rejects with HTTP 400 "Multiple authentication
      -            # credentials received". Use a placeholder for api_key and pass
      -            # the real key via x-goog-api-key header instead.
      -            # Fixes: https://github.com/NousResearch/hermes-agent/issues/7893
      -            extra["default_headers"] = {"x-goog-api-key": api_key}
      -            api_key = "[redacted:secret-assignment]"
               return OpenAI(api_key=[redacted:secret-assignment] base_url=base_url, **extra), model
       
           return None, None
      @@ -1666,16 +1648,6 @@ def _wrap_if_needed(client_obj, final_model_str: str, base_url_str: str = ""):
                   from hermes_cli.models import copilot_default_headers
       
                   headers.update(copilot_default_headers())
      -        elif "generativelanguage.googleapis.com" in base_url.lower():
      -            # Google's OpenAI-compatible endpoint only accepts x-goog-api-key.
      -            # Passing api_key= causes the OpenAI SDK to inject Authorization: Bearer,
      -            # which Google rejects with HTTP 400 "Multiple authentication credentials
      -            # received". Use a placeholder for api_key and pass the real key via
      -            # x-goog-api-key header instead.
      -            # Fixes: https://github.com/NousResearch/hermes-agent/issues/7893
      -            headers["x-goog-api-key"] = api_key
      -            api_key = "[redacted:secret-assignment]"
      -
               client = OpenAI(api_key=[redacted:secret-assignment] base_url=base_url,
                               **({"default_headers": headers} if headers else {}))
       
    • agent/models_dev.py
      @@ -420,7 +420,10 @@ def list_provider_models(provider: str) -> List[str]:
           models = _get_provider_models(provider)
           if models is None:
               return []
      -    return list(models.keys())
      +    return [
      +        mid for mid in models.keys()
      +        if not _should_hide_from_provider_catalog(provider, mid)
      +    ]
       
       
       # Patterns that indicate non-agentic or noise models (TTS, embedding,
      @@ -432,6 +435,43 @@ def list_provider_models(provider: str) -> List[str]:
           re.IGNORECASE,
       )
       
      +# Google's live Gemini catalogs currently include a mix of stale slugs and
      +# Gemma models whose TPM quotas are too small for normal Hermes agent traffic.
      +# Keep capability metadata available for direct/manual use, but hide these from
      +# the Gemini model catalogs we surface in setup and model selection.
      +_GOOGLE_HIDDEN_MODELS = frozenset({
      +    # Low-TPM Gemma models that trip Google input-token quota walls under
      +    # agent-style traffic despite advertising large context windows.
      +    "gemma-4-31b-it",
      +    "gemma-4-26b-it",
      +    "gemma-4-26b-a4b-it",
      +    "gemma-3-1b",
      +    "gemma-3-1b-it",
      +    "gemma-3-2b",
      +    "gemma-3-2b-it",
      +    "gemma-3-4b",
      +    "gemma-3-4b-it",
      +    "gemma-3-12b",
      +    "gemma-3-12b-it",
      +    "gemma-3-27b",
      +    "gemma-3-27b-it",
      +    # Stale/retired Google slugs that still surface through models.dev-backed
      +    # Gemini selection but 404 on the current Google endpoints.
      +    "gemini-1.5-flash",
      +    "gemini-1.5-pro",
      +    "gemini-1.5-flash-8b",
      +    "gemini-2.0-flash",
      +    "gemini-2.0-flash-lite",
      +})
      +
      +
      +def _should_hide_from_provider_catalog(provider: str, model_id: str) -> bool:
      +    provider_lower = (provider or "").strip().lower()
      +    model_lower = (model_id or "").strip().lower()
      +    if provider_lower in {"gemini", "google"} and model_lower in _GOOGLE_HIDDEN_MODELS:
      +        return True
      +    return False
      +
       
       def list_agentic_models(provider: str) -> List[str]:
           """Return model IDs suitable for agentic use from models.dev.
      @@ -448,6 +488,8 @@ def list_agentic_models(provider: str) -> List[str]:
           for mid, entry in models.items():
               if not isinstance(entry, dict):
                   continue
      +        if _should_hide_from_provider_catalog(provider, mid):
      +            continue
               if not entry.get("tool_call", False):
                   continue
               if _NOISE_PATTERNS.search(mid):
      @@ -582,5 +624,3 @@ def get_model_info(
                   return _parse_model_info(mid, mdata, mdev_id)
       
           return None
      -
      -
    • hermes_cli/models.py
      @@ -133,8 +133,6 @@ def _codex_curated_models() -> list[str]:
               "gemini-2.5-pro",
               "gemini-2.5-flash",
               "gemini-2.5-flash-lite",
      -        # Gemma open models (also served via AI Studio)
      -        "gemma-4-31b-it",
           ],
           "google-gemini-cli": [
               "gemini-2.5-pro",
    • hermes_cli/setup.py
      @@ -91,7 +91,6 @@ def _supports_same_provider_pool_setup(provider: str) -> bool:
           "gemini": [
               "gemini-3.1-pro-preview", "gemini-3-flash-preview", "gemini-3.1-flash-lite-preview",
               "gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.5-flash-lite",
      -        "gemma-4-31b-it",
           ],
           "zai": ["glm-5.1", "glm-5", "glm-4.7", "glm-4.5", "glm-4.5-flash"],
           "kimi-coding": ["kimi-k2.5", "kimi-k2-thinking", "kimi-k2-turbo-preview"],
    • run_agent.py
      @@ -1054,16 +1054,6 @@ def __init__(
                           }
                       elif "portal.qwen.ai" in effective_base.lower():
                           client_kwargs["default_headers"] = _qwen_portal_headers()
      -                elif "generativelanguage.googleapis.com" in effective_base.lower():
      -                    # Google's OpenAI-compatible endpoint only accepts x-goog-api-key.
      -                    # The OpenAI SDK auto-injects Authorization: Bearer when api_key= is
      -                    # set to a real value, causing HTTP 400 "Multiple authentication
      -                    # credentials received".  Pass a placeholder so the SDK does not
      -                    # emit Bearer, and carry the real key via x-goog-api-key instead.
      -                    # Fixes: https://github.com/NousResearch/hermes-agent/issues/7893
      -                    real_key = client_kwargs["api_key"]
      -                    client_kwargs["api_key"] = "not-used"
      -                    client_kwargs["default_headers"] = {"x-goog-api-key": real_key}
                   else:
                       # No explicit creds — use the centralized provider router
                       from agent.auxiliary_client import resolve_provider_client
      @@ -5245,17 +5235,6 @@ def _apply_client_headers_for_base_url(self, base_url: str) -> None:
                   self._client_kwargs["default_headers"] = {"User-Agent": "KimiCLI/1.30.0"}
               elif "portal.qwen.ai" in normalized:
                   self._client_kwargs["default_headers"] = _qwen_portal_headers()
      -        elif "generativelanguage.googleapis.com" in normalized:
      -            # Google's endpoint rejects Bearer tokens; use x-goog-api-key instead.
      -            # Swap the real key out of api_key and into the header so the OpenAI
      -            # SDK does not emit Authorization: Bearer.
      -            # Fixes: https://github.com/NousResearch/hermes-agent/issues/7893
      -            real_key = self._client_kwargs.get("api_key", "")
      -            if real_key and real_key != "not-used":
      -                self._client_kwargs["api_key"] = "not-used"
      -            self._client_kwargs["default_headers"] = {
      -                "x-goog-api-key": real_key or self._client_kwargs.get("api_key", ""),
      -            }
               else:
                   self._client_kwargs.pop("default_headers", None)
       
    • tests/hermes_cli/test_gemini_provider.py
      @@ -130,7 +130,7 @@ def test_provider_models_exist(self):
               models = _PROVIDER_MODELS["gemini"]
               assert "gemini-2.5-pro" in models
               assert "gemini-2.5-flash" in models
      -        assert "gemma-4-31b-it" in models
      +        assert "gemma-4-31b-it" not in models
       
           def test_provider_models_has_3x(self):
               models = _PROVIDER_MODELS["gemini"]
      @@ -207,14 +207,8 @@ def test_gemini_agent_uses_chat_completions(self, monkeypatch):
                   assert agent.api_mode == "chat_completions"
                   assert agent.provider == "gemini"
       
      -    def test_gemini_uses_x_goog_api_key_not_bearer(self, monkeypatch):
      -        """Regression test for issue #7893.
      -
      -        When provider=gemini, the OpenAI client must be constructed with
      -        api_key='[redacted:secret-assignment]' and default_headers={'x-goog-api-key': real_key}.
      -        This prevents the SDK from injecting Authorization: Bearer, which
      -        Google's endpoint rejects with HTTP 400.
      -        """
      +    def test_gemini_uses_bearer_auth(self, monkeypatch):
      +        """Gemini OpenAI-compatible endpoint should receive the real API key."""
               monkeypatch.setenv("GOOGLE_API_KEY", "AIzaSy_REAL_KEY")
               real_key = "AIzaSy_REAL_KEY"
               with patch("run_agent.OpenAI") as mock_openai:
      @@ -227,37 +221,22 @@ def test_gemini_uses_x_goog_api_key_not_bearer(self, monkeypatch):
                       base_url="https://generativelanguage.googleapis.com/v1beta/openai",
                   )
               call_kwargs = mock_openai.call_args[1]
      -        # The SDK must NOT receive the real key as api_key (which would emit Bearer)
      -        assert call_kwargs.get("api_key") == "not-used", (
      -            "api_key must be 'not-used' to suppress Authorization: Bearer for Gemini"
      -        )
      -        # The real key must be in x-goog-api-key header
      +        assert call_kwargs.get("api_key") == real_key
               headers = call_kwargs.get("default_headers", {})
      -        assert headers.get("x-goog-api-key") == real_key, (
      -            "x-goog-api-key header must carry the real Gemini API key"
      -        )
      +        assert "x-goog-api-key" not in headers
       
           def test_gemini_resolve_provider_client_auth(self, monkeypatch):
      -        """Regression test for issue #7893 — resolve_provider_client path.
      -
      -        When resolve_provider_client('gemini') is called, the returned OpenAI
      -        client must use x-goog-api-key header, not Authorization: Bearer.
      -        """
      +        """resolve_provider_client('gemini') should pass the real API key through."""
               monkeypatch.setenv("GEMINI_API_KEY", "AIzaSy_TEST_KEY")
               real_key = "AIzaSy_TEST_KEY"
               with patch("agent.auxiliary_client.OpenAI") as mock_openai:
                   mock_openai.return_value = MagicMock()
      -            mock_openai.return_value.api_key = "[redacted:secret-assignment]"
                   from agent.auxiliary_client import resolve_provider_client
                   resolve_provider_client("gemini")
               call_kwargs = mock_openai.call_args[1]
      -        assert call_kwargs.get("api_key") == "not-used", (
      -            "api_key must be 'not-used' to prevent Bearer injection for Gemini"
      -        )
      +        assert call_kwargs.get("api_key") == real_key
               headers = call_kwargs.get("default_headers", {})
      -        assert headers.get("x-goog-api-key") == real_key, (
      -            "x-goog-api-key header must carry the real Gemini API key"
      -        )
      +        assert "x-goog-api-key" not in headers
       
       
       # ── models.dev Integration ──
      @@ -313,9 +292,32 @@ def test_list_agentic_models_with_mock_data(self):
                   result = list_agentic_models("gemini")
               assert "gemini-3-flash-preview" in result
               assert "gemini-2.5-pro" in result
      -        assert "gemma-4-31b-it" in result
      +        assert "gemma-4-31b-it" not in result
               # Filtered out:
               assert "gemini-embedding-001" not in result      # no tool_call
               assert "gemini-2.5-flash-preview-tts" not in result  # no tool_call
               assert "gemini-live-2.5-flash" not in result     # noise: live-
               assert "gemini-2.5-flash-preview-04-17" not in result  # noise: dated preview
      +
      +    def test_list_provider_models_hides_low_tpm_google_gemmas(self):
      +        mock_data = {
      +            "google": {
      +                "models": {
      +                    "gemini-2.5-pro": {},
      +                    "gemma-4-31b-it": {},
      +                    "gemma-3-27b-it": {},
      +                    "gemini-1.5-pro": {},
      +                    "gemini-2.0-flash": {},
      +                }
      +            }
      +        }
      +        with patch("agent.models_dev.fetch_models_dev", return_value=mock_data):
      +            from agent.models_dev import list_provider_models
      +
      +            result = list_provider_models("gemini")
      +
      +        assert "gemini-2.5-pro" in result
      +        assert "gemma-4-31b-it" not in result
      +        assert "gemma-3-27b-it" not in result
      +        assert "gemini-1.5-pro" not in result
      +        assert "gemini-2.0-flash" not in result

2 PRs closed without merging

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

  • #3720 fix(gateway): resolve 403 on custom OpenAI endpoints by applying prov… · done elsewhere → #3717
  • #12204 Fix gemini auth header · superseded by another PR → #12251, #11961, #12127, #12168
Closed issues · 3
  • Closed#3717[Bug]: Hermes Gateway Returns 403 on OpenAI-Compatible Endpoints (Bearer Token Not Sent)
  • Closed#12127[Setup]: [Bug] HTTP 400 API_KEY_INVALID with Google Provider on the latest version (Worked fine in previous version)
  • Closed#12168[Bug]: Getting invalid key for gemini
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.18: 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
gateway
Merged PRs
#12251
Closed issues
3
Attempts
2 PRs closed without merging
Fix link checked
18 Apr 2026. This confirmation is old and may be out of date (a revert or reopen would not have invalidated it here); re-verify.

For your agent

The same facts, typed.