fix(providers): stop reading "no local credentials" as "model not found"

The diagnostic logging just added (cf42152) immediately paid off: the
second laptop's real error, for all five fallback models, was google-auth's
own "Your default credentials were not found" — raised client-side before
any request reaches Google. This laptop has no service account JSON and
has never run `gcloud auth application-default login`; it was never about
Vertex AI Model Garden rollout at all.

The bug was mine: that message contains the literal substring "not
found", so _is_model_not_found_error's naive check misread "you have no
Google credentials on this machine" as "this specific model doesn't
exist" — retrying all five models for an identical, unfixable-by-retrying
failure, then still reporting the wrong final message ("model does not
exist or is not available") because _gemini_error_message made the same
substring mistake.

_is_missing_credentials_error checks for this specific error first, in
both places: the fallback loop now fails fast on the first model instead
of wasting four more identical round trips, and the final message tells
the operator exactly what is actually true — no Google Cloud credentials
exist on this machine, it is per-machine state, and there are two ways to
fix it (paste a service account JSON, or run the gcloud login command
locally) — not that a model is unavailable, which was never the case.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
AFFAANh
2026-09-03 15:22:46 +05:30
co-authored by Claude Opus 5
parent cf42152e4a
commit 7207f9142e
2 changed files with 116 additions and 1 deletions
+83
View File
@@ -14,10 +14,23 @@ 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_missing_credentials_error,
_is_model_not_found_error,
_test_native_gemini_provider_connection,
)
# Verbatim from a real failure: a machine with Vertex enabled but no
# service account JSON and no `gcloud auth application-default login`
# ever run. Raised client-side by google-auth before any network call is
# made — every model would fail identically, since none of them are the
# actual problem.
_MISSING_ADC_ERROR = (
"Your default credentials were not found. To set up Application "
"Default Credentials, see "
"https://cloud.google.com/docs/authentication/external/set-up-adc "
"for more information."
)
class _FakeModels:
"""Stands in for `client.aio.models`. Queued outcomes, one per call."""
@@ -155,3 +168,73 @@ async def test_client_construction_failure_is_reported_gracefully(monkeypatch):
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
# --- the real incident: missing local Google credentials, not a model problem ---
#
# Reported live: a second laptop, same API key, same real Project ID,
# failed on every one of the five fallback models with the exact same
# "model does not exist" message. The raw error, logged only after this
# fix, was google-auth's own "Your default credentials were not found" —
# a client-side failure before any request reaches Google, misread as a
# per-model 404 because its own text contains "not found".
def test_missing_credentials_error_is_not_mistaken_for_a_missing_model():
"""The bug: this error's own text contains "not found", so the naive
substring check in _is_model_not_found_error treated "you have no
Google credentials on this machine" as "this one model is missing"
and retried four more times for an identical, unfixable-by-retrying
failure."""
exc = Exception(_MISSING_ADC_ERROR)
assert _is_missing_credentials_error(exc) is True
assert _is_model_not_found_error(exc) is False
async def test_missing_credentials_fails_fast_without_trying_every_model(
monkeypatch,
):
"""Retrying five models for a machine-wide auth problem wastes five
round trips and still arrives at a wrong final message. It must stop
on the first attempt."""
client = _FakeClient([Exception(_MISSING_ADC_ERROR)])
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 len(client.aio.models.calls) == 1
assert "credentials" in result["message"].lower()
# The old, wrong message must not appear — that was the entire bug.
assert "does not exist or is not available" not in result["message"]
async def test_missing_credentials_message_says_what_to_do_and_that_its_per_machine(
monkeypatch,
):
client = _FakeClient([Exception(_MISSING_ADC_ERROR)])
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",
)
message = result["message"].lower()
assert "gcloud auth application-default login" in message
assert "service account" in message
assert "per-machine" in message or "another computer" in message