fix(vertex,moa): register vertex in PROVIDER_REGISTRY and HERMES_OVERLAYS

The Vertex AI provider (added same-day, commit c73e74386) was never added to
either of the two provider registries that agent/auxiliary_client.py and the
MoA slot-resolution chain depend on, breaking Vertex outside the main
conversation loop:

1. hermes_cli/auth.py::PROVIDER_REGISTRY had no "vertex" entry. The
   plugin-auto-extend loop that normally fills gaps explicitly skips
   non-api_key auth types (`if _pp.auth_type != "api_key": continue`), and
   Vertex was never hand-declared like "bedrock" is. Because
   resolve_provider_client() in agent/auxiliary_client.py gates everything
   on `pconfig = PROVIDER_REGISTRY.get(provider)` and returns (None, None)
   immediately when pconfig is None, its `elif pconfig.auth_type == "vertex"`
   branch was permanently dead code — every auxiliary Vertex call (vision,
   title generation, reflection, context compression, MoA reference/
   aggregator slots) failed outright, not just a MoA-specific edge case.

2. hermes_cli/providers.py::HERMES_OVERLAYS also had no "vertex" entry, so
   hermes_cli.providers.get_provider("vertex") returned None. This backs
   _preserve_provider_with_base_url() in agent/auxiliary_client.py, which a
   MoA slot's resolved (base_url, api_key) pair needs to keep its "vertex"
   identity instead of silently collapsing to "custom" — losing the
   identity _refresh_provider_credentials() needs to re-mint an expired
   OAuth2 token (~1h lifetime) on a 401, and permanently breaking every
   subsequent call in that MoA preset for the rest of the session.

Fix mirrors the existing "bedrock"/aws_sdk entries in both registries
exactly, plus adds a "vertex" branch to _refresh_provider_credentials() (it
had branches for openai-codex/nous/anthropic/xai-oauth but not vertex,
so a 401 fell through to `return False` without evicting the stale cached
client).

- hermes_cli/auth.py: hand-declared vertex ProviderConfig(auth_type="vertex")
  in PROVIDER_REGISTRY, matching bedrock's shape.
- hermes_cli/providers.py: vertex HermesOverlay(auth_type="vertex") in
  HERMES_OVERLAYS + "Google Vertex AI" label override.
- agent/auxiliary_client.py: vertex branch in _refresh_provider_credentials
  that re-mints the token via get_vertex_config() and evicts the stale
  cached client.
- 8 new regression tests across tests/hermes_cli/test_vertex_provider.py and
  tests/agent/test_auxiliary_client.py: registry membership, end-to-end
  resolve_provider_client("vertex", ...) building a working client (proving
  the previously-dead branch is now reachable), and the 401-refresh/cache-
  eviction path.
This commit is contained in:
srojk34 2026-07-02 02:52:26 +03:00 committed by Teknium
parent a7d78ad685
commit 3ea35d6711
5 changed files with 134 additions and 0 deletions

View file

@ -3819,6 +3819,24 @@ def _refresh_provider_credentials(provider: str) -> bool:
return False
_evict_cached_clients(normalized)
return True
if normalized == "vertex":
# Mirrors run_agent.py's _try_refresh_vertex_client_credentials
# for the main conversation loop. Without this branch, an
# auxiliary Vertex client (vision, title generation, reflection,
# context compression, ...) that 401s on its ~1h token expiry
# falls through to the final `return False` below: the stale
# client is never evicted from _client_cache (whose cache key
# ignores the rotating bearer token), so every subsequent
# auxiliary Vertex call keeps 401ing until process restart.
from agent.vertex_adapter import get_vertex_config
token, base_url = get_vertex_config()
if not isinstance(token, str) or not token.strip():
return False
if not isinstance(base_url, str) or not base_url.strip():
return False
_evict_cached_clients(normalized)
return True
except Exception as exc:
logger.debug("Auxiliary provider credential refresh failed for %s: %s", normalized, exc)
return False

View file

@ -434,6 +434,17 @@ PROVIDER_REGISTRY: Dict[str, ProviderConfig] = {
api_key_env_vars=(),
base_url_env_var="BEDROCK_BASE_URL",
),
"vertex": ProviderConfig(
id="vertex",
name="Google Vertex AI",
auth_type="vertex",
# No static inference_base_url: Vertex's endpoint is computed per
# request from project_id + region (agent/vertex_adapter.py's
# build_vertex_base_url), not a fixed host like the other entries.
inference_base_url="",
api_key_env_vars=(), # OAuth2 (service-account JSON / ADC), not a key
base_url_env_var="",
),
"azure-foundry": ProviderConfig(
id="azure-foundry",
name="Azure Foundry",

View file

@ -222,6 +222,19 @@ HERMES_OVERLAYS: Dict[str, HermesOverlay] = {
transport="bedrock_converse",
auth_type="aws_sdk",
),
# Vertex authenticates via OAuth2 (service-account JSON / ADC), not a
# static API key or models.dev entry — resolved specially by
# agent/vertex_adapter.py, like bedrock's aws_sdk. Without an overlay
# entry get_provider("vertex") returns None, which makes
# _preserve_provider_with_base_url() in agent/auxiliary_client.py treat
# a Vertex MoA slot's resolved (base_url, api_key) pair as an unknown
# custom endpoint instead of "vertex" — losing the provider identity
# that _refresh_provider_credentials() needs to re-mint an expired
# OAuth2 token on a 401.
"vertex": HermesOverlay(
transport="openai_chat",
auth_type="vertex",
),
}
@ -390,6 +403,7 @@ _LABEL_OVERRIDES: Dict[str, str] = {
"lmstudio": "LM Studio",
"local": "Local endpoint",
"bedrock": "AWS Bedrock",
"vertex": "Google Vertex AI",
"ollama-cloud": "Ollama Cloud",
"xai-oauth": "xAI Grok OAuth (SuperGrok / Premium+)",
}

