Reported: identical code and the same API key on two laptops, one said
"connected", the other said "Gemini (Google) model does not exist or is
not available." Traced to the connectivity probe hardcoding a single
model, gemini-3.6-flash — the newest entry in GEMINI_MODELS. Vertex AI
Model Garden availability is scoped per Google Cloud project and region,
so a brand-new model can reach one project's console before another; the
key, billing and every older model can be completely fine on the "failing"
laptop and this probe would still report the connection as broken.
Root env-var mechanism is separate and expected: GOOGLE_GENAI_USE_VERTEXAI
/ GOOGLE_CLOUD_PROJECT / GOOGLE_CLOUD_LOCATION live in envs.json, which is
machine-local and does not travel with `git pull` — each laptop configures
its own. That part is not a bug. The bug is that whichever laptop's project
had not yet had gemini-3.6-flash enabled got a false "everything is
broken" instead of "your key works, one specific model isn't rolled out
here yet."
Fix: try every model in the registry (oldest to newest, skipping the
tool-calling "-customtools" variant) and succeed on the first one that
works. A 404 on one model moves to the next; any other error (auth,
permission, quota, billing) stops immediately and is reported as-is,
since that applies no matter which model is asked for.
Also fixes a regression introduced while writing this: client construction
moved outside the per-model try/except, so bad service-account JSON or a
missing google-genai install would have escaped as an unhandled exception
instead of the graceful {"success": False, ...} every other path returns.
Caught by test_client_construction_failure_is_reported_gracefully before
it shipped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
158 lines
5.8 KiB
Python
158 lines
5.8 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""The Gemini/Vertex connectivity probe used when a user first adds credentials.
|
|
|
|
Real incident this guards against: the probe called `generate_content` with
|
|
one hardcoded model, gemini-3.6-flash — the newest entry in the registry.
|
|
Vertex AI Model Garden availability is scoped per Google Cloud project and
|
|
region, so a brand-new model can be enabled in one project's Vertex AI
|
|
console before another. Two laptops with byte-identical MaskanX code and
|
|
the same Gemini API key produced opposite results: "connected" on one,
|
|
"model does not exist or is not available" on the other — because the
|
|
*model*, not the key, differed in availability, and the probe reported
|
|
the whole connection as broken over exactly one model.
|
|
"""
|
|
from adclaw.providers.registry import GEMINI_MODELS
|
|
from adclaw.providers.store import (
|
|
_GEMINI_PROBE_MODEL_IDS,
|
|
_is_model_not_found_error,
|
|
_test_native_gemini_provider_connection,
|
|
)
|
|
|
|
|
|
class _FakeModels:
|
|
"""Stands in for `client.aio.models`. Queued outcomes, one per call."""
|
|
|
|
def __init__(self, outcomes):
|
|
self._outcomes = list(outcomes)
|
|
self.calls = []
|
|
|
|
async def generate_content(self, model, contents, config):
|
|
self.calls.append(model)
|
|
outcome = self._outcomes.pop(0)
|
|
if isinstance(outcome, Exception):
|
|
raise outcome
|
|
return outcome
|
|
|
|
|
|
class _FakeAio:
|
|
def __init__(self, models):
|
|
self.models = models
|
|
|
|
|
|
class _FakeClient:
|
|
def __init__(self, outcomes):
|
|
self.aio = _FakeAio(_FakeModels(outcomes))
|
|
|
|
|
|
def _not_found(model_id: str) -> Exception:
|
|
return Exception(
|
|
f"404 NOT_FOUND. {{'error': {{'code': 404, 'message': "
|
|
f"'Publisher Model `{model_id}` not found.'}}}}",
|
|
)
|
|
|
|
|
|
def test_probe_model_ids_exclude_the_customtools_variant():
|
|
"""That id is a tool-calling mode of gemini-3.1-pro-preview, not a
|
|
distinct model Vertex serves — probing it separately proves nothing."""
|
|
assert "gemini-3.1-pro-preview-customtools" not in _GEMINI_PROBE_MODEL_IDS
|
|
assert set(_GEMINI_PROBE_MODEL_IDS) == {
|
|
m.id for m in GEMINI_MODELS if "customtools" not in m.id
|
|
}
|
|
|
|
|
|
async def test_a_missing_newest_model_falls_through(monkeypatch):
|
|
"""The exact scenario reported: the newest model 404s in this project;
|
|
an older one is fine. The connection must be reported as working."""
|
|
client = _FakeClient([_not_found("gemini-3.6-flash"), "ok"])
|
|
monkeypatch.setattr(
|
|
"adclaw.providers.store._gemini_vertex_enabled", lambda: True,
|
|
)
|
|
monkeypatch.setattr(
|
|
"adclaw.providers.store._create_gemini_vertex_client",
|
|
lambda genai: client,
|
|
)
|
|
|
|
result = await _test_native_gemini_provider_connection(
|
|
"Gemini (Google)", "AIzafake",
|
|
)
|
|
|
|
assert result["success"] is True
|
|
assert "gemini-3.5-flash" in result["message"]
|
|
# Proves it actually tried the first model and moved on, not that it
|
|
# skipped straight to the second.
|
|
assert client.aio.models.calls[0] == "gemini-3.6-flash"
|
|
assert client.aio.models.calls[1] == "gemini-3.5-flash"
|
|
|
|
|
|
async def test_every_model_missing_is_a_real_failure(monkeypatch):
|
|
"""If nothing in the whole registry is available, that is worth
|
|
reporting — the fallback must not paper over a genuinely broken
|
|
project (e.g. Vertex AI API never enabled for it)."""
|
|
outcomes = [_not_found(mid) for mid in _GEMINI_PROBE_MODEL_IDS]
|
|
client = _FakeClient(outcomes)
|
|
monkeypatch.setattr(
|
|
"adclaw.providers.store._gemini_vertex_enabled", lambda: True,
|
|
)
|
|
monkeypatch.setattr(
|
|
"adclaw.providers.store._create_gemini_vertex_client",
|
|
lambda genai: client,
|
|
)
|
|
|
|
result = await _test_native_gemini_provider_connection(
|
|
"Gemini (Google)", "AIzafake",
|
|
)
|
|
|
|
assert result["success"] is False
|
|
assert "does not exist or is not available" in result["message"]
|
|
assert len(client.aio.models.calls) == len(_GEMINI_PROBE_MODEL_IDS)
|
|
|
|
|
|
async def test_a_real_error_on_the_first_model_stops_immediately(monkeypatch):
|
|
"""A bad API key or missing billing applies no matter which model is
|
|
asked for — trying four more models would just be four more of the
|
|
same failure, and delay telling the operator what is actually wrong."""
|
|
client = _FakeClient([Exception("403 PERMISSION_DENIED")])
|
|
monkeypatch.setattr(
|
|
"adclaw.providers.store._gemini_vertex_enabled", lambda: True,
|
|
)
|
|
monkeypatch.setattr(
|
|
"adclaw.providers.store._create_gemini_vertex_client",
|
|
lambda genai: client,
|
|
)
|
|
|
|
result = await _test_native_gemini_provider_connection(
|
|
"Gemini (Google)", "AIzafake",
|
|
)
|
|
|
|
assert result["success"] is False
|
|
assert "permission" in result["message"].lower()
|
|
assert len(client.aio.models.calls) == 1
|
|
|
|
|
|
async def test_client_construction_failure_is_reported_gracefully(monkeypatch):
|
|
"""Regression check: the import + client construction moved outside the
|
|
per-model loop when the fallback was added. If either raises, the
|
|
caller must still get {"success": False, ...}, not an unhandled
|
|
exception — bad service-account JSON must not 500 the endpoint."""
|
|
def _boom(genai):
|
|
raise ValueError("Invalid service account JSON")
|
|
|
|
monkeypatch.setattr(
|
|
"adclaw.providers.store._gemini_vertex_enabled", lambda: True,
|
|
)
|
|
monkeypatch.setattr(
|
|
"adclaw.providers.store._create_gemini_vertex_client", _boom,
|
|
)
|
|
|
|
result = await _test_native_gemini_provider_connection(
|
|
"Gemini (Google)", "AIzafake",
|
|
)
|
|
|
|
assert result["success"] is False
|
|
assert "Invalid service account JSON" in result["message"]
|
|
|
|
|
|
def test_is_model_not_found_error_recognises_graph_style_404s():
|
|
assert _is_model_not_found_error(_not_found("gemini-3.6-flash")) is True
|
|
assert _is_model_not_found_error(Exception("403 PERMISSION_DENIED")) is False
|