From a5f97bfec5efaf330010e328d703802318df8956 Mon Sep 17 00:00:00 2001 From: datalounaesportinc-droid <260224409+datalounaesportinc-droid@users.noreply.github.com> Date: Fri, 17 Jul 2026 09:17:52 +0000 Subject: [PATCH] fix(vertex): detect ADC across model pickers --- hermes_cli/auth.py | 18 +++++++++ hermes_cli/model_switch.py | 17 ++++++++ hermes_cli/models.py | 7 ++++ hermes_cli/web_server.py | 11 +++++- tests/hermes_cli/test_provider_parity.py | 15 ++++++++ tests/hermes_cli/test_vertex_provider.py | 49 ++++++++++++++++++++++++ 6 files changed, 116 insertions(+), 1 deletion(-) diff --git a/hermes_cli/auth.py b/hermes_cli/auth.py index 13f7cf362f6b..af2aa32ee9b9 100644 --- a/hermes_cli/auth.py +++ b/hermes_cli/auth.py @@ -6438,6 +6438,24 @@ def get_auth_status(provider_id: Optional[str] = None) -> Dict[str, Any]: return {"logged_in": has_aws_credentials(), "provider": target} except ImportError: return {"logged_in": False, "provider": target, "error": "boto3 not installed"} + # Plugin-backed providers such as Vertex intentionally live outside the + # legacy auth PROVIDER_REGISTRY. Resolve their auth type from the runtime + # provider registry, which is the source of truth for routability. + try: + from providers import get_provider_profile + runtime_profile = get_provider_profile(target) + except Exception: + runtime_profile = None + # Vertex uses Google Application Default Credentials rather than an API + # key or Hermes auth-store entry. Keep this structural and fast: the + # adapter checks for an explicit project / credentials path without + # importing google-auth or minting a token. + if runtime_profile and runtime_profile.auth_type == "vertex": + try: + from agent.vertex_adapter import has_vertex_credentials + return {"logged_in": has_vertex_credentials(), "provider": target} + except Exception as exc: + return {"logged_in": False, "provider": target, "error": str(exc)} return {"logged_in": False} diff --git a/hermes_cli/model_switch.py b/hermes_cli/model_switch.py index fb9a348c8149..4461600c5413 100644 --- a/hermes_cli/model_switch.py +++ b/hermes_cli/model_switch.py @@ -1975,6 +1975,23 @@ def _has_aws_sdk_creds_for_listing(slug: str) -> bool: if not _cp_has_creds and _cp_config and getattr(_cp_config, "auth_type", "") == "aws_sdk": _cp_has_creds = _has_aws_sdk_creds_for_listing(_cp.slug) + # Vertex credentials live in Google's ADC chain, not in an API-key + # env var or the Hermes auth store. Use the adapter's fast structural + # detector so Vertex appears in task/model pickers without performing + # token acquisition or network I/O while the picker opens. + if not _cp_has_creds: + try: + from providers import get_provider_profile + _cp_runtime_profile = get_provider_profile(_cp.slug) + except Exception: + _cp_runtime_profile = None + if _cp_runtime_profile and _cp_runtime_profile.auth_type == "vertex": + try: + from agent.vertex_adapter import has_vertex_credentials + _cp_has_creds = bool(has_vertex_credentials()) + except Exception as exc: + logger.debug("Vertex credential check failed: %s", exc) + if not _cp_has_creds: continue diff --git a/hermes_cli/models.py b/hermes_cli/models.py index 389bfa0a5bd9..6851645c0cc8 100644 --- a/hermes_cli/models.py +++ b/hermes_cli/models.py @@ -544,6 +544,13 @@ def _xai_curated_models() -> list[str]: "us.meta.llama4-maverick-17b-instruct-v1:0", "us.meta.llama4-scout-17b-instruct-v1:0", ], + # Vertex exposes Gemini through Google's OpenAI-compatible endpoint and + # does not provide a /models listing route. Keep its curated catalog here + # so every model picker shares the same source as the setup flow. + "vertex": [ + "google/gemini-3-pro-preview", + "google/gemini-3-flash-preview", + ], # Azure Foundry: user-provided endpoint and model. # Empty list because models depend on the endpoint configuration. "azure-foundry": [], diff --git a/hermes_cli/web_server.py b/hermes_cli/web_server.py index a50f615d60a1..2842ba4229c7 100644 --- a/hermes_cli/web_server.py +++ b/hermes_cli/web_server.py @@ -6285,16 +6285,25 @@ def _catalog_provider_env_metadata() -> dict: async def get_env_vars(profile: Optional[str] = None): with _profile_scope(profile): env_on_disk = load_env() + try: + from agent.vertex_adapter import has_vertex_credentials + vertex_credentials_configured = bool(has_vertex_credentials()) + except Exception: + vertex_credentials_configured = False channel_keys = _channel_managed_env_keys() catalog_meta = _catalog_provider_env_metadata() def _row(var_name: str, info: dict, *, custom: bool = False) -> dict: value = env_on_disk.get(var_name) cat_meta = catalog_meta.get(var_name) or {} + is_set = bool(value) or ( + var_name == "VERTEX_CREDENTIALS_PATH" + and vertex_credentials_configured + ) # Hand OPTIONAL_ENV_VARS prose wins where present; the catalog fills any # gaps (description/url) and always supplies provider grouping hints. return { - "is_set": bool(value), + "is_set": is_set, "redacted_value": redact_key(value) if value else None, "description": info.get("description") or cat_meta.get("description", ""), "url": info.get("url") if info.get("url") is not None else cat_meta.get("url"), diff --git a/tests/hermes_cli/test_provider_parity.py b/tests/hermes_cli/test_provider_parity.py index d04feeb6723c..4cc11be3c8cc 100644 --- a/tests/hermes_cli/test_provider_parity.py +++ b/tests/hermes_cli/test_provider_parity.py @@ -11,6 +11,7 @@ can never silently drift again when a provider plugin is added. """ +import pytest from fastapi.testclient import TestClient from hermes_cli.models import CANONICAL_PROVIDERS @@ -95,3 +96,17 @@ def test_no_provider_appears_on_both_tabs(): """ overlap = (_keys_tab_providers() & _accounts_tab_providers()) - _EXEMPT - _DUAL_TAB assert not overlap, f"providers appearing on BOTH desktop tabs: {sorted(overlap)}" + + +@pytest.mark.asyncio +async def test_vertex_adc_marks_desktop_provider_connected(monkeypatch): + import agent.vertex_adapter as vertex_adapter + import hermes_cli.web_server as web_server + + monkeypatch.setattr(web_server, "load_env", lambda: {}) + monkeypatch.setattr(vertex_adapter, "has_vertex_credentials", lambda: True) + + result = await web_server.get_env_vars() + + assert result["VERTEX_CREDENTIALS_PATH"]["is_set"] is True + assert result["VERTEX_CREDENTIALS_PATH"]["redacted_value"] is None diff --git a/tests/hermes_cli/test_vertex_provider.py b/tests/hermes_cli/test_vertex_provider.py index af67aacac297..3356c90a60f4 100644 --- a/tests/hermes_cli/test_vertex_provider.py +++ b/tests/hermes_cli/test_vertex_provider.py @@ -80,6 +80,55 @@ def test_resolve_runtime_provider_raises_autherror_when_unresolved(monkeypatch): assert "not a static API key" in msg +def test_vertex_auth_status_uses_adc_credential_detector(monkeypatch): + import agent.vertex_adapter as va + from hermes_cli.auth import get_auth_status + + monkeypatch.setattr(va, "has_vertex_credentials", lambda: True) + + assert get_auth_status("vertex") == { + "logged_in": True, + "provider": "vertex", + } + + +def test_vertex_adc_surfaces_in_task_model_options(monkeypatch): + import agent.models_dev as models_dev + import agent.vertex_adapter as va + import hermes_cli.model_switch as model_switch + import hermes_cli.models as models + from hermes_cli.inventory import ConfigContext, build_models_payload + + monkeypatch.setattr(models_dev, "fetch_models_dev", lambda: {}) + monkeypatch.setattr(va, "has_vertex_credentials", lambda: True) + monkeypatch.setattr( + model_switch, + "_credential_pool_is_usable", + lambda *args, **kwargs: False, + ) + monkeypatch.setattr(models, "cached_provider_model_ids", lambda provider: []) + + payload = build_models_payload( + ConfigContext( + current_provider="openai-codex", + current_model="gpt-test", + current_base_url="", + user_providers={}, + custom_providers=[], + ), + include_unconfigured=True, + picker_hints=True, + ) + vertex = next(row for row in payload["providers"] if row["slug"] == "vertex") + + assert vertex["authenticated"] is True + assert vertex["source"] == "canonical" + assert vertex["models"] == [ + "google/gemini-3-pro-preview", + "google/gemini-3-flash-preview", + ] + + def test_vertex_extra_body_thinking_config(): from providers import get_provider_profile