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
- 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.
Next step: read #12251, then check whether your report is one of the 1 still open.
Read #12251Pick up a Lab taskMultiple 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
Fixes on record
-
#12251 fix(gemini): hide bad Google models and restore bearer auth
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.pyShow 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
Closed issues · 3
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.00means 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 LabAt 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.