NousResearch/hermes-agent

Google Vertex AI provider support and authentication

Cluster 13484 · agent. Data as of

1 open item. Fix merged in #56363 on 1 Jul 2026. Check it covers your case.

The short answer

0open issues
1open PR
5items grouped
Known?
Yes. 5 related items grouped here, 2 issues already closed.
Fix event
A fix was merged to main in PR #56363. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this unresolved. That is separate from the fix event.
Still open
0 issues and 1 PR.

Members all concern Google Vertex AI integration: several propose adding a Vertex AI provider (for Gemini models, via an AnthropicVertex SDK, or with OAuth2), and one requests simpler authentication for Vertex's OpenAI-compatible endpoint. The titles do not indicate the status of any of these.

Open issues0

No open issues.

Open PRs1

  • PR#55742feat: add Google Vertex AI provider (AnthropicVertex SDK)

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #56363 feat(vertex): add Google Vertex AI provider for Gemini (OAuth2)

    merged to main

    files: agent/auxiliary_client.py, agent/conversation_loop.py, agent/turn_retry_state.py, agent/usage_pricing.py, agent/vertex_adapter.py, hermes_cli/config.py, hermes_cli/main.py, hermes_cli/model_setup_flows.py, hermes_cli/models.py, hermes_cli/runtime_provider.py, hermes_cli/setup.py, hermes_cli/web_server.py, plugins/model-providers/vertex/__init__.py, plugins/model-providers/vertex/plugin.yaml, pyproject.toml, run_agent.py, scripts/release.py, tests/agent/test_turn_retry_state.py, tests/agent/test_vertex_adapter.py, tests/hermes_cli/test_vertex_provider.py (+4 more)

    Show the diff
    • agent/auxiliary_client.py
      @@ -4506,6 +4506,42 @@ def _wrap_if_needed(client_obj, final_model_str: str, base_url_str: str = "",
                                "directly supported", provider)
               return None, None
       
      +    elif pconfig.auth_type == "vertex":
      +        # Google Vertex AI — Gemini via the OpenAI-compatible endpoint with an
      +        # OAuth2 bearer token (NOT a static key). We build a standard OpenAI
      +        # client pointed at the runtime-computed Vertex base_url with a fresh
      +        # token; no custom SDK or message translation needed.
      +        try:
      +            from agent.vertex_adapter import get_vertex_config, has_vertex_credentials
      +        except ImportError:
      +            logger.warning("resolve_provider_client: vertex requested but "
      +                           "google-auth not installed")
      +            return None, None
      +
      +        if not has_vertex_credentials():
      +            logger.debug("resolve_provider_client: vertex requested but "
      +                         "no GCP credentials found")
      +            return None, None
      +
      +        token, base_url = get_vertex_config()
      +        if not token or not base_url:
      +            logger.warning("resolve_provider_client: vertex requested but "
      +                           "could not mint token / resolve project")
      +            return None, None
      +
      +        default_model = "google/gemini-3-flash-preview"
      +        final_model = _normalize_resolved_model(model or default_model, provider)
      +        try:
      +            from openai import OpenAI
      +            client = OpenAI(api_key=[redacted:secret-assignment] base_url=base_url)
      +        except Exception as exc:
      +            logger.warning("resolve_provider_client: cannot create Vertex "
      +                           "client: %s", exc)
      +            return None, None
      +        logger.debug("resolve_provider_client: vertex (%s)", final_model)
      +        return (_to_async_client(client, final_model, is_vision=is_vision) if async_mode
      +                else (client, final_model))
      +
           elif pconfig.auth_type == "aws_sdk":
               # AWS SDK providers (Bedrock) — use the Anthropic Bedrock client via
               # boto3's credential chain (IAM roles, SSO, env vars, instance metadata).
    • agent/conversation_loop.py
      @@ -2598,6 +2598,16 @@ def _perform_api_call(next_api_kwargs):
                               _label = "xAI OAuth" if agent.provider == "xai-oauth" else "Codex"
                               agent._buffer_vprint(f"🔐 {_label} auth refreshed after 401. Retrying request...")
                               continue
      +                if (
      +                    agent.api_mode == "chat_completions"
      +                    and agent.provider == "vertex"
      +                    and status_code == 401
      +                    and not _retry.vertex_auth_retry_attempted
      +                ):
      +                    _retry.vertex_auth_retry_attempted = True
      +                    if agent._try_refresh_vertex_client_credentials():
      +                        agent._buffer_vprint("🔐 Vertex AI token refreshed after 401. Retrying request...")
      +                        continue
                       if (
                           agent.api_mode == "chat_completions"
                           and agent.provider == "nous"
    • agent/turn_retry_state.py
      @@ -45,6 +45,7 @@ class TurnRetryState:
           nous_auth_retry_attempted: bool = False
           nous_paid_entitlement_refresh_attempted: bool = False
           copilot_auth_retry_attempted: bool = False
      +    vertex_auth_retry_attempted: bool = False
       
           # ── Format / payload recovery guards ─────────────────────────────────
           thinking_sig_retry_attempted: bool = False
    • agent/usage_pricing.py
      @@ -606,6 +606,11 @@ def resolve_billing_route(
               return BillingRoute(provider="openai", model=model.split("/")[-1], base_url=base_url or "", billing_mode="official_docs_snapshot")
           if provider_name in {"minimax", "minimax-cn"}:
               return BillingRoute(provider=provider_name, model=model.split("/")[-1], base_url=base_url or "", billing_mode="official_docs_snapshot")
      +    # Vertex AI hosts the same Gemini models as Google AI Studio; price them
      +    # off the gemini official-docs snapshot. Strip the "google/" vendor prefix
      +    # the OpenAI-compat endpoint requires so the pricing key matches.
      +    if provider_name == "vertex" or base_url_host_matches(base_url or "", "aiplatform.googleapis.com"):
      +        return BillingRoute(provider="gemini", model=model.split("/")[-1], base_url=base_url or "", billing_mode="official_docs_snapshot")
           if provider_name in {"custom", "local"} or (base and "localhost" in base):
               return BillingRoute(provider=provider_name or "custom", model=model, base_url=base_url or "", billing_mode="unknown")
           return BillingRoute(provider=provider_name or "unknown", model=model.split("/")[-1] if model else "", base_url=base_url or "", billing_mode="unknown")
    • agent/vertex_adapter.py
      @@ -0,0 +1,202 @@
      +"""Vertex AI (Google Cloud) adapter for Hermes Agent.
      +
      +Provides authentication and configuration for Vertex AI's OpenAI-compatible
      +endpoint. This allows Hermes to use Gemini models via Google Cloud with
      +enterprise-grade rate limits and quotas.
      +
      +Requires: pip install google-auth
      +
      +Environment variables honored (all optional):
      +  GOOGLE_APPLICATION_CREDENTIALS — path to a service account JSON file (secret).
      +  VERTEX_CREDENTIALS_PATH        — alias, takes precedence if set (secret).
      +  VERTEX_PROJECT_ID              — override the project_id embedded in creds.
      +  VERTEX_REGION                  — override default region ("global" unless set).
      +
      +Non-secret routing settings (project_id, region) also live in config.yaml
      +under the ``vertex:`` section; env vars take precedence over config.yaml.
      +"""
      +
      +import logging
      +import os
      +import time
      +from typing import Optional, Tuple
      +
      +# Ensure google-auth is installed before importing. The [vertex] extra is no
      +# longer in [all] per the lazy-install policy added 2026-05-12 — lazy_deps
      +# handles on-demand installation so the Vertex provider still works for users
      +# who installed plain `hermes-agent` and only later selected a Gemini model.
      +try:
      +    from tools.lazy_deps import ensure as _lazy_ensure
      +    _lazy_ensure("provider.vertex", prompt=False)
      +except Exception:
      +    pass  # lazy_deps unavailable or install failed — fall through to the real ImportError below
      +
      +try:
      +    import google.auth
      +    import google.auth.transport.requests
      +    from google.oauth2 import service_account
      +except ImportError:
      +    google = None  # type: ignore[assignment]
      +
      +logger = logging.getLogger(__name__)
      +
      +DEFAULT_REGION = "global"
      +
      +_creds_cache: dict = {}
      +
      +
      +def _vertex_config() -> dict:
      +    """Return the ``vertex:`` section of config.yaml, or {} on any failure.
      +
      +    Non-secret routing settings (project_id, region) live in config.yaml per
      +    the .env-secrets-only rule. Env vars still take precedence — they are read
      +    directly at the call sites below, with config.yaml as the fallback.
      +    """
      +    try:
      +        from hermes_cli.config import load_config
      +
      +        section = load_config().get("vertex")
      +        return section if isinstance(section, dict) else {}
      +    except Exception:
      +        return {}
      +
      +
      +def _resolve_region(explicit: Optional[str] = None) -> str:
      +    """Region precedence: explicit arg > VERTEX_REGION env > config.yaml > default."""
      +    if explicit:
      +        return explicit
      +    env_region = os.environ.get("VERTEX_REGION", "").strip()
      +    if env_region:
      +        return env_region
      +    cfg_region = str(_vertex_config().get("region") or "").strip()
      +    return cfg_region or DEFAULT_REGION
      +
      +
      +def _resolve_project_override() -> Optional[str]:
      +    """Project-ID override precedence: VERTEX_PROJECT_ID env > config.yaml.
      +
      +    Returns None when neither is set (the credentials' embedded project_id
      +    is used in that case).
      +    """
      +    env_project = os.environ.get("VERTEX_PROJECT_ID", "").strip()
      +    if env_project:
      +        return env_project
      +    cfg_project = str(_vertex_config().get("project_id") or "").strip()
      +    return cfg_project or None
      +
      +
      +def _resolve_credentials_path(explicit: Optional[str]) -> Optional[str]:
      +    if explicit and os.path.exists(explicit):
      +        return explicit
      +    for env_var in ("VERTEX_CREDENTIALS_PATH", "GOOGLE_APPLICATION_CREDENTIALS"):
      +        path = os.environ.get(env_var)
      +        if path and os.path.exists(path):
      +            return path
      +    return None
      +
      +
      +def _refresh_credentials(creds) -> None:
      +    auth_req = google.auth.transport.requests.Request()
      +    creds.refresh(auth_req)
      +
      +
      +def get_vertex_credentials(credentials_path: [redacted:secret-assignment] = None) -> Tuple[Optional[str], Optional[str]]:
      +    """Return a (fresh access_token, project_id) pair or (None, None) on failure.
      +
      +    Caches the underlying Credentials object and refreshes it when within
      +    5 minutes of expiry, so repeated calls don't thrash the token endpoint.
      +    """
      +    if google is None:
      +        logger.warning("google-auth package not installed. Cannot use Vertex AI.")
      +        return None, None
      +
      +    resolved_path = _resolve_credentials_path(credentials_path)
      +    cache_key = resolved_path or "__adc__"
      +
      +    try:
      +        cached = _creds_cache.get(cache_key)
      +        if cached is None:
      +            if resolved_path:
      +                creds = service_account.Credentials.from_service_account_file(
      +                    resolved_path,
      +                    scopes=["https://www.googleapis.com/auth/cloud-platform"],
      +                )
      +                project_id = creds.project_id
      +            else:
      +                creds, project_id = google.auth.default(
      +                    scopes=["https://www.googleapis.com/auth/cloud-platform"]
      +                )
      +            _creds_cache[cache_key] = (creds, project_id)
      +        else:
      +            creds, project_id = cached
      +
      +        needs_refresh = (
      +            not getattr(creds, "token", None)
      +            or getattr(creds, "expired", False)
      +            or (
      +                getattr(creds, "expiry", None) is not None
      +                and (creds.expiry.timestamp() - time.time()) < 300
      +            )
      +        )
      +        if needs_refresh:
      +            _refresh_credentials(creds)
      +
      +        override_project = _resolve_project_override()
      +        if override_project:
      +            project_id = override_project
      +
      +        return creds.token, project_id
      +    except Exception as e:
      +        logger.error(f"Failed to resolve Vertex AI credentials: {e}")
      +        _creds_cache.pop(cache_key, None)
      +
      +        # If ADC failed (e.g. expired refresh token), try the SA file
      +        # before giving up — it may have been added after initial startup.
      +        if cache_key == "__adc__":
      +            sa_path = _resolve_credentials_path(credentials_path)
      +            if sa_path:
      +                logger.info("ADC failed, retrying with service account: %s", sa_path)
      +                return get_vertex_credentials(sa_path)
      +
      +        return None, None
      +
      +
      +def build_vertex_base_url(project_id: str, region: str = DEFAULT_REGION) -> str:
      +    """Build the OpenAI-compatible base URL for Vertex AI.
      +
      +    The `global` location uses a bare `aiplatform.googleapis.com` hostname,
      +    while regional locations use `{region}-aiplatform.googleapis.com`.
      +    Gemini 3.x preview models are only served via the global endpoint at
      +    the time of writing.
      +    """
      +    host = "aiplatform.googleapis.com" if region == "global" else f"{region}-aiplatform.googleapis.com"
      +    return f"https://{host}/v1beta1/projects/{project_id}/locations/{region}/endpoints/openapi"
      +
      +
      +def get_vertex_config(
      +    credentials_path: [redacted:secret-assignment] = None,
      +    region: Optional[str] = None,
      +) -> Tuple[Optional[str], Optional[str]]:
      +    """Resolve (access_token, base_url) for Vertex AI, or (None, None) on failure."""
      +    token, project_id = get_vertex_credentials(credentials_path)
      +    if not token or not project_id:
      +        return None, None
      +
      +    effective_region = _resolve_region(region)
      +    base_url = build_vertex_base_url(project_id, effective_region)
      +    return token, base_url
      +
      +
      +def has_vertex_credentials() -> bool:
      +    """Fast check for whether Vertex credentials appear configured.
      +
      +    No network calls and no google-auth import — safe for provider
      +    auto-detection and setup-status display. True when either a service
      +    account JSON path is resolvable, or an explicit project ID is configured
      +    (env or config.yaml, implying ADC is intended).
      +    """
      +    if _resolve_credentials_path(None):
      +        return True
      +    if _resolve_project_override():
      +        return True
      +    return False
    • hermes_cli/config.py
      @@ -3086,6 +3086,24 @@ def _ensure_hermes_home_managed(home: Path):
           },
       
       
      +    # Google Vertex AI provider (Gemini via the OpenAI-compatible endpoint).
      +    # Auth is OAuth2 (short-lived access tokens minted from a service-account
      +    # JSON or Application Default Credentials) — NOT a static API key. The
      +    # credential *path* is a secret-adjacent pointer and lives in .env
      +    # (VERTEX_CREDENTIALS_PATH / GOOGLE_APPLICATION_CREDENTIALS); these two
      +    # settings are non-secret routing config and live here. Both are bridged to
      +    # the VERTEX_PROJECT_ID / VERTEX_REGION env vars the adapter reads, so an
      +    # explicit env var still wins over config.yaml.
      +    "vertex": {
      +        # GCP project ID. Empty → use the project_id embedded in the service
      +        # account JSON (or ADC-resolved project).
      +        "project_id": "",
      +        # Vertex region. "global" is required for the Gemini 3.x preview models
      +        # (regional endpoints silently 404 them). Override to a regional value
      +        # (e.g. "us-central1") only if your models are pinned to a region.
      +        "region": "global",
      +    },
      +
           # Config schema version - bump this when adding new required fields
           "_config_version": 32,
       }
      @@ -3155,6 +3173,18 @@ def _ensure_hermes_home_managed(home: Path):
               "category": "provider",
               "advanced": True,
           },
      +    "VERTEX_CREDENTIALS_PATH": {
      +        "description": "Path to a Google Cloud service account JSON for Vertex AI (Gemini). "
      +                       "Vertex uses OAuth2, not a static API key — this points at the "
      +                       "credentials Hermes mints short-lived tokens from. Falls back to "
      +                       "GOOGLE_APPLICATION_CREDENTIALS, then to ADC (gcloud auth "
      +                       "application-default login). Set project/region under vertex: in config.yaml.",
      +        "prompt": "Vertex service account JSON path (leave empty to use ADC / GOOGLE_APPLICATION_CREDENTIALS)",
      +        "url": "https://cloud.google.com/iam/docs/keys-create-delete",
      +        "password": [redacted:secret-assignment]
      +        "category": "provider",
      +        "advanced": True,
      +    },
           "XAI_API_KEY": {
               "description": "xAI API key",
               "prompt": "xAI API key",
    • hermes_cli/main.py
      @@ -616,6 +616,7 @@ def _resolve_sudo_user_profile_env(name: str) -> str | None:
           _model_flow_stepfun,
           _model_flow_bedrock_api_key,
           _model_flow_bedrock,
      +    _model_flow_vertex,
           _model_flow_api_key_provider,
           _model_flow_anthropic,
           _model_flow_moa,
      @@ -3109,6 +3110,8 @@ def _active_custom_key_from_base_url() -> str:
               _model_flow_stepfun(config, current_model)
           elif selected_provider == "bedrock":
               _model_flow_bedrock(config, current_model)
      +    elif selected_provider == "vertex":
      +        _model_flow_vertex(config, current_model)
           elif selected_provider == "azure-foundry":
               _model_flow_azure_foundry(config, current_model)
           elif selected_provider in {
      @@ -11902,7 +11905,7 @@ def _build_provider_choices() -> list[str]:
               # Fallback: static list guarantees the CLI always works
               return [
                   "auto", "openrouter", "nous", "openai-codex", "xai-oauth", "copilot-acp", "copilot",
      -            "anthropic", "gemini", "xai", "bedrock", "azure-foundry",
      +            "anthropic", "gemini", "vertex", "xai", "bedrock", "azure-foundry",
                   "ollama-cloud", "huggingface", "zai", "kimi-coding", "kimi-coding-cn",
                   "stepfun", "minimax", "minimax-cn", "kilocode", "novita", "xiaomi", "arcee",
                   "nvidia", "deepseek", "alibaba", "qwen-oauth", "opencode-zen", "opencode-go",
    • hermes_cli/model_setup_flows.py
      @@ -2316,6 +2316,110 @@ def _sort_key(m):
           else:
               print("  No change.")
       
      +
      +def _model_flow_vertex(config, current_model=""):
      +    """Google Vertex AI provider: Gemini via the OpenAI-compatible endpoint.
      +
      +    Auth is OAuth2 — short-lived tokens minted from a service-account JSON or
      +    Application Default Credentials (ADC). No static API key. The credential
      +    *path* lives in .env (VERTEX_CREDENTIALS_PATH / GOOGLE_APPLICATION_CREDENTIALS);
      +    project ID and region are non-secret and saved to config.yaml under vertex:.
      +    """
      +    from hermes_cli.auth import (
      +        _prompt_model_selection,
      +        _save_model_choice,
      +        deactivate_provider,
      +    )
      +    from hermes_cli.config import load_config, save_config, get_env_value
      +    from hermes_cli.models import _PROVIDER_MODELS
      +
      +    # 1. Credential source detection (fast, no network / no google-auth import).
      +    sa_path = (
      +        get_env_value("VERTEX_CREDENTIALS_PATH")
      +        or get_env_value("GOOGLE_APPLICATION_CREDENTIALS")
      +        or ""
      +    ).strip()
      +    if sa_path:
      +        print(f"  Vertex credentials: service account JSON ({sa_path}) ✓")
      +    else:
      +        print("  Vertex credentials: Application Default Credentials (ADC)")
      +        print("    Vertex uses OAuth2, not a static API key. Either:")
      +        print("      • run 'gcloud auth application-default login', or")
      +        print("      • set VERTEX_CREDENTIALS_PATH in ~/.hermes/.env to a service account JSON")
      +    print()
      +
      +    cfg = load_config()
      +    vertex_cfg = cfg.get("vertex")
      +    if not isinstance(vertex_cfg, dict):
      +        vertex_cfg = {}
      +
      +    # 2. Project ID (optional — falls back to the project embedded in creds).
      +    current_project = str(vertex_cfg.get("project_id") or "").strip()
      +    try:
      +        project_input = input(
      +            f"  GCP project ID [{current_project or 'from credentials'}]: "
      +        ).strip()
      +    except (KeyboardInterrupt, EOFError):
      +        print()
      +        return
      +    project_id = project_input or current_project
      +
      +    # 3. Region (default global — required for the Gemini 3.x previews).
      +    current_region = str(vertex_cfg.get("region") or "global").strip() or "global"
      +    try:
      +        region_input = input(f"  Vertex region [{current_region}]: ").strip()
      +    except (KeyboardInterrupt, EOFError):
      +        print()
      +        return
      +    region = region_input or current_region
      +
      +    # 4. Model selection (curated list — Vertex has no /models listing route).
      +    model_list = _PROVIDER_MODELS.get("vertex", []) or [
      +        "google/gemini-3-pro-preview",
      +        "google/gemini-3-flash-preview",
      +    ]
      +    base_url_preview = (
      +        "https://aiplatform.googleapis.com/v1beta1/projects/<project>/"
      +        f"locations/{region}/endpoints/openapi"
      +        if region == "global"
      +        else f"https://{region}-aiplatform.googleapis.com/v1beta1/projects/<project>/"
      +        f"locations/{region}/endpoints/openapi"
      +    )
      +    selected = _prompt_model_selection(
      +        model_list,
      +        current_model=current_model,
      +        confirm_provider="vertex",
      +        confirm_base_url=base_url_preview,
      +    )
      +
      +    if selected:
      +        _save_model_choice(selected)
      +
      +        cfg = load_config()
      +        model = cfg.get("model")
      +        if not isinstance(model, dict):
      +            model = {"default": model} if model else {}
      +            cfg["model"] = model
      +        model["provider"] = "vertex"
      +        # base_url is computed at runtime from project+region; do not pin it.
      +        model.pop("base_url", None)
      +        model.pop("api_mode", None)  # chat_completions is the profile default
      +        clear_model_endpoint_credentials(model, clear_api_mode=False)
      +
      +        vcfg = cfg.get("vertex")
      +        if not isinstance(vcfg, dict):
      +            vcfg = {}
      +        vcfg["project_id"] = project_id
      +        vcfg["region"] = region
      +        cfg["vertex"] = vcfg
      +
      +        save_config(cfg)
      +        deactivate_provider()
      +
      +        print(f"  Default model set to: {selected} (via Google Vertex AI, {region})")
      +    else:
      +        print("  No change.")
      +
       def _select_zai_endpoint(current_base: str) -> str:
           """Present a picker for Z.AI endpoint selection during setup.
       
    • hermes_cli/models.py
      @@ -1032,6 +1032,7 @@ class ProviderEntry(NamedTuple):
           ProviderEntry("copilot-acp",    "GitHub Copilot ACP",       "GitHub Copilot ACP (Spawns copilot --acp --stdio)"),
           ProviderEntry("huggingface",    "Hugging Face",             "Hugging Face Inference Providers"),
           ProviderEntry("gemini",         "Google AI Studio",         "Google AI Studio (Native Gemini API)"),
      +    ProviderEntry("vertex",         "Google Vertex AI",         "Google Vertex AI (Gemini via GCP; OAuth2 service account or ADC, GCP billing/quotas)"),
           ProviderEntry("deepseek",       "DeepSeek",                 "DeepSeek (V3, R1, coder, direct API)"),
           ProviderEntry("xai",            "xAI",                      "xAI Grok (Direct API)"),
           ProviderEntry("zai",            "Z.AI / GLM",               "Z.AI / GLM (Zhipu direct API)"),
      @@ -1062,7 +1063,7 @@ class ProviderEntry(NamedTuple):
           for _pp in _list_providers_for_canonical():
               if _pp.name in _canonical_slugs:
                   continue
      -        if _pp.auth_type in {"oauth_device_code", "oauth_external", "external_process", "aws_sdk", "copilot"}:
      +        if _pp.auth_type in {"oauth_device_code", "oauth_external", "external_process", "aws_sdk", "copilot", "vertex"}:
                   continue  # non-api-key flows need bespoke picker UX; skip auto-inject
               _label = _pp.display_name or _pp.name
               _desc = _pp.description or f"{_label} (direct API)"
      @@ -1193,6 +1194,10 @@ def group_providers(slugs):
           "google": "gemini",
           "google-gemini": "gemini",
           "google-ai-studio": "gemini",
      +    "google-vertex": "vertex",
      +    "vertex-ai": "vertex",
      +    "gcp-vertex": "vertex",
      +    "vertexai": "vertex",
           "kimi": "kimi-coding",
           "moonshot": "kimi-coding",
           "kimi-cn": "kimi-coding-cn",
    • hermes_cli/runtime_provider.py
      @@ -1540,6 +1540,39 @@ def resolve_runtime_provider(
               )
               return azure_runtime
       
      +    # Vertex AI: OAuth2-token provider (Gemini via the OpenAI-compatible
      +    # endpoint). Resolve BEFORE the custom-runtime / credential-pool / generic
      +    # paths. The credential *path* (GOOGLE_APPLICATION_CREDENTIALS /
      +    # VERTEX_CREDENTIALS_PATH) must never reach the credential pool or the
      +    # generic api_key resolver — those would treat the file path as a static
      +    # API key. Instead we mint a short-lived OAuth2 access token here and hand
      +    # it to the standard OpenAI client as api_key, with base_url computed from
      +    # the project ID + region. The token is re-minted per call (5-min refresh
      +    # margin) by get_vertex_config(); mid-session expiry is additionally
      +    # recovered on 401 by run_agent._try_refresh_vertex_client_credentials().
      +    if requested_provider in ("vertex", "google-vertex", "vertex-ai", "gcp-vertex", "vertexai"):
      +        from agent.vertex_adapter import get_vertex_config
      +
      +        token, base_url = get_vertex_config()
      +        if not token or not base_url:
      +            raise AuthError(
      +                "Vertex AI credentials could not be resolved. Vertex uses "
      +                "OAuth2 (not a static API key): provide a service-account JSON "
      +                "via GOOGLE_APPLICATION_CREDENTIALS (or VERTEX_CREDENTIALS_PATH) "
      +                "in ~/.hermes/.env, or run 'gcloud auth application-default "
      +                "login' for ADC. Set the GCP project/region under vertex: in "
      +                "config.yaml if they aren't embedded in the credentials. "
      +                "Install the extra with: pip install 'hermes-agent[vertex]'."
      +            )
      +        return {
      +            "provider": "vertex",
      +            "api_mode": "chat_completions",
      +            "base_url": base_url.rstrip("/"),
      +            "api_key": [redacted:secret-assignment]
      +            "source": "vertex-oauth",
      +            "requested_provider": requested_provider,
      +        }
      +
           custom_runtime = _resolve_named_custom_runtime(
               requested_provider=requested_provider,
               explicit_api_key=[redacted:secret-assignment]
    • hermes_cli/setup.py
      @@ -93,6 +93,11 @@ def _supports_same_provider_pool_setup(provider: str) -> bool:
               "gemini-3.1-pro-preview", "gemini-3-pro-preview",
               "gemini-3-flash-preview", "gemini-3.1-flash-lite-preview",
           ],
      +    "vertex": [
      +        "google/gemini-3.1-pro-preview", "google/gemini-3-pro-preview",
      +        "google/gemini-3-flash-preview", "google/gemini-3.1-flash-lite-preview",
      +        "google/gemini-2.5-pro", "google/gemini-2.5-flash",
      +    ],
           "zai": ["glm-5.2", "glm-5.1", "glm-5", "glm-4.7", "glm-4.5", "glm-4.5-flash"],
           "kimi-coding": ["kimi-k2.6", "kimi-k2.5", "kimi-k2-thinking", "kimi-k2-turbo-preview"],
           "kimi-coding-cn": ["kimi-k2.6", "kimi-k2.5", "kimi-k2-thinking", "kimi-k2-turbo-preview"],
    • hermes_cli/web_server.py
      @@ -4846,6 +4846,25 @@ def _catalog_provider_env_metadata() -> dict:
                           "advanced": existing.get("advanced", True),
                           "category": "provider",
                       }
      +
      +        # Vertex AI authenticates via OAuth2 (service-account JSON or ADC), not a
      +        # pasted API key, so it also has no api_key_env_vars. Tag its credential
      +        # env var to the provider card so it appears on the Keys tab (otherwise
      +        # Vertex — a `hermes model` provider — would be invisible in the desktop
      +        # app). The value is a filesystem path, not a secret string, so it is
      +        # not a password field.
      +        if d.auth_type == "vertex":
      +            existing = meta.get("VERTEX_CREDENTIALS_PATH", {})
      +            meta["VERTEX_CREDENTIALS_PATH"] = {
      +                "provider": d.slug,
      +                "provider_label": d.label,
      +                "description": existing.get("description")
      +                or f"{d.label} — service account JSON path (or use ADC)",
      +                "url": existing.get("url"),
      +                "is_password": [redacted:secret-assignment]
      +                "advanced": existing.get("advanced", True),
      +                "category": "provider",
      +            }
           return meta
       
       
    • plugins/model-providers/vertex/__init__.py
      @@ -0,0 +1,75 @@
      +"""Google Vertex AI provider profile.
      +
      +vertex: Gemini models via Google Cloud's OpenAI-compatible endpoint.
      +
      +Auth is OAuth2 — short-lived access tokens minted from a service-account JSON
      +or Application Default Credentials (ADC), NOT a static API key. Token
      +resolution and refresh live in ``agent/vertex_adapter.py``; runtime_provider.py
      +calls it to obtain a fresh ``(token, base_url)`` pair, then hands the token to
      +the standard OpenAI client as ``api_key``. Because the wire format is the
      +OpenAI-compatible chat/completions surface, no message translation is needed —
      +the only Gemini-specific concern is the ``thinking_config`` reasoning hook,
      +which is emitted here exactly as the ``gemini`` provider does for its
      +OpenAI-compat subpath (``extra_body.google.thinking_config``).
      +
      +``auth_type="vertex"`` marks this as an OAuth-token provider (resolved
      +specially, like bedrock's ``aws_sdk``) so it is never treated as an
      +api_key provider that would mistake a credentials-file path for a key.
      +"""
      +
      +from typing import Any
      +
      +from providers import register_provider
      +from providers.base import ProviderProfile
      +
      +
      +class VertexProfile(ProviderProfile):
      +    """Vertex AI — reuse Gemini's thinking_config translation for extra_body."""
      +
      +    def build_extra_body(
      +        self, *, session_id: str | None = None, **context: Any
      +    ) -> dict[str, Any]:
      +        """Emit ``extra_body.google.thinking_config`` for the OpenAI-compat
      +        Vertex surface, mirroring the ``gemini`` provider's behavior.
      +        """
      +        from agent.transports.chat_completions import (
      +            _build_gemini_thinking_config,
      +            _snake_case_gemini_thinking_config,
      +        )
      +
      +        model = context.get("model") or ""
      +        reasoning_config = context.get("reasoning_config")
      +
      +        raw_thinking_config = _build_gemini_thinking_config(model, reasoning_config)
      +        if not raw_thinking_config:
      +            return {}
      +
      +        thinking_config = _snake_case_gemini_thinking_config(raw_thinking_config)
      +        if not thinking_config:
      +            return {}
      +        return {"extra_body": {"google": {"thinking_config": thinking_config}}}
      +
      +    def fetch_models(
      +        self,
      +        *,
      +        api_key: str | None = None,
      +        base_url: str | None = None,
      +        timeout: float = 8.0,
      +    ) -> list[str] | None:
      +        """Vertex's OpenAI-compat endpoint has no ``/models`` listing route;
      +        model discovery is not available. The setup wizard ships a curated list.
      +        """
      +        return None
      +
      +
      +vertex = VertexProfile(
      +    name="vertex",
      +    aliases=("google-vertex", "vertex-ai", "gcp-vertex"),
      +    api_mode="chat_completions",
      +    env_vars=(),  # OAuth2 via service account / ADC — not a static key env var
      +    base_url="https://aiplatform.googleapis.com",  # real base_url computed at runtime
      +    auth_type="vertex",
      +    default_aux_model="google/gemini-3-flash-preview",
      +)
      +
      +register_provider(vertex)
    • plugins/model-providers/vertex/plugin.yaml
      @@ -0,0 +1,5 @@
      +name: vertex-provider
      +kind: model-provider
      +version: 1.0.0
      +description: Google Vertex AI (Gemini via OpenAI-compatible endpoint, OAuth2)
      +author: Steve Lawton (@slawt), Hermes Agent
    • pyproject.toml
      @@ -225,6 +225,7 @@ acp = ["agent-client-protocol==0.9.0"]
       # installs (see [all] policy comment below).
       mistral = ["mistralai==2.4.8"]
       bedrock = ["boto3==1.42.89"]
      +vertex = ["google-auth==2.55.1"]
       azure-identity = ["azure-identity==1.25.3"]
       termux = [
         # Baseline Android / Termux path for reliable fresh installs.
    • run_agent.py
      @@ -4185,6 +4185,43 @@ def _try_refresh_nous_client_credentials(
       
               return True
       
      +    def _try_refresh_vertex_client_credentials(self) -> bool:
      +        """Re-mint the Vertex OAuth2 access token and rebuild the OpenAI client.
      +
      +        Vertex tokens live ~1 hour. On a long-lived agent (gateway session) a
      +        cached client's bearer token will expire mid-session, producing a 401.
      +        This re-resolves credentials via the adapter (which refreshes the
      +        underlying google-auth Credentials object when near expiry), swaps the
      +        new token into the client kwargs, and rebuilds the primary OpenAI
      +        client. Returns True when a usable token+base_url were obtained.
      +        """
      +        if self.api_mode != "chat_completions" or self.provider != "vertex":
      +            return False
      +
      +        try:
      +            from agent.vertex_adapter import get_vertex_config
      +
      +            token, base_url = get_vertex_config()
      +        except Exception as exc:
      +            logger.debug("Vertex credential refresh failed: %s", exc)
      +            return False
      +
      +        if not isinstance(token, str) or not token.strip():
      +            return False
      +        if not isinstance(base_url, str) or not base_url.strip():
      +            return False
      +
      +        self.api_key = token.strip()
      +        self.base_url = base_url.strip().rstrip("/")
      +        self._client_kwargs["api_key"] = self.api_key
      +        self._client_kwargs["base_url"] = self.base_url
      +
      +        if not self._replace_primary_openai_client(reason="vertex_credential_refresh"):
      +            return False
      +
      +        logger.info("Vertex AI OAuth token refreshed")
      +        return True
      +
           def _try_refresh_copilot_client_credentials(self) -> bool:
               """Refresh Copilot credentials and rebuild the shared OpenAI client.
       
      @@ -5093,7 +5130,7 @@ def _anthropic_preserve_dots(self) -> bool:
                   "alibaba", "minimax", "minimax-cn",
                   "opencode-go", "opencode-zen",
                   "zai", "bedrock",
      -            "xiaomi",
      +            "xiaomi", "vertex",
               }:
                   return True
               base = (getattr(self, "base_url", "") or "").lower()
      @@ -5104,6 +5141,9 @@ def _anthropic_preserve_dots(self) -> bool:
                   or "opencode.ai/zen/" in base
                   or "bigmodel.cn" in base
                   or "xiaomimimo.com" in base
      +            # Vertex AI OpenAI-compat endpoint — Gemini model ids keep dots
      +            # (e.g. google/gemini-3.5-flash); the hyphenated form is wrong.
      +            or "aiplatform.googleapis.com" in base
                   # AWS Bedrock runtime endpoints — defense-in-depth when
                   # ``provider`` is unset but ``base_url`` still names Bedrock.
                   or "bedrock-runtime." in base
    • scripts/release.py
      @@ -47,6 +47,7 @@
       AUTHOR_MAP = {
           "[redacted:email]": "udatny",  # PR #29433 salvage (subdirectory_hints: catch RuntimeError from Path.expanduser()/Path.home() so a literal ~ in tool-call args — e.g. LLM "~500-700" or ~unknownuser — can't escape the hint walker and crash the conversation loop)
           "[redacted:email]": "valenteff",  # PR #53277 salvage (macOS launchd reload: retry bootstrap via _launchctl_bootstrap until launchctl-list confirms registration or the restart-drain window elapses; retry TimeoutExpired not just CalledProcessError; log persistent orphans)
      +    "[redacted:email]": "slawt",  # PR #8427 salvage (Google Vertex AI provider for Gemini: OAuth2 token minting via service-account JSON / ADC on the OpenAI-compat endpoint, rewired as a provider profile with per-turn 401 token refresh)
           "[redacted:email]": "bitcryptic-gw",  # PR #53997 salvage (Matrix E2EE: resolve device_id via query_keys({mxid: []}) when whoami returns none; guard verification call sites so query_keys is never sent [null]; reset _device_id_unverified at connect() start; disconnect before reconnect)
           "[email protected]": "abchiaravalle",  # PR #46997 salvage (recover resume_pending sessions: dual freshness signal + empty-turn safety net so restart auto-resume never sends a blank user turn)
           "[email protected]": "swissly",  # PR #47167 salvage (wrap cron delivery thread-pool fallback in its own try/except so a per-target failure can't escape the except-RuntimeError block and crash the multi-target delivery loop; #47163)
    • tests/agent/test_turn_retry_state.py
      @@ -19,6 +19,7 @@
           "nous_auth_retry_attempted",
           "nous_paid_entitlement_refresh_attempted",
           "copilot_auth_retry_attempted",
      +    "vertex_auth_retry_attempted",
           "thinking_sig_retry_attempted",
           "invalid_encrypted_content_retry_attempted",
           "image_shrink_retry_attempted",
    • tests/agent/test_vertex_adapter.py
      @@ -0,0 +1,169 @@
      +"""Tests for the Vertex AI adapter (agent/vertex_adapter.py).
      +
      +Vertex uses OAuth2 (short-lived access tokens from a service-account JSON or
      +ADC), NOT a static API key. These tests mock google-auth entirely — no network
      +calls — and cover token minting, the config.yaml→env precedence bridge, the
      +global vs regional base-URL shapes, and the ADC→service-account fallback.
      +"""
      +
      +from __future__ import annotations
      +
      +import importlib
      +import sys
      +import types
      +
      +import pytest
      +
      +
      +def _install_fake_google_auth(monkeypatch, *, adc_ok=True, adc_project="adc-project",
      +                              sa_project="sa-project", token="[redacted:secret-assignment]"):
      +    """Register a fake google-auth tree in sys.modules and return the module set."""
      +    ga = types.ModuleType("google.auth")
      +    gt = types.ModuleType("google.auth.transport")
      +    gtr = types.ModuleType("google.auth.transport.requests")
      +    go = types.ModuleType("google.oauth2")
      +    gsa = types.ModuleType("google.oauth2.service_account")
      +    gp = types.ModuleType("google")
      +
      +    gtr.Request = type("Request", (), {})
      +
      +    class _Creds:
      +        def __init__(self):
      +            self.token = None
      +            self.expiry = None
      +            self.expired = False
      +
      +        def refresh(self, req):
      +            self.token = token
      +
      +    def _default(scopes=None):
      +        if not adc_ok:
      +            raise RuntimeError("Could not automatically determine credentials")
      +        return _Creds(), adc_project
      +
      +    ga.default = _default
      +    ga.transport = gt
      +    gt.requests = gtr
      +
      +    class _SA:
      +        @staticmethod
      +        def from_service_account_file(path, scopes=None):
      +            c = _Creds()
      +            c.project_id = sa_project
      +            return c
      +
      +    gsa.Credentials = _SA
      +    go.service_account = gsa
      +    gp.auth = ga
      +    gp.oauth2 = go
      +
      +    for name, mod in [
      +        ("google", gp), ("google.auth", ga), ("google.auth.transport", gt),
      +        ("google.auth.transport.requests", gtr), ("google.oauth2", go),
      +        ("google.oauth2.service_account", gsa),
      +    ]:
      +        monkeypatch.setitem(sys.modules, name, mod)
      +    return gp
      +
      +
      [redacted:email]
      +def vertex_adapter(monkeypatch):
      +    """Fresh vertex_adapter with a fake google-auth and clean caches/env."""
      +    for var in ("VERTEX_CREDENTIALS_PATH", "GOOGLE_APPLICATION_CREDENTIALS",
      +                "VERTEX_PROJECT_ID", "VERTEX_REGION", "GOOGLE_CLOUD_PROJECT"):
      +        monkeypatch.delenv(var, raising=False)
      +    _install_fake_google_auth(monkeypatch)
      +    import agent.vertex_adapter as va
      +    va = importlib.reload(va)
      +    va._creds_cache.clear()
      +    # Neutralize config.yaml by default; individual tests re-patch _vertex_config.
      +    monkeypatch.setattr(va, "_vertex_config", lambda: {})
      +    return va
      +
      +
      +def test_build_base_url_global(vertex_adapter):
      +    url = vertex_adapter.build_vertex_base_url("proj", "global")
      +    assert url == (
      +        "https://aiplatform.googleapis.com/v1beta1/projects/proj/"
      +        "locations/global/endpoints/openapi"
      +    )
      +
      +
      +def test_build_base_url_regional(vertex_adapter):
      +    url = vertex_adapter.build_vertex_base_url("proj", "us-central1")
      +    assert url == (
      +        "https://us-central1-aiplatform.googleapis.com/v1beta1/projects/proj/"
      +        "locations/us-central1/endpoints/openapi"
      +    )
      +
      +
      +def test_get_vertex_config_uses_adc_and_default_region(vertex_adapter):
      +    token, base = vertex_adapter.get_vertex_config()
      +    assert token == "ya29.FAKE"
      +    assert base == (
      +        "https://aiplatform.googleapis.com/v1beta1/projects/adc-project/"
      +        "locations/global/endpoints/openapi"
      +    )
      +
      +
      +def test_config_yaml_supplies_project_and_region(vertex_adapter, monkeypatch):
      +    monkeypatch.setattr(
      +        vertex_adapter, "_vertex_config",
      +        lambda: {"project_id": "cfg-project", "region": "europe-west4"},
      +    )
      +    token, base = vertex_adapter.get_vertex_config()
      +    assert token == "ya29.FAKE"
      +    assert "projects/cfg-project" in base
      +    assert "europe-west4-aiplatform.googleapis.com" in base
      +    assert "locations/europe-west4" in base
      +
      +
      +def test_env_overrides_config_yaml(vertex_adapter, monkeypatch):
      +    monkeypatch.setattr(
      +        vertex_adapter, "_vertex_config",
      +        lambda: {"project_id": "cfg-project", "region": "cfg-region"},
      +    )
      +    monkeypatch.setenv("VERTEX_PROJECT_ID", "env-project")
      +    monkeypatch.setenv("VERTEX_REGION", "us-east4")
      +    assert vertex_adapter._resolve_project_override() == "env-project"
      +    assert vertex_adapter._resolve_region() == "us-east4"
      +
      +
      +def test_has_vertex_credentials_via_config_project(vertex_adapter, monkeypatch):
      +    monkeypatch.setattr(vertex_adapter, "_vertex_config", lambda: {"project_id": "p"})
      +    assert vertex_adapter.has_vertex_credentials() is True
      +
      +
      +def test_has_vertex_credentials_false_when_nothing_set(vertex_adapter):
      +    assert vertex_adapter.has_vertex_credentials() is False
      +
      +
      +def test_missing_google_auth_returns_none(monkeypatch):
      +    for var in ("VERTEX_CREDENTIALS_PATH", "GOOGLE_APPLICATION_CREDENTIALS",
      +                "VERTEX_PROJECT_ID", "VERTEX_REGION"):
      +        monkeypatch.delenv(var, raising=False)
      +    import agent.vertex_adapter as va
      +    va = importlib.reload(va)
      +    monkeypatch.setattr(va, "google", None)
      +    va._creds_cache.clear()
      +    assert va.get_vertex_credentials() == (None, None)
      +
      +
      +def test_adc_failure_falls_back_to_service_account(monkeypatch, tmp_path):
      +    """When ADC refresh fails but a service-account JSON exists, use the SA."""
      +    for var in ("VERTEX_PROJECT_ID", "VERTEX_REGION", "GOOGLE_CLOUD_PROJECT"):
      +        monkeypatch.delenv(var, raising=False)
      +    sa_file = tmp_path / "sa.json"
      +    sa_file.write_text('{"project_id": "sa-project"}')
      +    monkeypatch.setenv("GOOGLE_APPLICATION_CREDENTIALS", str(sa_file))
      +    monkeypatch.delenv("VERTEX_CREDENTIALS_PATH", raising=False)
      +    _install_fake_google_auth(monkeypatch, adc_ok=False)
      +    import agent.vertex_adapter as va
      +    va = importlib.reload(va)
      +    va._creds_cache.clear()
      +    monkeypatch.setattr(va, "_vertex_config", lambda: {})
      +    # A resolvable SA path means the primary cache key is the file (not __adc__),
      +    # so this exercises the direct-SA path.
      +    token, project = va.get_vertex_credentials()
      +    assert token == "ya29.FAKE"
      +    assert project == "sa-project"
    • tests/hermes_cli/test_vertex_provider.py
      @@ -0,0 +1,100 @@
      +"""Tests for Vertex AI runtime-provider resolution and profile registration.
      +
      +Covers: provider-profile registration + aliases, alias canonicalization,
      +resolve_runtime_provider(vertex) minting an OAuth token, and the friendly
      +AuthError when credentials can't be resolved. No network calls.
      +"""
      +
      +from __future__ import annotations
      +
      +import pytest
      +
      +
      +def test_vertex_profile_registered():
      +    from providers import get_provider_profile
      +
      +    p = get_provider_profile("vertex")
      +    assert p is not None
      +    assert p.name == "vertex"
      +    assert p.api_mode == "chat_completions"
      +    assert p.auth_type == "vertex"
      +
      +
      [redacted:email]("alias", ["google-vertex", "vertex-ai", "gcp-vertex"])
      +def test_vertex_aliases_resolve(alias):
      +    from providers import get_provider_profile
      +
      +    assert get_provider_profile(alias).name == "vertex"
      +
      +
      [redacted:email]("alias", ["google-vertex", "vertex-ai", "gcp-vertex", "vertexai"])
      +def test_alias_canonicalizes_to_vertex(alias):
      +    from hermes_cli.models import _PROVIDER_ALIASES
      +
      +    assert _PROVIDER_ALIASES[alias] == "vertex"
      +
      +
      +def test_google_vertex_not_confused_with_gemini():
      +    """`google-vertex` must map to vertex, not the AI-Studio `gemini` provider."""
      +    from hermes_cli.models import _PROVIDER_ALIASES
      +
      +    assert _PROVIDER_ALIASES["google-vertex"] == "vertex"
      +    assert _PROVIDER_ALIASES["google-gemini"] == "gemini"
      +
      +
      +def test_resolve_runtime_provider_mints_token(monkeypatch):
      +    import agent.vertex_adapter as va
      +    from hermes_cli import runtime_provider as rp
      +
      +    monkeypatch.setattr(
      +        va, "get_vertex_config",
      +        lambda: ("ya29.TOKEN", "https://aiplatform.googleapis.com/v1beta1/projects/p/locations/global/endpoints/openapi"),
      +    )
      +    rt = rp.resolve_runtime_provider(requested="vertex")
      +    assert rt["provider"] == "vertex"
      +    assert rt["api_mode"] == "chat_completions"
      +    assert rt["source"] == "vertex-oauth"
      +    assert rt["api_key"] == "ya29.TOKEN"
      +    assert "aiplatform.googleapis.com" in rt["base_url"]
      +
      +
      +def test_resolve_runtime_provider_alias(monkeypatch):
      +    import agent.vertex_adapter as va
      +    from hermes_cli import runtime_provider as rp
      +
      +    monkeypatch.setattr(va, "get_vertex_config", lambda: ("t", "https://aiplatform.googleapis.com/v1beta1/projects/p/locations/global/endpoints/openapi"))
      +    rt = rp.resolve_runtime_provider(requested="google-vertex")
      +    assert rt["provider"] == "vertex"
      +
      +
      +def test_resolve_runtime_provider_raises_autherror_when_unresolved(monkeypatch):
      +    import agent.vertex_adapter as va
      +    from hermes_cli import runtime_provider as rp
      +    from hermes_cli.auth import AuthError
      +
      +    monkeypatch.setattr(va, "get_vertex_config", lambda: (None, None))
      +    with pytest.raises(AuthError) as exc:
      +        rp.resolve_runtime_provider(requested="vertex")
      +    msg = str(exc.value)
      +    assert "OAuth2" in msg
      +    assert "not a static API key" in msg
      +
      +
      +def test_vertex_extra_body_thinking_config():
      +    from providers import get_provider_profile
      +
      +    p = get_provider_profile("vertex")
      +    body = p.build_extra_body(
      +        model="google/gemini-3-pro-preview",
      +        reasoning_config={"effort": "high"},
      +    )
      +    assert "extra_body" in body
      +    assert "google" in body["extra_body"]
      +    assert "thinking_config" in body["extra_body"]["google"]
      +
      +
      +def test_vertex_extra_body_empty_without_reasoning():
      +    from providers import get_provider_profile
      +
      +    p = get_provider_profile("vertex")
      +    assert p.build_extra_body(model="google/gemini-3-flash-preview") == {}
    • tools/lazy_deps.py
      @@ -99,6 +99,10 @@
           "provider.anthropic": ("anthropic==0.87.0",),  # CVE-2026-34450, CVE-2026-34452
           # AWS Bedrock provider
           "provider.bedrock": ("boto3==1.42.89",),
      +    # Google Vertex AI provider — OAuth2 token minting for the Gemini
      +    # OpenAI-compatible endpoint. Only loaded when provider=vertex is selected;
      +    # google-auth is NOT in [all] so plain installs don't carry it.
      +    "provider.vertex": ("google-auth==2.55.1",),
           # Microsoft Foundry — Entra ID auth (managed identity, workload identity,
           # service principal, az login, VS Code, azd, PowerShell). Only loaded
           # when model.auth_mode=entra_id is selected; key-based azure-foundry
    • uv.lock
      @@ -1357,15 +1357,15 @@ wheels = [
       
       [[package]]
       name = "google-auth"
      -version = "2.49.2"
      +version = "2.55.1"
       source = { registry = "https://pypi.org/simple" }
       dependencies = [
           { name = "cryptography" },
           { name = "pyasn1-modules" },
       ]
      -sdist = { url = "https://files.pythonhosted.org/packages/c6/fc/e925290a1ad95c975c459e2df070fac2b90954e13a0370ac505dff78cb99/google_auth-2.49.2.tar.gz", hash = "sha256:c1ae38500e73065dcae57355adb6278cf8b5c8e391994ae9cbadbcb9631ab409", size = 333958, upload-time = "2026-04-10T00:41:21.888Z" }
      +sdist = { url = "https://files.pythonhosted.org/packages/a3/6f/f3f4ac177c67bbee8fe8e88f2ab4f36af88c44a096e165c5217accf6e5d3/google_auth-2.55.1.tar.gz", hash = "sha256:fb2d9b730f2c9b8d326ec8d7222f21aef2ead15bf0513793d6442485d87af0a1", size = 349527, upload-time = "2026-06-25T23:39:27.182Z" }
       wheels = [
      -    { url = "https://files.pythonhosted.org/packages/73/76/d241a5c927433420507215df6cac1b1fa4ac0ba7a794df42a84326c68da8/google_auth-2.49.2-py3-none-any.whl", hash = "sha256:c2720924dfc82dedb962c9f52cabb2ab16714fd0a6a707e40561d217574ed6d5", size = 240638, upload-time = "2026-04-10T00:41:14.501Z" },
      +    { url = "https://files.pythonhosted.org/packages/e8/1d/f6d3ca1ad0725f2e08a1c6915640748a52de2e66596160a4d53b010cccf0/google_auth-2.55.1-py3-none-any.whl", hash = "sha256:eada68dfd52b3b81191827601e2a0c3fa12540c818534b630ddc5355769c3995", size = 252349, upload-time = "2026-06-25T23:38:52.946Z" },
       ]
       
       [[package]]
      @@ -1709,6 +1709,9 @@ termux-all = [
       tts-premium = [
           { name = "elevenlabs" },
       ]
      +vertex = [
      +    { name = "google-auth" },
      +]
       voice = [
           { name = "faster-whisper" },
           { name = "numpy" },
      @@ -1763,6 +1766,7 @@ requires-dist = [
           { name = "fire", specifier = "==0.7.1" },
           { name = "firecrawl-py", marker = "extra == 'firecrawl'", specifier = "==4.17.0" },
           { name = "google-api-python-client", marker = "extra == 'google'", specifier = "==2.194.0" },
      +    { name = "google-auth", marker = "extra == 'vertex'", specifier = "==2.55.1" },
           { name = "google-auth-httplib2", marker = "extra == 'google'", specifier = "==0.3.1" },
           { name = "google-auth-oauthlib", marker = "extra == 'google'", specifier = "==1.3.1" },
           { name = "hermes-agent", extras = ["acp"], marker = "extra == 'all'" },
      @@ -1849,7 +1853,7 @@ requires-dist = [
           { name = "websockets", specifier = "==15.0.1" },
           { name = "youtube-transcript-api", marker = "extra == 'youtube'", specifier = "==1.2.4" },
       ]
      -provides-extras = ["anthropic", "exa", "firecrawl", "parallel-web", "fal", "edge-tts", "modal", "daytona", "hindsight", "dev", "messaging", "cron", "slack", "matrix", "wecom", "cli", "tts-premium", "voice", "pty", "honcho", "supermemory", "mem0", "vision", "mcp", "nemo-relay", "homeassistant", "sms", "teams", "computer-use", "acp", "mistral", "bedrock", "azure-identity", "termux", "termux-all", "dingtalk", "feishu", "google", "youtube", "web", "all"]
      +provides-extras = ["anthropic", "exa", "firecrawl", "parallel-web", "fal", "edge-tts", "modal", "daytona", "hindsight", "dev", "messaging", "cron", "slack", "matrix", "wecom", "cli", "tts-premium", "voice", "pty", "honcho", "supermemory", "mem0", "vision", "mcp", "nemo-relay", "homeassistant", "sms", "teams", "computer-use", "acp", "mistral", "bedrock", "vertex", "azure-identity", "termux", "termux-all", "dingtalk", "feishu", "google", "youtube", "web", "all"]
       
       [[package]]
       name = "hf-xet"
    • website/docs/guides/google-vertex.md
      @@ -0,0 +1,146 @@
      +---
      +sidebar_position: 15
      +title: "Google Vertex AI"
      +description: "Use Hermes Agent with Gemini on Google Cloud Vertex AI — OAuth2 service account or ADC, GCP billing and quotas, no static API key"
      +---
      +
      +# Google Vertex AI
      +
      +Hermes Agent supports **Gemini models on Google Cloud Vertex AI** through Vertex's OpenAI-compatible endpoint. Unlike the [Google AI Studio provider](/guides/google-gemini) (which uses a static API key against `generativelanguage.googleapis.com`), Vertex gives you **enterprise-grade rate limits and GCP billing/credits**, and is the right choice when you want Gemini usage to draw on your Google Cloud account rather than an AI Studio key.
      +
      +:::info Vertex authenticates with OAuth2, not an API key
      +Vertex has **no static API key** for the standard endpoint. Every request needs a short-lived **OAuth2 access token** (≈1 hour TTL) minted from either a service-account JSON or Application Default Credentials (ADC). Hermes mints and **auto-refreshes** these tokens for you — you never paste a token by hand. This is why pasting a temporary token into a custom provider's `api_key` field does not work: it expires mid-session.
      +:::
      +
      +## Prerequisites
      +
      +- **A Google Cloud project** with the **Vertex AI API enabled** and billing active.
      +- **Credentials**, one of:
      +  - a **service-account JSON** key file with the `roles/aiplatform.user` role, or
      +  - **Application Default Credentials** via `gcloud auth application-default login` (or the metadata server when running on a GCP VM).
      +- **`google-auth`** — installed automatically the first time you select Vertex (lazy install), or explicitly with `pip install 'hermes-agent[vertex]'`.
      +
      +## Quick Start
      +
      +```bash
      +# Option A — service account JSON (recommended for servers / gateways)
      +echo "VERTEX_CREDENTIALS_PATH=/path/to/service-account.json" >> ~/.hermes/.env
      +
      +# Option B — Application Default Credentials (good for local dev)
      +gcloud auth application-default login
      +
      +# Select Vertex as your provider
      +hermes model
      +# → Choose "More providers..." → "Google Vertex AI"
      +# → Enter your GCP project ID (or leave blank to use the one in your credentials)
      +# → Choose a region (default: global)
      +# → Select a Gemini model
      +
      +# Start chatting
      +hermes chat
      +```
      +
      +## Configuration
      +
      +Vertex splits its settings by sensitivity:
      +
      +- The **credential path** is a pointer to a secret and lives in `~/.hermes/.env`.
      +- **Project ID and region** are non-secret routing settings and live in `~/.hermes/config.yaml`.
      +
      +`~/.hermes/.env`:
      +
      +```bash
      +# One of these (checked in this order); omit both to use ADC:
      +VERTEX_CREDENTIALS_PATH=/path/to/service-account.json
      +GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json
      +```
      +
      +`~/.hermes/config.yaml`:
      +
      +```yaml
      +model:
      +  default: google/gemini-3-flash-preview
      +  provider: vertex
      +
      +vertex:
      +  project_id: my-gcp-project   # blank → use the project embedded in the credentials
      +  region: global               # "global" is required for the Gemini 3.x previews
      +```
      +
      +:::tip Environment variables win over config.yaml
      +`VERTEX_PROJECT_ID` and `VERTEX_REGION` override the `vertex.project_id` / `vertex.region` values in `config.yaml`. Use them for per-shell overrides; keep the durable settings in `config.yaml`.
      +:::
      +
      +### How authentication works
      +
      +1. Hermes resolves credentials in this order: `VERTEX_CREDENTIALS_PATH` → `GOOGLE_APPLICATION_CREDENTIALS` → ADC.
      +2. It mints an OAuth2 access token (`cloud-platform` scope) and caches it, refreshing when the token is within 5 minutes of expiry.
      +3. The token is handed to a standard OpenAI client pointed at the Vertex endpoint:
      +   ```text
      +   https://aiplatform.googleapis.com/v1beta1/projects/{project}/locations/{region}/endpoints/openapi
      +   ```
      +   Regional locations use a `{region}-aiplatform.googleapis.com` host instead.
      +4. If a session runs longer than the token lifetime and a request returns `401`, Hermes re-mints the token and retries automatically. On a long-running gateway, if ADC's refresh token has itself expired, Hermes falls back to the service-account JSON when one is configured.
      +
      +## Available Models
      +
      +Vertex requires the `google/` vendor prefix on model IDs. The `hermes model` picker offers:
      +
      +| Model | ID |
      +|-------|----|
      +| Gemini 3.1 Pro Preview | `google/gemini-3.1-pro-preview` |
      +| Gemini 3 Pro Preview | `google/gemini-3-pro-preview` |
      +| Gemini 3 Flash Preview | `google/gemini-3-flash-preview` |
      +| Gemini 3.1 Flash Lite Preview | `google/gemini-3.1-flash-lite-preview` |
      +| Gemini 2.5 Pro | `google/gemini-2.5-pro` |
      +| Gemini 2.5 Flash | `google/gemini-2.5-flash` |
      +
      +:::note `global` region for Gemini 3.x
      +The Gemini 3.x preview models are served through the `global` endpoint. Regional endpoints (`us-central1`, etc.) may 404 them. Leave `region: global` unless you have a specific reason to pin a region.
      +:::
      +
      +## Switching Models Mid-Session
      +
      +```text
      +/model google/gemini-3-pro-preview
      +/model google/gemini-3-flash-preview
      +```
      +
      +`/model` switches among already-configured providers and models; it does not collect new credentials. Configure Vertex with `hermes model` first.
      +
      +## Reasoning / Thinking
      +
      +Vertex exposes Gemini's thinking budget through the OpenAI-compatible surface. Hermes maps its reasoning-effort setting onto `extra_body.google.thinking_config` automatically, so `reasoning_effort` works the same way it does on other Gemini surfaces.
      +
      +## Diagnostics
      +
      +```bash
      +hermes doctor
      +```
      +
      +The doctor reports whether Vertex credentials can be resolved (service-account path or ADC) and whether the provider is configured.
      +
      +## Troubleshooting
      +
      +### "Vertex AI credentials could not be resolved"
      +
      +Hermes found neither a service-account JSON nor working ADC. Either set `VERTEX_CREDENTIALS_PATH` in `~/.hermes/.env`, or run `gcloud auth application-default login`. If your project isn't embedded in the credentials, set `vertex.project_id` in `config.yaml`.
      +
      +### `google-auth` not installed
      +
      +Install the extra: `pip install 'hermes-agent[vertex]'`. Hermes also lazy-installs it the first time you select the Vertex provider.
      +
      +### 404 on Gemini 3.x models
      +
      +You are probably on a regional endpoint. Set `region: global` in the `vertex:` section of `config.yaml` (or unset `VERTEX_REGION`).
      +
      +### 403 / permission denied
      +
      +The service account (or your ADC identity) needs the `roles/aiplatform.user` role on the project, and the Vertex AI API must be enabled for that project.
      +
      +## Related
      +
      +- [Google Gemini (AI Studio)](/guides/google-gemini) — static-API-key Gemini without GCP
      +- [AWS Bedrock](/guides/aws-bedrock) — another native cloud-provider integration
      +- [AI Providers](/integrations/providers)
      +- [Configuration](/user-guide/configuration)
    • website/docs/integrations/providers.md
      @@ -40,6 +40,7 @@ You need at least one way to connect to an LLM. Use `hermes model` to switch pro
       | **DeepSeek** | `DEEPSEEK_API_KEY` in `~/.hermes/.env` (provider: `deepseek`) |
       | **Hugging Face** | `HF_TOKEN` in `~/.hermes/.env` (provider: `huggingface`, aliases: `hf`) |
       | **Google / Gemini** | `GOOGLE_API_KEY` (or `GEMINI_API_KEY`) in `~/.hermes/.env` (provider: `gemini`) |
      +| **Google Vertex AI** | `hermes model` → "Google Vertex AI" (provider: `vertex`; OAuth2 via service-account JSON or ADC, GCP billing) |
       | **OpenAI API (direct)** | `OPENAI_API_KEY` in `~/.hermes/.env` (provider: `openai-api`, optional `OPENAI_BASE_URL`) |
       | **Azure AI Foundry** | `hermes model` → "Azure AI Foundry" (provider: `azure-foundry`; uses Azure OpenAI / Foundry endpoint and key) |
       | **AWS Bedrock** | `hermes model` → "AWS Bedrock" (provider: `bedrock`; standard AWS credentials chain via boto3) |
      @@ -372,6 +373,31 @@ Bedrock uses the **Converse API** under the hood — requests are translated to
       
       See the [AWS Bedrock guide](/guides/aws-bedrock) for a walkthrough of IAM setup, region selection, and cross-region inference.
       
      +### Google Vertex AI
      +
      +Gemini models on Google Cloud Vertex AI via Vertex's OpenAI-compatible endpoint. Authentication is **OAuth2** — a short-lived access token (~1 hour) minted from a service-account JSON or Application Default Credentials (ADC). There is **no static API key**; Hermes mints and auto-refreshes the token for you, including re-minting on a mid-session `401`.
      +
      +```bash
      +# Service account JSON (recommended for servers / gateways)
      +echo "VERTEX_CREDENTIALS_PATH=/path/to/service-account.json" >> ~/.hermes/.env
      +# or Application Default Credentials
      +gcloud auth application-default login
      +
      +hermes model   # → "Google Vertex AI" → project → region → model
      +```
      +
      +Or in `config.yaml` (project/region are non-secret and live here; the credential path stays in `.env`):
      +```yaml
      +model:
      +  provider: "vertex"
      +  default: "google/gemini-3-flash-preview"   # Vertex requires the google/ prefix
      +vertex:
      +  project_id: "my-gcp-project"   # blank → use the project embedded in the credentials
      +  region: "global"               # required for the Gemini 3.x previews
      +```
      +
      +`VERTEX_PROJECT_ID` / `VERTEX_REGION` env vars override the `config.yaml` values. Install with `pip install 'hermes-agent[vertex]'` (or let Hermes lazy-install `google-auth` on first use). See the [Google Vertex AI guide](/guides/google-vertex) for the full walkthrough, and the [Google Gemini guide](/guides/google-gemini) for the static-API-key AI Studio path instead.
      +
       ### Qwen Portal (OAuth)
       
       Alibaba's Qwen Portal with browser-based OAuth login. Pick **Qwen OAuth (Portal)** in `hermes model`, sign in through the browser, and Hermes persists the refresh token.
    • website/sidebars.ts
      @@ -701,6 +701,7 @@ const sidebars: SidebarsConfig = {
               'guides/webhook-github-pr-review',
               'guides/migrate-from-openclaw',
               'guides/aws-bedrock',
      +        'guides/google-vertex',
               'guides/azure-foundry',
               'guides/xai-grok-oauth',
               'guides/oauth-over-ssh',

1 PR closed without merging

From the close comments: superseded by another PR 1

  • #36253 feat(vertex): add Google Vertex AI provider for Gemini models · superseded by another PR → #56363, #8427
Closed issues · 2
  • Closed#13484Feature: native Google Cloud Vertex AI provider support
  • Closed#56259[Feature]: simplify Vertex OpenAI-compatible authentication
How this cluster was grouped
  • Link evidence (partly structural links): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
  • Members outside that structural core may belong to a different underlying problem.
  • Pairwise link density 0.34: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. 1.00 means every pair is linked.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
agent
Merged PRs
#56363
Closed issues
2
Attempts
1 PR closed without merging
Fix link checked
1 Jul 2026

For your agent

The same facts, typed.