From ba44a3d256848391af2370c3c66788fe0a48e2de Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Fri, 24 Apr 2026 05:35:17 -0700 Subject: [PATCH] fix(gemini): fail fast on missing API key + surface it in hermes dump (#15133) Two small fixes triggered by a support report where the user saw a cryptic 'HTTP 400 - Error 400 (Bad Request)!!1' (Google's GFE HTML error page, not a real API error) on every gemini-2.5-pro request. The underlying cause was an empty GOOGLE_API_KEY / GEMINI_API_KEY, but nothing in our output made that diagnosable: 1. hermes_cli/dump.py: the api_keys section enumerated 23 providers but omitted Google entirely, so users had no way to verify from 'hermes dump' whether the key was set. Added GOOGLE_API_KEY and GEMINI_API_KEY rows. 2. agent/gemini_native_adapter.py: GeminiNativeClient.__init__ accepted an empty/whitespace api_key and stamped it into the x-goog-api-key header, which made Google's frontend return a generic HTML 400 long before the request reached the Generative Language backend. Now we raise RuntimeError at construction with an actionable message pointing at GOOGLE_API_KEY/GEMINI_API_KEY and aistudio.google.com. Added a regression test that covers '', ' ', and None. --- agent/gemini_native_adapter.py | 7 +++++++ hermes_cli/dump.py | 2 ++ tests/agent/test_gemini_native_adapter.py | 13 +++++++++++++ 3 files changed, 22 insertions(+) diff --git a/agent/gemini_native_adapter.py b/agent/gemini_native_adapter.py index 9178fb5a4..5f64636f2 100644 --- a/agent/gemini_native_adapter.py +++ b/agent/gemini_native_adapter.py @@ -801,6 +801,13 @@ class GeminiNativeClient: http_client: Optional[httpx.Client] = None, **_: Any, ) -> None: + if not (api_key or "").strip(): + raise RuntimeError( + "Gemini native client requires an API key, but none was provided. " + "Set GOOGLE_API_KEY or GEMINI_API_KEY in your environment / ~/.hermes/.env " + "(get one at https://aistudio.google.com/app/apikey), or run `hermes setup` " + "to configure the Google provider." + ) self.api_key = api_key normalized_base = (base_url or DEFAULT_GEMINI_BASE_URL).rstrip("/") if normalized_base.endswith("/openai"): diff --git a/hermes_cli/dump.py b/hermes_cli/dump.py index 90364a261..3d7280244 100644 --- a/hermes_cli/dump.py +++ b/hermes_cli/dump.py @@ -267,6 +267,8 @@ def run_dump(args): ("ANTHROPIC_API_KEY", "anthropic"), ("ANTHROPIC_TOKEN", "anthropic_token"), ("NOUS_API_KEY", "nous"), + ("GOOGLE_API_KEY", "google/gemini"), + ("GEMINI_API_KEY", "gemini"), ("GLM_API_KEY", "glm/zai"), ("ZAI_API_KEY", "zai"), ("KIMI_API_KEY", "kimi"), diff --git a/tests/agent/test_gemini_native_adapter.py b/tests/agent/test_gemini_native_adapter.py index a36b1e71c..4b066b4f4 100644 --- a/tests/agent/test_gemini_native_adapter.py +++ b/tests/agent/test_gemini_native_adapter.py @@ -234,6 +234,19 @@ def test_native_client_accepts_injected_http_client(): assert client._http is injected +def test_native_client_rejects_empty_api_key_with_actionable_message(): + """Empty/whitespace api_key must raise at construction, not produce a cryptic + Google GFE 'Error 400 (Bad Request)!!1' HTML page on the first request.""" + from agent.gemini_native_adapter import GeminiNativeClient + + for bad in ("", " ", None): + with pytest.raises(RuntimeError) as excinfo: + GeminiNativeClient(api_key=bad) # type: ignore[arg-type] + msg = str(excinfo.value) + assert "GOOGLE_API_KEY" in msg and "GEMINI_API_KEY" in msg + assert "aistudio.google.com" in msg + + @pytest.mark.asyncio async def test_async_native_client_streams_without_requiring_async_iterator_from_sync_client(): from agent.gemini_native_adapter import AsyncGeminiNativeClient