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.
This commit is contained in:
Teknium 2026-04-24 05:35:17 -07:00 committed by GitHub
parent a1caec1088
commit ba44a3d256
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 22 additions and 0 deletions

View file

@ -801,6 +801,13 @@ class GeminiNativeClient:
http_client: Optional[httpx.Client] = None, http_client: Optional[httpx.Client] = None,
**_: Any, **_: Any,
) -> None: ) -> 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 self.api_key = api_key
normalized_base = (base_url or DEFAULT_GEMINI_BASE_URL).rstrip("/") normalized_base = (base_url or DEFAULT_GEMINI_BASE_URL).rstrip("/")
if normalized_base.endswith("/openai"): if normalized_base.endswith("/openai"):

View file

@ -267,6 +267,8 @@ def run_dump(args):
("ANTHROPIC_API_KEY", "anthropic"), ("ANTHROPIC_API_KEY", "anthropic"),
("ANTHROPIC_TOKEN", "anthropic_token"), ("ANTHROPIC_TOKEN", "anthropic_token"),
("NOUS_API_KEY", "nous"), ("NOUS_API_KEY", "nous"),
("GOOGLE_API_KEY", "google/gemini"),
("GEMINI_API_KEY", "gemini"),
("GLM_API_KEY", "glm/zai"), ("GLM_API_KEY", "glm/zai"),
("ZAI_API_KEY", "zai"), ("ZAI_API_KEY", "zai"),
("KIMI_API_KEY", "kimi"), ("KIMI_API_KEY", "kimi"),

View file

@ -234,6 +234,19 @@ def test_native_client_accepts_injected_http_client():
assert client._http is injected 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 @pytest.mark.asyncio
async def test_async_native_client_streams_without_requiring_async_iterator_from_sync_client(): async def test_async_native_client_streams_without_requiring_async_iterator_from_sync_client():
from agent.gemini_native_adapter import AsyncGeminiNativeClient from agent.gemini_native_adapter import AsyncGeminiNativeClient