View file

@ -4347,6 +4347,69 @@ class TestAuxiliaryAuthRefreshRetry:
mock_write.assert_called_once_with("fresh-token", "refresh-token-2", 9999999999999)
stale_client.close.assert_called_once()
def test_refresh_provider_credentials_remints_vertex_token_and_evicts_cache(self):
"""Vertex tokens live ~1h; on a long-running gateway the cached
auxiliary client's bearer token expires mid-session and 401s.
_refresh_provider_credentials("vertex") must re-mint the token via
the adapter (which refreshes in place when near expiry) and evict
the stale cached client so the next call rebuilds with a fresh one
previously there was no "vertex" branch here at all, so this fell
through to the final `return False` and the stale client (and its
dead token) stayed cached until process restart."""
stale_client = MagicMock()
cache_key = ("vertex", False, None, None, None)
with (
patch("agent.auxiliary_client._client_cache", {cache_key: (stale_client, "google/gemini-3-flash-preview", None)}),
patch(
"agent.vertex_adapter.get_vertex_config",
return_value=("ya29.FRESH", "https://aiplatform.googleapis.com/v1beta1/projects/p/locations/global/endpoints/openapi"),
) as mock_get_config,
):
from agent.auxiliary_client import _refresh_provider_credentials
assert _refresh_provider_credentials("vertex") is True
mock_get_config.assert_called_once()
stale_client.close.assert_called_once()
def test_refresh_provider_credentials_vertex_returns_false_when_unminted(self):
"""No usable token/base_url (e.g. ADC and the service-account file
both failed) refresh must report failure, not silently evict and
pretend the client is fixed."""
with patch("agent.vertex_adapter.get_vertex_config", return_value=(None, None)):
from agent.auxiliary_client import _refresh_provider_credentials
assert _refresh_provider_credentials("vertex") is False
def test_resolve_provider_client_vertex_builds_client_from_minted_token(self):
"""End-to-end: resolve_provider_client("vertex", ...) must reach the
auth_type == "vertex" branch and build a working client, not die at
the PROVIDER_REGISTRY lookup (a plain HERMES_OVERLAYS-only fix would
leave this branch dead code PROVIDER_REGISTRY is what
resolve_provider_client actually gates on)."""
with (
patch("agent.vertex_adapter.has_vertex_credentials", return_value=True),
patch(
"agent.vertex_adapter.get_vertex_config",
return_value=("ya29.FRESH", "https://aiplatform.googleapis.com/v1beta1/projects/p/locations/global/endpoints/openapi"),
),
):
client, model = resolve_provider_client("vertex", "google/gemini-3-flash-preview")
assert client is not None
assert model == "google/gemini-3-flash-preview"
assert str(client.base_url).rstrip("/") == (
"https://aiplatform.googleapis.com/v1beta1/projects/p/locations/global/endpoints/openapi"
)
def test_resolve_provider_client_vertex_none_when_no_credentials(self):
with patch("agent.vertex_adapter.has_vertex_credentials", return_value=False):
client, model = resolve_provider_client("vertex", "google/gemini-3-flash-preview")
assert client is None
assert model is None
@pytest.mark.asyncio
async def test_async_call_llm_refreshes_anthropic_on_401_for_non_vision(self):
stale_client = MagicMock()

View file

@ -98,3 +98,31 @@ def test_vertex_extra_body_empty_without_reasoning():
p = get_provider_profile("vertex")
assert p.build_extra_body(model="google/gemini-3-flash-preview") == {}
def test_vertex_registered_in_provider_registry():
"""PROVIDER_REGISTRY (hermes_cli.auth) is what agent/auxiliary_client.py's
resolve_provider_client() looks up before dispatching on auth_type. Without
an entry here, the ``elif pconfig.auth_type == "vertex":`` branch there is
unreachable dead code every auxiliary Vertex call (vision, title
generation, MoA reference/aggregator slots, ...) fails at the
``pconfig is None`` guard before ever reaching it."""
from hermes_cli.auth import PROVIDER_REGISTRY
cfg = PROVIDER_REGISTRY.get("vertex")
assert cfg is not None
assert cfg.auth_type == "vertex"
def test_vertex_registered_in_hermes_overlays():
"""hermes_cli.providers.get_provider("vertex") backs
_preserve_provider_with_base_url() in agent/auxiliary_client.py, which
decides whether a MoA slot's resolved Vertex (base_url, api_key) pair
keeps its "vertex" provider identity or silently collapses to "custom"
losing the identity _refresh_provider_credentials() needs to re-mint an
expired OAuth2 token on a 401."""
from hermes_cli.providers import get_provider
resolved = get_provider("vertex")
assert resolved is not None
assert resolved.auth_type == "vertex"