Vertex: countTokens verify, global-location host, honest region help

Model listing 403/404s under plain ADC; countTokens is free and proves
project+location+API in one call. Verified live: Gemini (global), Qwen MaaS (us-south1).
This commit is contained in:
Rohit C Prasad
2026-07-27 12:58:16 -07:00
committed by Rohit P
parent f281b29ff1
commit 33d3efd3b2
3 changed files with 51 additions and 16 deletions
+24 -10
View File
@@ -400,9 +400,10 @@ DESCRIPTORS: list[ProviderDescriptor] = [
"location", "location",
"Location", "Location",
secret=False, secret=False,
placeholder="us-east5", placeholder="global",
help="The region your Vertex AI models are enabled in " help="Use `global` for the newest Gemini and Claude models. Some models "
"(Claude models: us-east5 or europe-west1).", "are regional — Model Garden lists each (Claude also: us-east5 / "
"europe-west1; Qwen3 Coder: us-south1).",
), ),
ProviderField( ProviderField(
"auth_method", "auth_method",
@@ -695,9 +696,16 @@ def _verify_bedrock(fields: dict[str, Any], timeout: float) -> dict[str, Any]:
return {"ok": True} return {"ok": True}
# Verify probe: countTokens on a stable Gemini model — free (no generation), works with
# plain ADC (the model list/GET endpoints 403/404 under user credentials — checked live
# 2026-07-26), and exercises project + location + API enablement in one call.
_VERTEX_PROBE_MODEL = "gemini-2.5-flash"
_VERTEX_PROBE_BODY = {"contents": [{"role": "user", "parts": [{"text": "hi"}]}]}
def _verify_vertex(fields: dict[str, Any], timeout: float) -> dict[str, Any]: def _verify_vertex(fields: dict[str, Any], timeout: float) -> dict[str, Any]:
"""One cheap read-only call (list Google's publisher models) through the SELECTED """One cheap call (countTokens) through the SELECTED auth method: ADC /
auth method: ADC / service-account bearer, or the express API key header.""" service-account bearer, or the express API key header."""
import httpx import httpx
from .vertex_provider import load_credentials from .vertex_provider import load_credentials
@@ -713,9 +721,11 @@ def _verify_vertex(fields: dict[str, Any], timeout: float) -> dict[str, Any]:
return {"ok": False, "error": "Enter a Vertex API key to test."} return {"ok": False, "error": "Enter a Vertex API key to test."}
try: try:
# Express mode is global — no region host, no project in the path. # Express mode is global — no region host, no project in the path.
resp = httpx.get( resp = httpx.post(
"https://aiplatform.googleapis.com/v1/publishers/google/models", "https://aiplatform.googleapis.com/v1/publishers/google/models/"
f"{_VERTEX_PROBE_MODEL}:countTokens",
headers={"x-goog-api-key": key}, headers={"x-goog-api-key": key},
json=_VERTEX_PROBE_BODY,
timeout=timeout, timeout=timeout,
) )
except Exception as exc: except Exception as exc:
@@ -754,11 +764,15 @@ def _verify_vertex(fields: dict[str, Any], timeout: float) -> dict[str, Any]:
if kind in ("RefreshError", "MalformedError", "JSONDecodeError", "ValueError"): if kind in ("RefreshError", "MalformedError", "JSONDecodeError", "ValueError"):
return {"ok": False, "error": "Google rejected the credentials."} return {"ok": False, "error": "Google rejected the credentials."}
return {"ok": False, "error": f"Couldn't load Google credentials ({kind})."} return {"ok": False, "error": f"Couldn't load Google credentials ({kind})."}
from .vertex_provider import _regional_host
try: try:
resp = httpx.get( resp = httpx.post(
f"https://{location}-aiplatform.googleapis.com/v1/projects/{project}" f"https://{_regional_host(location)}/v1/projects/{project}"
f"/locations/{location}/publishers/google/models", f"/locations/{location}/publishers/google/models/"
f"{_VERTEX_PROBE_MODEL}:countTokens",
headers={"Authorization": f"Bearer {creds.token}"}, headers={"Authorization": f"Bearer {creds.token}"},
json=_VERTEX_PROBE_BODY,
timeout=timeout, timeout=timeout,
) )
except Exception as exc: except Exception as exc:
+9 -1
View File
@@ -46,6 +46,14 @@ _SCOPES = ["https://www.googleapis.com/auth/cloud-platform"]
_FAMILIES = ("gemini", "claude", "openweight") _FAMILIES = ("gemini", "claude", "openweight")
def _regional_host(location: Optional[str]) -> str:
"""Vertex REST host for a location — `global` (newer Gemini models) has no region
prefix (checked live 2026-07-26)."""
if not location or location == "global":
return "aiplatform.googleapis.com"
return f"{location}-aiplatform.googleapis.com"
def load_credentials(service_account_json: Optional[str]) -> Any: def load_credentials(service_account_json: Optional[str]) -> Any:
"""Explicit service-account JSON (content or path) → Credentials; blank → None (the """Explicit service-account JSON (content or path) → Credentials; blank → None (the
SDKs and the token path then fall back to Application Default Credentials).""" SDKs and the token path then fall back to Application Default Credentials)."""
@@ -191,7 +199,7 @@ class VertexProvider(ProviderClient):
client = self._clients.get("openweight") client = self._clients.get("openweight")
if client is None: if client is None:
base = ( base = (
f"https://{self._location}-aiplatform.googleapis.com/v1/projects/" f"https://{_regional_host(self._location)}/v1/projects/"
f"{self._project}/locations/{self._location}/endpoints/openapi" f"{self._project}/locations/{self._location}/endpoints/openapi"
) )
client = OpenAIProvider(api_key=creds.token, base_url=base) client = OpenAIProvider(api_key=creds.token, base_url=base)
+18 -5
View File
@@ -105,6 +105,16 @@ def test_openweight_builds_maas_endpoint_and_refreshes_bearer():
assert rebuilt._api_key == "tok-3" assert rebuilt._api_key == "tok-3"
def test_openweight_global_location_has_no_region_host():
creds = _FakeCreds()
p = VertexProvider(project="proj", location="global", credentials=creds)
client = p._openweight_client()
assert client._base_url == (
"https://aiplatform.googleapis.com/v1/projects/proj"
"/locations/global/endpoints/openapi"
)
# -- credentials ------------------------------------------------------------------------ # -- credentials ------------------------------------------------------------------------
@@ -240,7 +250,7 @@ def test_verify_vertex_api_key_method(monkeypatch):
captured: dict = {} captured: dict = {}
def fake_get(url, headers=None, timeout=None, **kw): def fake_post(url, headers=None, json=None, timeout=None, **kw):
captured["url"] = url captured["url"] = url
captured["headers"] = headers captured["headers"] = headers
@@ -249,7 +259,7 @@ def test_verify_vertex_api_key_method(monkeypatch):
return _Resp() return _Resp()
monkeypatch.setattr(httpx, "get", fake_get) monkeypatch.setattr(httpx, "post", fake_post)
out = verify_provider_key( out = verify_provider_key(
"vertex", "vertex",
fields={ fields={
@@ -262,7 +272,10 @@ def test_verify_vertex_api_key_method(monkeypatch):
assert out == {"ok": True} assert out == {"ok": True}
assert captured["headers"]["x-goog-api-key"] == "AQ.k" assert captured["headers"]["x-goog-api-key"] == "AQ.k"
# Express mode is global — no region host, no project in the path. # Express mode is global — no region host, no project in the path.
assert captured["url"] == "https://aiplatform.googleapis.com/v1/publishers/google/models" assert captured["url"] == (
"https://aiplatform.googleapis.com/v1/publishers/google/models/"
"gemini-2.5-flash:countTokens"
)
def test_verify_vertex_service_account_requires_json(): def test_verify_vertex_service_account_requires_json():
@@ -306,7 +319,7 @@ def _patch_verify(monkeypatch, creds: Any, status_code: Optional[int]):
monkeypatch.setattr(vp, "load_credentials", lambda raw: creds) monkeypatch.setattr(vp, "load_credentials", lambda raw: creds)
captured: dict = {} captured: dict = {}
def fake_get(url, headers=None, timeout=None, **kw): def fake_post(url, headers=None, json=None, timeout=None, **kw):
captured["url"] = url captured["url"] = url
captured["headers"] = headers captured["headers"] = headers
@@ -317,7 +330,7 @@ def _patch_verify(monkeypatch, creds: Any, status_code: Optional[int]):
resp.status_code = status_code resp.status_code = status_code
return resp return resp
monkeypatch.setattr(httpx, "get", fake_get) monkeypatch.setattr(httpx, "post", fake_post)
return captured return captured