mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-24 16:54:43 +00:00
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:
parent
a7d78ad685
commit
3ea35d6711
5 changed files with 134 additions and 0 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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+)",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue