fix(gemini): route Vertex express keys to aiplatform in doctor and dashboard key probes

hermes doctor and the dashboard GEMINI_API_KEY validator still sent AQ. express
keys to generativelanguage.googleapis.com, which always 403s, so a working key
was reported as rejected while chat succeeded. Both probe URLs now go through
normalize_gemini_base_url(base, api_key), the same routing the chat client uses.
This commit is contained in:
teknium1
2026-09-18 03:03:50 -07:00
committed by Teknium
parent 08339bf167
commit df53cae72d
3 changed files with 67 additions and 0 deletions

View File

@@ -210,6 +210,9 @@ def _apikey_request(key: str, base_env, default_url) -> tuple:
# Google's Generative Language API rejects ``Authorization: Bearer <api-key>`` with 401
# ACCESS_TOKEN_TYPE_UNSUPPORTED (reserved for OAuth 2 tokens); plain keys use ``x-goog-api-key``.
if url and base_url_host_matches(url, "generativelanguage.googleapis.com"):
from agent.gemini_native_adapter import normalize_gemini_base_url
# A Vertex express key (AQ.) can only 403 on the Studio host; normalize routes it to aiplatform.
url = normalize_gemini_base_url(url.rsplit("/models", 1)[0], key) + "/models"
headers.pop("Authorization", None)
headers["x-goog-api-key"] = key
return base, url, headers

View File

@@ -736,6 +736,10 @@ async def validate_provider_credential(body: EnvVarUpdate, request: Request):
return {"ok": True, "reachable": False, "message": ""}
url, auth = probe
if key == "GEMINI_API_KEY":
from agent.gemini_native_adapter import normalize_gemini_base_url
# A Vertex express key (AQ.) can only 403 on the Studio host; normalize routes it to aiplatform.
url = normalize_gemini_base_url(url.rsplit("/models", 1)[0], value) + "/models"
headers = {"Accept": "application/json"}
params = {}
if auth == "bearer":

View File

@@ -0,0 +1,60 @@
"""Key-validation surfaces route Vertex AI express keys (``AQ.``) to aiplatform, matching chat (#114335).
Sending an express key to generativelanguage.googleapis.com always 403s, so ``hermes doctor`` and the
dashboard key test would report a working key as rejected while chat succeeded.
"""
from __future__ import annotations
import asyncio
from agent.gemini_native_adapter import VERTEX_EXPRESS_BASE_URL
_STUDIO_MODELS = "https://generativelanguage.googleapis.com/v1beta/models"
def test_doctor_gemini_probe_routes_express_key_to_aiplatform():
from hermes_cli.doctor_connectivity import _apikey_request
_, url, headers = _apikey_request("AQ.express-key", None, _STUDIO_MODELS)
assert url == VERTEX_EXPRESS_BASE_URL + "/models"
assert headers["x-goog-api-key"] == "AQ.express-key" and "Authorization" not in headers
# AI Studio keys keep hitting the Studio host.
assert _apikey_request("AIza-studio-key", None, _STUDIO_MODELS)[1] == _STUDIO_MODELS
def test_dashboard_gemini_key_probe_routes_express_key_to_aiplatform(monkeypatch):
import hermes_cli.web_routers.config_env as mod
from hermes_cli.web_models import EnvVarUpdate
seen = {}
class _Resp:
status_code = 200
is_success = True
class _Client:
def __init__(self, *a, **k):
pass
async def __aenter__(self):
return self
async def __aexit__(self, *a):
return False
async def get(self, url, **k):
seen["url"] = url
return _Resp()
import httpx
monkeypatch.setattr(httpx, "AsyncClient", _Client)
monkeypatch.setattr(mod, "_require_token", lambda request: None)
body = EnvVarUpdate(key="GEMINI_API_KEY", value="AQ.express-key")
out = asyncio.run(mod.validate_provider_credential(body, request=None)) # type: ignore[arg-type]
assert out["ok"] is True
assert seen["url"] == VERTEX_EXPRESS_BASE_URL + "/models"