# NousResearch/hermes-agent · Google Vertex AI provider support and authentication

## Open now

- 0 issues, 1 PR open on GitHub.
- PR #55742: feat: add Google Vertex AI provider (AnthropicVertex SDK)

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.

## TL;DR

- 5 member(s): 0 open issue(s), 2 closed issue(s), 1 open PR(s), 1 merged PR(s), 1 PR(s) closed without merging.
- A fix was merged to main in PR #56363. Verify it addresses your case.
- **Recommended:** read PR #56363 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `unresolved`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): 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 member 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.

## Closed / duplicate issues

- #13484: Feature: native Google Cloud Vertex AI provider support
- #56259: \[Feature\]: simplify Vertex OpenAI-compatible authentication

## Prior & competing PRs

- **#56363: feat(vertex): add Google Vertex AI provider for Gemini (OAuth2)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - 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)
  - fix hunks:
  - `agent/auxiliary_client.py`

    ```diff
    @@ -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`

    ```diff
    @@ -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`

    ```diff
    @@ -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`

    ```diff
    @@ -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`

    ```diff
    @@ -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`

    ```diff
    @@ -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,
     }
    ```

    ```diff
    @@ -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`

    ```diff
    @@ -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,
    ```

    ```diff
    @@ -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 {
    ```

    ```diff
    @@ -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`

    ```diff
    @@ -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`

    ```diff
    @@ -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)"),
    ```

    ```diff
    @@ -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)"
    ```

    ```diff
    @@ -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`

    ```diff
    @@ -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`

    ```diff
    @@ -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`

    ```diff
    @@ -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`

    ```diff
    @@ -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`

    ```diff
    @@ -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`

    ```diff
    @@ -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`

    ```diff
    @@ -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.
     
    ```

    ```diff
    @@ -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()
    ```

    ```diff
    @@ -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`

    ```diff
    @@ -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)
         "7698789+abchiaravalle@users.noreply.github.com": "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)
         "swissly@users.noreply.github.com": "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`

    ```diff
    @@ -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`

    ```diff
    @@ -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`

    ```diff
    @@ -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`

    ```diff
    @@ -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`

    ```diff
    @@ -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]]
    ```

    ```diff
    @@ -1709,6 +1709,9 @@ termux-all = [
     tts-premium = [
         { name = "elevenlabs" },
     ]
    +vertex = [
    +    { name = "google-auth" },
    +]
     voice = [
         { name = "faster-whisper" },
         { name = "numpy" },
    ```

    ```diff
    @@ -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'" },
    ```

    ```diff
    @@ -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`

    ```diff
    @@ -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`

    ```diff
    @@ -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) |
    ```

    ```diff
    @@ -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`

    ```diff
    @@ -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',
    ```
- **#36253: feat(vertex): add Google Vertex AI provider for Gemini models**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#56363`, `#8427`
- **#55742: feat: add Google Vertex AI provider (AnthropicVertex SDK)**. open
  - maintainer direction: category `partial_fix`; implication `needs_llm`; refs `#3569`, `#6491`, `#27356`
  - files: `agent/agent_init.py`, `agent/agent_runtime_helpers.py`, `agent/anthropic_adapter.py`, `agent/auxiliary_client.py`, `agent/chat_completion_helpers.py`, `hermes_cli/auth.py`, `hermes_cli/model_setup_flows.py`, `hermes_cli/models.py`, `hermes_cli/runtime_provider.py`, `plugins/model-providers/vertex/__init__.py`, `plugins/model-providers/vertex/plugin.yaml`, `pyproject.toml`, `run_agent.py`, `tests/agent/test_anthropic_adapter.py`, `tests/hermes_cli/test_runtime_provider_resolution.py`, `tests/run_agent/test_run_agent.py`, `tools/lazy_deps.py`, `website/docs/guides/google-vertex.md`, `website/docs/integrations/providers.md`

## Fix status

- Merged to main in PR #56363 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-07-01`.
- Subsystem: `agent`
