Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions hermes_cli/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}


Expand Down
17 changes: 17 additions & 0 deletions hermes_cli/model_switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
7 changes: 7 additions & 0 deletions hermes_cli/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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": [],
Expand Down
11 changes: 10 additions & 1 deletion hermes_cli/web_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
15 changes: 15 additions & 0 deletions tests/hermes_cli/test_provider_parity.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
49 changes: 49 additions & 0 deletions tests/hermes_cli/test_vertex_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down