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