# NousResearch/hermes-agent · Gemini 429 error handling and provider cleanup

## Open now

- Nothing open on GitHub.

These items address distinguishing transient Gemini rate-limit errors (per-minute TPM/RPM) from daily quota exhaustion, improving error messaging accordingly. Also included is removal of the google-gemini-cli and google-antigravity OAuth providers.

## TL;DR

- 6 member(s): 0 open issue(s), 2 closed issue(s), 0 open PR(s), 1 merged PR(s), 3 PR(s) closed without merging.
- A fix was merged to main in PR #50492. Verify it addresses your case.
- **Recommended:** read PR #50492 (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.29`: 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

- #15895: \[Bug\]: google-gemini-cli causing 429 but gquota ok
- #38804: \[Bug\]: Temporary Gemini rate limits are surfaced as "quota exhausted" errors

## Prior & competing PRs

- **#50492: feat(providers): remove google-gemini-cli + google-antigravity OAuth providers**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `agent/agent_runtime_helpers.py`, `agent/antigravity_cloudcode_adapter.py`, `agent/antigravity_code_assist.py`, `agent/antigravity_oauth.py`, `agent/gemini_cloudcode_adapter.py`, `agent/google_code_assist.py`, `agent/google_oauth.py`, `agent/transports/chat_completions.py`, `apps/desktop/src/app/settings/constants.ts`, `apps/desktop/src/app/settings/helpers.test.ts`, `apps/desktop/src/lib/desktop-slash-commands.ts`, `cli.py`, `hermes_cli/auth.py`, `hermes_cli/auth_commands.py`, `hermes_cli/cli_commands_mixin.py`, `hermes_cli/commands.py`, `hermes_cli/config.py`, `hermes_cli/doctor.py`, `hermes_cli/main.py`, `hermes_cli/model_setup_flows.py` (+48 more)
  - fix hunks:
  - `agent/agent_runtime_helpers.py`

    ```diff
    @@ -1378,37 +1378,6 @@ def create_openai_client(agent, client_kwargs: dict, *, reason: str, shared: boo
                 agent._client_log_context(),
             )
             return client
    -    if agent.provider == "google-gemini-cli" or str(client_kwargs.get("base_url", "")).startswith("cloudcode-pa://"):
    -        from agent.gemini_cloudcode_adapter import GeminiCloudCodeClient
    -
    -        # Strip OpenAI-specific kwargs the Gemini client doesn't accept
    -        safe_kwargs = {
    -            k: v for k, v in client_kwargs.items()
    -            if k in {"api_key", "base_url", "default_headers", "project_id", "timeout"}
    -        }
    -        client = GeminiCloudCodeClient(**safe_kwargs)
    -        _ra().logger.info(
    -            "Gemini Cloud Code Assist client created (%s, shared=%s) %s",
    -            reason,
    -            shared,
    -            agent._client_log_context(),
    -        )
    -        return client
    -    if agent.provider == "google-antigravity" or str(client_kwargs.get("base_url", "")).startswith("antigravity-pa://"):
    -        from agent.antigravity_cloudcode_adapter import AntigravityCloudCodeClient
    -
    -        safe_kwargs = {
    -            k: v for k, v in client_kwargs.items()
    -            if k in {"api_key", "base_url", "default_headers", "project_id", "timeout"}
    -        }
    -        client = AntigravityCloudCodeClient(**safe_kwargs)
    -        _ra().logger.info(
    -            "Antigravity Code Assist client created (%s, shared=%s) %s",
    -            reason,
    -            shared,
    -            agent._client_log_context(),
    -        )
    -        return client
         if agent.provider == "gemini":
             from agent.gemini_native_adapter import GeminiNativeClient, is_native_gemini_base_url
     
    ```
  - `/dev/null`

    ```diff
    @@ -1,164 +0,0 @@
    -"""OpenAI-compatible facade for Antigravity native OAuth inference."""
    -
    -from __future__ import annotations
    -
    -from typing import Any, Dict, Iterator, List, Optional
    -
    -import httpx
    -
    -from agent import antigravity_oauth
    -from agent.antigravity_code_assist import (
    -    ANTIGRAVITY_CODE_ASSIST_ENDPOINT,
    -    CodeAssistError,
    -    ProjectContext,
    -    build_headers,
    -    resolve_project_context,
    -)
    -from agent.gemini_cloudcode_adapter import (
    -    GeminiCloudCodeClient,
    -    _GeminiStreamChunk,
    -    _gemini_http_error,
    -    _iter_sse_events,
    -    _translate_gemini_response,
    -    _translate_stream_event,
    -    build_gemini_request,
    -    wrap_code_assist_request,
    -)
    -
    -MARKER_BASE_URL = "antigravity-pa://google"
    -
    -
    -class AntigravityCloudCodeClient(GeminiCloudCodeClient):
    -    """Minimal OpenAI-SDK-compatible facade over Antigravity Code Assist."""
    -
    -    def __init__(
    -        self,
    -        *,
    -        api_key: [redacted:secret-assignment] = None,
    -        base_url: Optional[str] = None,
    -        default_headers: Optional[Dict[str, str]] = None,
    -        project_id: str = "",
    -        **kwargs: Any,
    -    ):
    -        super().__init__(
    -            api_key=[redacted:secret-assignment] or "antigravity-oauth",
    -            base_url=base_url or MARKER_BASE_URL,
    -            default_headers=default_headers,
    -            project_id=project_id,
    -            **kwargs,
    -        )
    -
    -    def _ensure_project_context(self, access_token: str, model: str) -> ProjectContext:
    -        if self._project_context is not None:
    -            return self._project_context  # type: ignore[return-value]
    -
    -        env_project = antigravity_oauth.resolve_project_id_from_env()
    -        creds = antigravity_oauth.load_credentials()
    -        stored_project = creds.project_id if creds else ""
    -        if stored_project:
    -            self._project_context = ProjectContext(
    -                project_id=stored_project,
    -                managed_project_id=creds.managed_project_id if creds else "",
    -                source="stored",
    -            )
    -            return self._project_context
    -
    -        ctx = resolve_project_context(
    -            access_token,
    -            configured_project_id=self._configured_project_id,
    -            env_project_id=env_project,
    -        )
    -        if ctx.project_id or ctx.managed_project_id:
    -            antigravity_oauth.update_project_ids(
    -                project_id=ctx.project_id,
    -                managed_project_id=ctx.managed_project_id,
    -            )
    -        self._project_context = ctx
    -        return ctx
    -
    -    def _create_chat_completion(
    -        self,
    -        *,
    -        model: str = "gemini-3-flash-agent",
    -        messages: Optional[List[Dict[str, Any]]] = None,
    -        stream: bool = False,
    -        tools: Any = None,
    -        tool_choice: Any = None,
    -        temperature: Optional[float] = None,
    -        max_tokens: [redacted:secret-assignment] = None,
    -        top_p: Optional[float] = None,
    -        stop: Any = None,
    -        extra_body: Optional[Dict[str, Any]] = None,
    -        timeout: Any = None,
    -        **_: Any,
    -    ) -> Any:
    -        access_token = [redacted:secret-assignment]
    -        ctx = self._ensure_project_context(access_token, model)
    -
    -        thinking_config = None
    -        if isinstance(extra_body, dict):
    -            thinking_config = extra_body.get("thinking_config") or extra_body.get("thinkingConfig")
    -
    -        inner = build_gemini_request(
    -            messages=messages or [],
    -            tools=tools,
    -            tool_choice=tool_choice,
    -            temperature=temperature,
    -            max_tokens=[redacted:secret-assignment]
    -            top_p=top_p,
    -            stop=stop,
    -            thinking_config=thinking_config,
    -        )
    -        wrapped = wrap_code_assist_request(
    -            project_id=ctx.project_id,
    -            model=model,
    -            inner_request=inner,
    -        )
    -
    -        headers = build_headers(access_token)
    -        headers.update(self._default_headers)
    -
    -        if stream:
    -            return self._stream_completion(model=model, wrapped=wrapped, headers=headers)
    -
    -        url = f"{ANTIGRAVITY_CODE_ASSIST_ENDPOINT}/v1internal:generateContent"
    -        response = self._http.post(url, json=wrapped, headers=headers)
    -        if response.status_code != 200:
    -            raise _gemini_http_error(response)
    -        try:
    -            payload = response.json()
    -        except ValueError as exc:
    -            raise CodeAssistError(
    -                f"Invalid JSON from Antigravity Code Assist: {exc}",
    -                code="antigravity_code_assist_invalid_json",
    -            ) from exc
    -        return _translate_gemini_response(payload, model=model)
    -
    -    def _stream_completion(
    -        self,
    -        *,
    -        model: str,
    -        wrapped: Dict[str, Any],
    -        headers: Dict[str, str],
    -    ) -> Iterator[_GeminiStreamChunk]:
    -        url = f"{ANTIGRAVITY_CODE_ASSIST_ENDPOINT}/v1internal:streamGenerateContent?alt=sse"
    -        stream_headers = dict(headers)
    -        stream_headers["Accept"] = "text/event-stream"
    -
    -        def _generator() -> Iterator[_GeminiStreamChunk]:
    -            try:
    -                with self._http.stream("POST", url, json=wrapped, headers=stream_headers) as response:
    -                    if response.status_code != 200:
    -                        response.read()
    -                        raise _gemini_http_error(response)
    -                    tool_call_counter: List[int] = [0]
    -                    for event in _iter_sse_events(response):
    -                        for chunk in _translate_stream_event(event, model, tool_call_counter):
    -                            yield chunk
    -            except httpx.HTTPError as exc:
    -                raise CodeAssistError(
    -                    f"Antigravity streaming request failed: {exc}",
    -                    code="antigravity_code_assist_stream_error",
    -                ) from exc
    -
    -        return _generator()
    ```
  - `/dev/null`

    ```diff
    @@ -1,286 +0,0 @@
    -"""Antigravity Code Assist control-plane helpers.
    -
    -The new Antigravity CLI uses the same v1internal Code Assist family as
    -gemini-cli, but with Antigravity OAuth scopes, metadata and model catalog. This
    -module keeps that provider-specific surface separate from
    -``agent.google_code_assist``.
    -"""
    -
    -from __future__ import annotations
    -
    -import json
    -import logging
    -import urllib.error
    -import urllib.request
    -import uuid
    -from dataclasses import dataclass, field
    -from typing import Any, Dict, Iterable, List, Optional
    -
    -from agent.google_code_assist import CodeAssistError
    -
    -logger = logging.getLogger(__name__)
    -
    -ANTIGRAVITY_CODE_ASSIST_ENDPOINT = "https://daily-cloudcode-pa.sandbox.googleapis.com"
    -ANTIGRAVITY_MODEL_ENDPOINTS = [
    -    ANTIGRAVITY_CODE_ASSIST_ENDPOINT,
    -    "https://cloudcode-pa.googleapis.com",
    -    "https://autopush-cloudcode-pa.sandbox.googleapis.com",
    -]
    -
    -ANTIGRAVITY_CLIENT_METADATA = {
    -    "ideType": "ANTIGRAVITY",
    -    "platform": "PLATFORM_UNSPECIFIED",
    -    "pluginType": "GEMINI",
    -}
    -ANTIGRAVITY_USER_AGENT = "antigravity/1.0.0 windows/amd64"
    -ANTIGRAVITY_X_GOOG_API_CLIENT = "google-cloud-sdk vscode_cloudshelleditor/0.1"
    -
    -DEFAULT_AGENT_MODEL_IDS = [
    -    "gemini-3-flash-agent",
    -    "gemini-3.5-flash-low",
    -    "gemini-pro-agent",
    -    "gemini-3.1-pro-low",
    -    "claude-sonnet-4-6",
    -    "claude-opus-4-6-thinking",
    -    "gpt-oss-120b-medium",
    -]
    -
    -DEPRECATED_MODEL_REPLACEMENTS = {
    -    "gemini-3.1-pro-high": "gemini-pro-agent",
    -}
    -
    -
    -@dataclass
    -class AntigravityProjectInfo:
    -    project_id: str = ""
    -    raw: Dict[str, Any] = field(default_factory=dict)
    -
    -
    -@dataclass
    -class ProjectContext:
    -    project_id: str = ""
    -    managed_project_id: str = ""
    -    tier_id: str = ""
    -    source: str = ""
    -
    -
    -def _client_metadata() -> Dict[str, str]:
    -    return dict(ANTIGRAVITY_CLIENT_METADATA)
    -
    -
    -def build_headers(access_token: str, *, accept: str = "application/json") -> Dict[str, str]:
    -    return {
    -        "Content-Type": "application/json",
    -        "Accept": accept,
    -        "Authorization": f"Bearer {access_token}",
    -        "User-Agent": ANTIGRAVITY_USER_AGENT,
    -        "X-Goog-Api-Client": ANTIGRAVITY_X_GOOG_API_CLIENT,
    -        "Client-Metadata": json.dumps(_client_metadata(), separators=(",", ":")),
    -        "x-activity-request-id": str(uuid.uuid4()),
    -    }
    -
    -
    -def _post_json(
    -    url: str,
    -    body: Dict[str, Any],
    -    access_token: str,
    -    *,
    -    timeout: float = 30.0,
    -) -> Dict[str, Any]:
    -    data = json.dumps(body).encode("utf-8")
    -    request = urllib.request.Request(
    -        url,
    -        data=data,
    -        method="POST",
    -        headers=build_headers(access_token),
    -    )
    -    try:
    -        with urllib.request.urlopen(request, timeout=timeout) as response:
    -            raw = response.read().decode("utf-8", errors="replace")
    -            return json.loads(raw) if raw else {}
    -    except urllib.error.HTTPError as exc:
    -        detail = ""
    -        try:
    -            detail = exc.read().decode("utf-8", errors="replace")
    -        except Exception:
    -            pass
    -        raise CodeAssistError(
    -            f"Antigravity Code Assist HTTP {exc.code}: {detail or exc.reason}",
    -            code=f"antigravity_code_assist_http_{exc.code}",
    -        ) from exc
    -    except urllib.error.URLError as exc:
    -        raise CodeAssistError(
    -            f"Antigravity Code Assist request failed: {exc}",
    -            code="antigravity_code_assist_network_error",
    -        ) from exc
    -
    -
    -def load_code_assist(
    -    access_token: str,
    -    *,
    -    project_id: str = "",
    -    endpoint: str = ANTIGRAVITY_CODE_ASSIST_ENDPOINT,
    -) -> AntigravityProjectInfo:
    -    metadata = _client_metadata()
    -    if project_id:
    -        metadata["duetProject"] = project_id
    -    body: Dict[str, Any] = {"metadata": metadata}
    -    if project_id:
    -        body["cloudaicompanionProject"] = project_id
    -    resp = _post_json(f"{endpoint}/v1internal:loadCodeAssist", body, access_token)
    -    project = (
    -        str(resp.get("cloudaicompanionProject") or "").strip()
    -        or str(resp.get("project") or "").strip()
    -    )
    -    return AntigravityProjectInfo(project_id=project, raw=resp)
    -
    -
    -def resolve_project_context(
    -    access_token: str,
    -    *,
    -    configured_project_id: str = "",
    -    env_project_id: str = "",
    -) -> ProjectContext:
    -    if configured_project_id:
    -        return ProjectContext(project_id=configured_project_id, source="config")
    -    if env_project_id:
    -        return ProjectContext(project_id=env_project_id, source="env")
    -    info = load_code_assist(access_token)
    -    if info.project_id:
    -        return ProjectContext(
    -            project_id=info.project_id,
    -            managed_project_id=info.project_id,
    -            source="discovered",
    -        )
    -    # Discovery returned no project (common on fresh consumer accounts that
    -    # haven't been onboarded). Fall back to the public default project so the
    -    # call chain still succeeds — mirrors the Antigravity CLI reference flow.
    -    from agent.antigravity_oauth import DEFAULT_PROJECT_ID
    -    return ProjectContext(
    -        project_id=DEFAULT_PROJECT_ID,
    -        managed_project_id=DEFAULT_PROJECT_ID,
    -        source="default",
    -    )
    -
    -
    -def fetch_available_models(
    -    access_token: str,
    -    *,
    -    project_id: str = "",
    -    endpoint: str = ANTIGRAVITY_CODE_ASSIST_ENDPOINT,
    -) -> Dict[str, Any]:
    -    body: Dict[str, Any] = {}
    -    if project_id:
    -        body["project"] = project_id
    -    return _post_json(f"{endpoint}/v1internal:fetchAvailableModels", body, access_token)
    -
    -
    -def fetch_available_models_with_fallbacks(
    -    access_token: str,
    -    *,
    -    project_id: str = "",
    -    endpoints: Optional[Iterable[str]] = None,
    -) -> Dict[str, Any]:
    -    last_err: Optional[Exception] = None
    -    for endpoint in endpoints or ANTIGRAVITY_MODEL_ENDPOINTS:
    -        try:
    -            return fetch_available_models(
    -                access_token,
    -                project_id=project_id,
    -                endpoint=endpoint,
    -            )
    -        except Exception as exc:
    -            last_err = exc
    -            logger.debug("Antigravity fetchAvailableModels failed on %s: %s", endpoint, exc)
    -    if last_err:
    -        raise last_err
    -    return {}
    -
    -
    -def _model_id_from_value(value: Any) -> str:
    -    if isinstance(value, str):
    -        return value.strip()
    -    if isinstance(value, dict):
    -        for key in ("modelId", "model_id", "id", "name"):
    -            candidate = str(value.get(key) or "").strip()
    -            if candidate:
    -                return candidate
    -    return ""
    -
    -
    -def _ids_from_sort(sort: Dict[str, Any]) -> List[str]:
    -    ids: List[str] = []
    -    for key in ("modelIds", "model_ids", "models", "modelSorts"):
    -        value = sort.get(key)
    -        if isinstance(value, list):
    -            for item in value:
    -                mid = _model_id_from_value(item)
    -                if mid:
    -                    ids.append(mid)
    -        elif isinstance(value, dict):
    -            mid = _model_id_from_value(value)
    -            if mid:
    -                ids.append(mid)
    -    return ids
    -
    -
    -def _is_recommended_sort(sort: Dict[str, Any]) -> bool:
    -    label = " ".join(
    -        str(sort.get(key) or "")
    -        for key in ("name", "displayName", "title", "category", "group")
    -    ).lower()
    -    return "recommended" in label
    -
    -
    -def _raw_model_ids(payload: Dict[str, Any]) -> List[str]:
    -    ids: List[str] = []
    -    models = payload.get("models")
    -    if isinstance(models, list):
    -        for item in models:
    -            mid = _model_id_from_value(item)
    -            if mid:
    -                ids.append(mid)
    -    return ids
    -
    -
    -def filter_agent_model_ids(ids: Iterable[str]) -> List[str]:
    -    seen: set[str] = set()
    -    filtered: List[str] = []
    -    raw = [str(mid).strip() for mid in ids if str(mid).strip()]
    -    replacements = set(DEPRECATED_MODEL_REPLACEMENTS.values())
    -    for mid in raw:
    -        if mid in seen:
    -            continue
    -        if mid.startswith(("chat_", "tab_")):
    -            continue
    -        if mid in DEPRECATED_MODEL_REPLACEMENTS and DEPRECATED_MODEL_REPLACEMENTS[mid] in raw:
    -            continue
    -        if mid in replacements and mid in seen:
    -            continue
    -        seen.add(mid)
    -        filtered.append(mid)
    -    return filtered
    -
    -
    -def parse_agent_model_ids(payload: Dict[str, Any]) -> List[str]:
    -    """Return the user-facing Antigravity agent model list in display order."""
    -    sorts = payload.get("agentModelSorts")
    -    ordered: List[str] = []
    -    if isinstance(sorts, list):
    -        recommended = [s for s in sorts if isinstance(s, dict) and _is_recommended_sort(s)]
    -        rest = [s for s in sorts if isinstance(s, dict) and not _is_recommended_sort(s)]
    -        for sort in recommended + rest:
    -            ordered.extend(_ids_from_sort(sort))
    -
    -    if not ordered:
    -        default_id = str(payload.get("defaultAgentModelId") or "").strip()
    -        if default_id:
    -            ordered.append(default_id)
    -        for mid in DEFAULT_AGENT_MODEL_IDS:
    -            ordered.append(mid)
    -        ordered.extend(_raw_model_ids(payload))
    -
    -    filtered = filter_agent_model_ids(ordered)
    -    if filtered:
    -        return filtered
    -    return list(DEFAULT_AGENT_MODEL_IDS)
    ```
  - `/dev/null`

    ```diff
    @@ -1,907 +0,0 @@
    -"""Google OAuth PKCE flow for the Antigravity (google-antigravity) provider.
    -
    -Tokens are stored separately from the existing ``google-gemini-cli`` provider so
    -development and production credentials do not accidentally bleed across:
    -
    -    ~/.hermes/auth/antigravity_oauth.json
    -
    -The on-disk schema matches ``agent.google_oauth`` so the runtime resolver can
    -share the same refresh/project-id packing convention.
    -"""
    -
    -from __future__ import annotations
    -
    -import base64
    -import contextlib
    -import hashlib
    -import http.server
    -import json
    -import logging
    -import os
    -import re
    -import secrets
    -import shutil
    -import stat
    -import threading
    -import time
    -import urllib.error
    -import urllib.parse
    -import urllib.request
    -import webbrowser
    -from dataclasses import dataclass
    -from pathlib import Path
    -from typing import Any, Dict, Optional, Tuple
    -
    -from hermes_constants import get_hermes_home
    -from utils import atomic_replace
    -
    -logger = logging.getLogger(__name__)
    -
    -ENV_CLIENT_ID = "HERMES_ANTIGRAVITY_CLIENT_ID"
    -ENV_CLIENT_SECRET = "[redacted:secret-assignment]"
    -ENV_CLI_PATH = "HERMES_ANTIGRAVITY_CLI_PATH"
    -
    -# Public Antigravity CLI desktop OAuth client. Like Google's gemini-cli
    -# credentials (see agent/google_oauth.py), this is a DESKTOP OAuth client and
    -# its "secret" is not confidential — installed-app clients have no
    -# secret-keeping requirement (PKCE provides the security), and these creds are
    -# baked into every copy of the Antigravity CLI. Shipping them as a fallback
    -# lets users without `agy` installed authenticate directly. Split into parts
    -# with explicit comments per the convention in google_oauth.py.
    -_PUBLIC_CLIENT_ID_PROJECT_NUM = "1071006060591"
    -_PUBLIC_CLIENT_ID_HASH = "tmhssin2h21lcre235vtolojh4g403ep"
    -_PUBLIC_CLIENT_SECRET_SUFFIX = "[redacted:secret-assignment]"
    -
    -_DEFAULT_CLIENT_ID = (
    -    f"{_PUBLIC_CLIENT_ID_PROJECT_NUM}-{_PUBLIC_CLIENT_ID_HASH}"
    -    ".apps.googleusercontent.com"
    -)
    -_DEFAULT_CLIENT_SECRET = f"GOCSPX-{_PUBLIC_CLIENT_SECRET_SUFFIX}"
    -
    -# Fallback project ID when Code Assist project discovery fails entirely.
    -DEFAULT_PROJECT_ID = "rising-fact-p41fc"
    -
    -_CLIENT_ID_PATTERN = re.compile(
    -    r"([0-9]{8,}-[a-z0-9]{20,}\.apps\.googleusercontent\.com)"
    -)
    -_CLIENT_SECRET_PATTERN = [redacted:secret-assignment]"(GOCSPX-[A-Za-z0-9_-]{20,80})")
    -_DISCOVERY_MAX_FILE_BYTES = 25 * 1024 * 1024
    -_DISCOVERY_MAX_AGY_BINARY_BYTES = 220 * 1024 * 1024
    -_DISCOVERY_MAX_FILES = 600
    -_DISCOVERY_EXTENSIONS = {
    -    "",
    -    ".cjs",
    -    ".exe",
    -    ".js",
    -    ".json",
    -    ".mjs",
    -    ".node",
    -    ".ts",
    -}
    -_DISCOVERY_SKIP_DIRS = {
    -    ".system_generated",
    -    "brain",
    -    "conversations",
    -    "log",
    -    "logs",
    -    "scratch",
    -}
    -
    -AUTH_ENDPOINT = "https://accounts.google.com/o/oauth2/v2/auth"
    -TOKEN_ENDPOINT = "[redacted:secret-assignment]"
    -USERINFO_ENDPOINT = "https://www.googleapis.com/oauth2/v1/userinfo"
    -
    -OAUTH_SCOPES = (
    -    "https://www.googleapis.com/auth/cloud-platform "
    -    "https://www.googleapis.com/auth/userinfo.email "
    -    "https://www.googleapis.com/auth/userinfo.profile "
    -    "https://www.googleapis.com/auth/cclog "
    -    "https://www.googleapis.com/auth/experimentsandconfigs"
    -)
    -
    -DEFAULT_REDIRECT_PORT = 51121
    -REDIRECT_HOST = "localhost"
    -CALLBACK_PATH = "/oauth-callback"
    -REFRESH_SKEW_SECONDS = 60
    -TOKEN_REQUEST_TIMEOUT_SECONDS = 20.0
    -CALLBACK_WAIT_SECONDS = 300
    -LOCK_TIMEOUT_SECONDS = 30.0
    -
    -
    -class AntigravityOAuthError(RuntimeError):
    -    def __init__(self, message: str, *, code: str = "antigravity_oauth_error") -> None:
    -        super().__init__(message)
    -        self.code = code
    -
    -
    -def _credentials_path() -> Path:
    -    return get_hermes_home() / "auth" / "antigravity_oauth.json"
    -
    -
    -def _lock_path() -> Path:
    -    return _credentials_path().with_suffix(".json.lock")
    -
    -
    -_lock_state = threading.local()
    -
    -
    [redacted:email]
    -def _credentials_lock(timeout_seconds: float = LOCK_TIMEOUT_SECONDS):
    -    depth = getattr(_lock_state, "depth", 0)
    -    if depth > 0:
    -        _lock_state.depth = depth + 1
    -        try:
    -            yield
    -        finally:
    -            _lock_state.depth -= 1
    -        return
    -
    -    lock_file_path = _lock_path()
    -    lock_file_path.parent.mkdir(parents=True, exist_ok=True)
    -    fd = os.open(str(lock_file_path), os.O_CREAT | os.O_RDWR, 0o600)
    -    acquired = False
    -    try:
    -        try:
    -            import fcntl
    -        except ImportError:
    -            fcntl = None
    -
    -        if fcntl is not None:
    -            deadline = time.monotonic() + max(0.0, float(timeout_seconds))
    -            while True:
    -                try:
    -                    fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
    -                    acquired = True
    -                    break
    -                except BlockingIOError:
    -                    if time.monotonic() >= deadline:
    -                        raise TimeoutError(
    -                            f"Timed out acquiring Antigravity OAuth credentials lock at {lock_file_path}."
    -                        )
    -                    time.sleep(0.05)
    -        else:
    -            try:
    -                import msvcrt  # type: ignore[import-not-found]
    -
    -                deadline = time.monotonic() + max(0.0, float(timeout_seconds))
    -                while True:
    -                    try:
    -                        msvcrt.locking(fd, msvcrt.LK_NBLCK, 1)
    -                        acquired = True
    -                        break
    -                    except OSError:
    -                        if time.monotonic() >= deadline:
    -                            raise TimeoutError(
    -                                f"Timed out acquiring Antigravity OAuth credentials lock at {lock_file_path}."
    -                            )
    -                        time.sleep(0.05)
    -            except ImportError:
    -                acquired = True
    -
    -        _lock_state.depth = 1
    -        yield
    -    finally:
    -        try:
    -            if acquired:
    -                try:
    -                    import fcntl
    -
    -                    fcntl.flock(fd, fcntl.LOCK_UN)
    -                except ImportError:
    -                    try:
    -                        import msvcrt  # type: ignore[import-not-found]
    -
    -                        try:
    -                            msvcrt.locking(fd, msvcrt.LK_UNLCK, 1)
    -                        except OSError:
    -                            pass
    -                    except ImportError:
    -                        pass
    -        finally:
    -            os.close(fd)
    -            _lock_state.depth = 0
    -
    -
    -_discovered_creds_cache: Dict[str, Any] = {}
    -
    -
    -def _secret_candidates(raw: str) -> list[str]:
    -    candidates: list[str] = []
    -    for length in (35, 34, 36, 33, 37, 38, 39, 40, 41, 42):
    -        if len(raw) >= length:
    -            candidates.append(raw[:length])
    -    candidates.append(raw)
    -    return list(dict.fromkeys(candidates))
    -
    -
    -def _candidate_discovery_roots() -> list[Path]:
    -    roots: list[Path] = []
    -
    -    explicit = (os.getenv(ENV_CLI_PATH) or "").strip()
    -    if explicit:
    -        roots.append(Path(explicit))
    -
    -    for command in ("agy", "agy.exe", "antigravity", "antigravity.exe"):
    -        found = shutil.which(command)
    -        if found:
    -            roots.append(Path(found))
    -
    -    for env_key in ("LOCALAPPDATA", "APPDATA", "ProgramFiles", "ProgramFiles(x86)"):
    -        base = os.getenv(env_key)
    -        if not base:
    -            continue
    -        base_path = Path(base)
    -        roots.extend((
    -            base_path / "agy",
    -            base_path / "agy" / "bin" / "agy.exe",
    -            base_path / "Programs" / "Antigravity",
    -            base_path / "Programs" / "Antigravity CLI",
    -            base_path / "Google" / "Antigravity",
    -            base_path / "Google" / "Antigravity CLI",
    -        ))
    -
    -    home = Path.home()
    -    for root in (
    -        home / ".gemini" / "antigravity-cli",
    -        home / ".antigravitycli",
    -        home / ".antigravity",
    -    ):
    -        roots.append(root)
    -
    -    unique: list[Path] = []
    -    seen: set[str] = set()
    -    for root in roots:
    -        try:
    -            key = str(root.expanduser().resolve())
    -        except OSError:
    -            key = str(root.expanduser())
    -        if key not in seen:
    -            seen.add(key)
    -            unique.append(root)
    -    return unique
    -
    -
    -def _iter_discovery_files() -> list[Path]:
    -    files: list[Path] = []
    -    seen: set[str] = set()
    -
    -    def add(path: Path) -> None:
    -        if len(files) >= _DISCOVERY_MAX_FILES:
    -            return
    -        if path.suffix.lower() not in _DISCOVERY_EXTENSIONS:
    -            return
    -        try:
    -            stat_info = path.stat()
    -            max_bytes = (
    -                _DISCOVERY_MAX_AGY_BINARY_BYTES
    -                if path.name.lower() in {"agy", "agy.exe", "antigravity", "antigravity.exe"}
    -                else _DISCOVERY_MAX_FILE_BYTES
    -            )
    -            if not path.is_file() or stat_info.st_size > max_bytes:
    -                return
    -            key = str(path.resolve())
    -        except OSError:
    -            return
    -        if key in seen:
    -            return
    -        seen.add(key)
    -        files.append(path)
    -
    -    for root in _candidate_discovery_roots():
    -        if len(files) >= _DISCOVERY_MAX_FILES:
    -            break
    -        try:
    -            if root.is_file():
    -                add(root)
    -                continue
    -            if not root.is_dir():
    -                continue
    -        except OSError:
    -            continue
    -
    -        for dirpath, dirnames, filenames in os.walk(root):
    -            dirnames[:] = [
    -                d for d in dirnames
    -                if d not in _DISCOVERY_SKIP_DIRS and not d.startswith(".git")
    -            ]
    -            for filename in filenames:
    -                add(Path(dirpath) / filename)
    -                if len(files) >= _DISCOVERY_MAX_FILES:
    -                    break
    -            if len(files) >= _DISCOVERY_MAX_FILES:
    -                break
    -    return files
    -
    -
    -def _extract_client_credential_candidates_from_text(content: str) -> list[Tuple[str, str]]:
    -    client_ids = list(dict.fromkeys(match.group(1) for match in _CLIENT_ID_PATTERN.finditer(content)))
    -    secrets: [redacted:secret-assignment] = []
    -    for match in _CLIENT_SECRET_PATTERN.finditer(content):
    -        secrets.extend(_secret_candidates(match.group(1)))
    -    secrets = [redacted:secret-assignment]
    -    return [(client_id, secret) for client_id in client_ids for secret in secrets]
    -
    -
    -def _discover_client_credentials() -> Tuple[str, str]:
    -    if _discovered_creds_cache.get("resolved"):
    -        return (
    -            _discovered_creds_cache.get("client_id", ""),
    -            _discovered_creds_cache.get("client_secret", ""),
    -        )
    -
    -    for path in _iter_discovery_files():
    -        try:
    -            content = path.read_bytes().decode("utf-8", errors="ignore")
    -        except OSError:
    -            continue
    -        candidates = _extract_client_credential_candidates_from_text(content)
    -        if candidates:
    -            client_id, client_secret = [redacted:secret-assignment]
    -            _discovered_creds_cache.update({
    -                "client_id": client_id,
    -                "client_secret": client_secret,
    -                "candidates": candidates,
    -                "resolved": "1",
    -            })
    -            logger.info("Discovered Antigravity OAuth client credentials from %s", path)
    -            return client_id, client_secret
    -
    -    _discovered_creds_cache["resolved"] = "1"
    -    return "", ""
    -
    -
    -def _get_client_id() -> str:
    -    env_val = (os.getenv(ENV_CLIENT_ID) or "").strip()
    -    if env_val:
    -        return env_val
    -    discovered, _ = _discover_client_credentials()
    -    if discovered:
    -        return discovered
    -    return _DEFAULT_CLIENT_ID
    -
    -
    -def _get_client_secret() -> str:
    -    env_val = (os.getenv(ENV_CLIENT_SECRET) or "").strip()
    -    if env_val:
    -        return env_val
    -    _, discovered = _discover_client_credentials()
    -    if discovered:
    -        return discovered
    -    return _DEFAULT_CLIENT_SECRET
    -
    -
    -def _iter_client_credential_candidates() -> list[Tuple[str, str]]:
    -    env_id = (os.getenv(ENV_CLIENT_ID) or "").strip()
    -    env_secret = [redacted:secret-assignment] or "").strip()
    -    if env_id and env_secret:
    -        return [(env_id, env_secret)]
    -
    -    _discover_client_credentials()
    -    cached = _discovered_creds_cache.get("candidates")
    -    candidates: list[Tuple[str, str]] = []
    -    if isinstance(cached, list):
    -        candidates = [
    -            (str(client_id), str(client_secret))
    -            for client_id, client_secret in cached
    -            if client_id and client_secret
    -        ]
    -    else:
    -        client_id = str(_discovered_creds_cache.get("client_id") or "")
    -        client_secret = [redacted:secret-assignment]"client_secret") or "")
    -        if client_id and client_secret:
    -            candidates = [(client_id, client_secret)]
    -
    -    # Always include the public baked-in default as a last-resort candidate so
    -    # users without `agy` installed can still authenticate. De-dupe in case
    -    # discovery already surfaced the same client.
    -    default_pair = (_DEFAULT_CLIENT_ID, _DEFAULT_CLIENT_SECRET)
    -    if default_pair not in candidates:
    -        candidates.append(default_pair)
    -    return candidates
    -
    -
    -def _require_client_id() -> str:
    -    client_id = _get_client_id()
    -    if not client_id:
    -        raise AntigravityOAuthError(
    -            "Antigravity OAuth client ID is not available. Install Antigravity CLI "
    -            "so Hermes can discover its desktop OAuth client, set "
    -            f"{ENV_CLI_PATH} to the agy executable, or set {ENV_CLIENT_ID} and "
    -            f"{ENV_CLIENT_SECRET} in ~/.hermes/.env.",
    -            code="antigravity_oauth_client_id_missing",
    -        )
    -    return client_id
    -
    -
    -def _require_client_secret() -> str:
    -    client_secret = [redacted:secret-assignment]
    -    if not client_secret:
    -        raise AntigravityOAuthError(
    -            "Antigravity OAuth client secret is not available. Install Antigravity CLI "
    -            "so Hermes can discover its desktop OAuth client, set "
    -            f"{ENV_CLI_PATH} to the agy executable, or set {ENV_CLIENT_ID} and "
    -            f"{ENV_CLIENT_SECRET} in ~/.hermes/.env.",
    -            code="antigravity_oauth_client_secret_missing",
    -        )
    -    return client_secret
    -
    -
    -def _require_client_credentials() -> Tuple[str, str]:
    -    candidates = _iter_client_credential_candidates()
    -    if not candidates:
    -        _require_client_id()
    -        _require_client_secret()
    -    return candidates[0]
    -
    -
    -def _generate_pkce_pair() -> Tuple[str, str]:
    -    verifier = secrets.token_urlsafe(64)
    -    digest = hashlib.sha256(verifier.encode("ascii")).digest()
    -    challenge = base64.urlsafe_b64encode(digest).rstrip(b"=").decode("ascii")
    -    return verifier, challenge
    -
    -
    -@dataclass
    -class RefreshParts:
    -    refresh_token: str
    -    project_id: str = ""
    -    managed_project_id: str = ""
    -
    -    @classmethod
    -    def parse(cls, packed: str) -> "RefreshParts":
    -        if not packed:
    -            return cls(refresh_token="")
    -        parts = packed.split("|", 2)
    -        return cls(
    -            refresh_token=[redacted:secret-assignment]
    -            project_id=parts[1] if len(parts) > 1 else "",
    -            managed_project_id=parts[2] if len(parts) > 2 else "",
    -        )
    -
    -    def format(self) -> str:
    -        if not self.refresh_token:
    -            return ""
    -        if not self.project_id and not self.managed_project_id:
    -            return self.refresh_token
    -        return f"{self.refresh_token}|{self.project_id}|{self.managed_project_id}"
    -
    -
    -@dataclass
    -class AntigravityCredentials:
    -    access_token: str
    -    refresh_token: str
    -    expires_ms: int
    -    email: str = ""
    -    project_id: str = ""
    -    managed_project_id: str = ""
    -
    -    def to_dict(self) -> Dict[str, Any]:
    -        return {
    -            "refresh": RefreshParts(
    -                refresh_token=[redacted:secret-assignment]
    -                project_id=self.project_id,
    -                managed_project_id=self.managed_project_id,
    -            ).format(),
    -            "access": self.access_token,
    -            "expires": int(self.expires_ms),
    -            "email": self.email,
    -        }
    -
    -    @classmethod
    -    def from_dict(cls, data: Dict[str, Any]) -> "AntigravityCredentials":
    -        parts = RefreshParts.parse(str(data.get("refresh", "") or ""))
    -        return cls(
    -            access_token=[redacted:secret-assignment]"access", "") or ""),
    -            refresh_token=[redacted:secret-assignment]
    -            expires_ms=int(data.get("expires", 0) or 0),
    -            email=str(data.get("email", "") or ""),
    -            project_id=parts.project_id,
    -            managed_project_id=parts.managed_project_id,
    -        )
    -
    -    def access_token_expired(self, skew_seconds: int = REFRESH_SKEW_SECONDS) -> bool:
    -        if not self.access_token or not self.expires_ms:
    -            return True
    -        return (time.time() + max(0, skew_seconds)) * 1000 >= self.expires_ms
    -
    -
    -def load_credentials() -> Optional[AntigravityCredentials]:
    -    path = _credentials_path()
    -    if not path.exists():
    -        return None
    -    try:
    -        with _credentials_lock():
    -            raw = path.read_text(encoding="utf-8")
    -        data = json.loads(raw)
    -    except (json.JSONDecodeError, OSError, IOError) as exc:
    -        logger.warning("Failed to read Antigravity OAuth credentials at %s: %s", path, exc)
    -        return None
    -    if not isinstance(data, dict):
    -        return None
    -    creds = AntigravityCredentials.from_dict(data)
    -    if not creds.access_token:
    -        return None
    -    return creds
    -
    -
    -def save_credentials(creds: AntigravityCredentials) -> Path:
    -    path = _credentials_path()
    -    path.parent.mkdir(parents=True, exist_ok=True)
    -    try:
    -        os.chmod(path.parent, 0o700)
    -    except OSError:
    -        pass
    -    payload = json.dumps(creds.to_dict(), indent=2, sort_keys=True) + "\n"
    -    with _credentials_lock():
    -        tmp_path = path.with_suffix(f".tmp.{os.getpid()}.{secrets.token_hex(4)}")
    -        try:
    -            fd = os.open(
    -                str(tmp_path),
    -                os.O_WRONLY | os.O_CREAT | os.O_EXCL,
    -                stat.S_IRUSR | stat.S_IWUSR,
    -            )
    -            with os.fdopen(fd, "w", encoding="utf-8") as fh:
    -                fh.write(payload)
    -                fh.flush()
    -                os.fsync(fh.fileno())
    -            atomic_replace(tmp_path, path)
    -        finally:
    -            try:
    -                if tmp_path.exists():
    -                    tmp_path.unlink()
    -            except OSError:
    -                pass
    -    return path
    -
    -
    -def clear_credentials() -> None:
    -    path = _credentials_path()
    -    with _credentials_lock():
    -        try:
    -            path.unlink()
    -        except FileNotFoundError:
    -            pass
    -        except OSError as exc:
    -            logger.warning("Failed to remove Antigravity OAuth credentials at %s: %s", path, exc)
    -
    -
    -def _post_form(url: str, data: Dict[str, str], timeout: float) -> Dict[str, Any]:
    -    body = urllib.parse.urlencode(data).encode("ascii")
    -    request = urllib.request.Request(
    -        url,
    -        data=body,
    -        method="POST",
    -        headers={
    -            "Content-Type": "application/x-www-form-urlencoded",
    -            "Accept": "application/json",
    -        },
    -    )
    -    try:
    -        with urllib.request.urlopen(request, timeout=timeout) as response:
    -            raw = response.read().decode("utf-8", errors="replace")
    -            return json.loads(raw)
    -    except urllib.error.HTTPError as exc:
    -        detail = ""
    -        try:
    -            detail = exc.read().decode("utf-8", errors="replace")
    -        except Exception:
    -            pass
    -        code = "antigravity_oauth_token_http_error"
    -        if "invalid_grant" in detail.lower():
    -            code = "antigravity_oauth_invalid_grant"
    -        elif "invalid_client" in detail.lower():
    -            code = "antigravity_oauth_invalid_client"
    -        raise AntigravityOAuthError(
    -            f"Antigravity OAuth token endpoint returned HTTP {exc.code}: {detail or exc.reason}",
    -            code=code,
    -        ) from exc
    -    except urllib.error.URLError as exc:
    -        raise AntigravityOAuthError(
    -            f"Antigravity OAuth token request failed: {exc}",
    -            code="antigravity_oauth_token_network_error",
    -        ) from exc
    -
    -
    -def exchange_code(
    -    code: str,
    -    verifier: str,
    -    redirect_uri: str,
    -    *,
    -    timeout: float = TOKEN_REQUEST_TIMEOUT_SECONDS,
    -) -> Dict[str, Any]:
    -    last_error: Optional[AntigravityOAuthError] = None
    -    candidates = _iter_client_credential_candidates()
    -    if not candidates:
    -        candidates = [_require_client_credentials()]
    -    for client_id, client_secret in candidates:
    -        data = {
    -            "grant_type": "authorization_code",
    -            "code": code,
    -            "code_verifier": verifier,
    -            "client_id": client_id,
    -            "client_secret": client_secret,
    -            "redirect_uri": redirect_uri,
    -        }
    -        try:
    -            return _post_form(TOKEN_ENDPOINT, data, timeout)
    -        except AntigravityOAuthError as exc:
    -            last_error = exc
    -            if exc.code != "antigravity_oauth_invalid_client":
    -                raise
    -    if last_error is not None:
    -        raise last_error
    -    raise AntigravityOAuthError(
    -        "Antigravity OAuth client credentials are unavailable.",
    -        code="antigravity_oauth_client_missing",
    -    )
    -
    -
    -def refresh_access_token(
    -    refresh_token: str,
    -    *,
    -    timeout: float = TOKEN_REQUEST_TIMEOUT_SECONDS,
    -) -> Dict[str, Any]:
    -    if not refresh_token:
    -        raise AntigravityOAuthError(
    -            "Cannot refresh: refresh_token is empty. Re-run OAuth login.",
    -            code="antigravity_oauth_refresh_token_missing",
    -        )
    -    last_error: Optional[AntigravityOAuthError] = None
    -    candidates = _iter_client_credential_candidates()
    -    if not candidates:
    -        candidates = [_require_client_credentials()]
    -    for client_id, client_secret in candidates:
    -        data = {
    -            "grant_type": "refresh_token",
    -            "refresh_token": refresh_token,
    -            "client_id": client_id,
    -            "client_secret": client_secret,
    -        }
    -        try:
    -            return _post_form(TOKEN_ENDPOINT, data, timeout)
    -        except AntigravityOAuthError as exc:
    -            last_error = exc
    -            if exc.code not in {
    -                "antigravity_oauth_invalid_client",
    -                "antigravity_oauth_invalid_grant",
    -            }:
    -                raise
    -    if last_error is not None:
    -        raise last_error
    -    raise AntigravityOAuthError(
    -        "Antigravity OAuth client credentials are unavailable.",
    -        code="antigravity_oauth_client_missing",
    -    )
    -
    -
    -def _fetch_user_email(access_token: str, timeout: float = TOKEN_REQUEST_TIMEOUT_SECONDS) -> str:
    -    try:
    -        request = urllib.request.Request(
    -            USERINFO_ENDPOINT + "?alt=json",
    -            headers={"Authorization": f"Bearer {access_token}"},
    -        )
    -        with urllib.request.urlopen(request, timeout=timeout) as response:
    -            raw = response.read().decode("utf-8", errors="replace")
    -        data = json.loads(raw)
    -        return str(data.get("email", "") or "")
    -    except Exception as exc:
    -        logger.debug("Antigravity userinfo fetch failed (non-fatal): %s", exc)
    -        return ""
    -
    -
    -_refresh_inflight: Dict[str, threading.Event] = {}
    -_refresh_inflight_lock = threading.Lock()
    -
    -
    -def get_valid_access_token(*, force_refresh: bool = False) -> str:
    -    creds = load_credentials()
    -    if creds is None:
    -        raise AntigravityOAuthError(
    -            "No Antigravity OAuth credentials found. Run `hermes login --provider google-antigravity` first.",
    -            code="antigravity_oauth_not_logged_in",
    -        )
    -    if not force_refresh and not creds.access_token_expired():
    -        return creds.access_token
    -
    -    rt = creds.refresh_token
    -    with _refresh_inflight_lock:
    -        event = _refresh_inflight.get(rt)
    -        if event is None:
    -            event = threading.Event()
    -            _refresh_inflight[rt] = event
    -            owner = True
    -        else:
    -            owner = False
    -
    -    if not owner:
    -        event.wait(timeout=LOCK_TIMEOUT_SECONDS)
    -        fresh = load_credentials()
    -        if fresh is not None and not fresh.access_token_expired():
    -            return fresh.access_token
    -
    -    try:
    -        try:
    -            resp = refresh_access_token(rt)
    -        except AntigravityOAuthError as exc:
    -            if exc.code == "antigravity_oauth_invalid_grant":
    -                clear_credentials()
    -            raise
    -        new_access = str(resp.get("access_token", "") or "").strip()
    -        if not new_access:
    -            raise AntigravityOAuthError(
    -                "Refresh response did not include an access_token.",
    -                code="antigravity_oauth_refresh_empty",
    -            )
    -        creds.access_token = [redacted:secret-assignment]
    -        creds.refresh_token = [redacted:secret-assignment]"refresh_token", "") or "").strip() or creds.refresh_token
    -        expires_in = int(resp.get("expires_in", 0) or 0)
    -        creds.expires_ms = int((time.time() + max(60, expires_in)) * 1000)
    -        save_credentials(creds)
    -        return creds.access_token
    -    finally:
    -        if owner:
    -            with _refresh_inflight_lock:
    -                _refresh_inflight.pop(rt, None)
    -            event.set()
    -
    -
    -def update_project_ids(project_id: str = "", managed_project_id: str = "") -> None:
    -    creds = load_credentials()
    -    if creds is None:
    -        return
    -    if project_id:
    -        creds.project_id = project_id
    -    if managed_project_id:
    -        creds.managed_project_id = managed_project_id
    -    save_credentials(creds)
    -
    -
    -class _OAuthCallbackHandler(http.server.BaseHTTPRequestHandler):
    -    expected_state: str = ""
    -    captured_code: Optional[str] = None
    -    captured_error: Optional[str] = None
    -    ready: Optional[threading.Event] = None
    -
    -    def log_message(self, format: str, *args: Any) -> None:  # noqa: A002, N802
    -        logger.debug("Antigravity OAuth callback: " + format, *args)
    -
    -    def do_GET(self) -> None:  # noqa: N802
    -        parsed = urllib.parse.urlparse(self.path)
    -        if parsed.path != CALLBACK_PATH:
    -            self.send_response(404)
    -            self.end_headers()
    -            return
    -
    -        params = urllib.parse.parse_qs(parsed.query)
    -        state = (params.get("state") or [""])[0]
    -        error = (params.get("error") or [""])[0]
    -        code = (params.get("code") or [""])[0]
    -
    -        handler_cls = type(self)
    -        if state != self.expected_state:
    -            handler_cls.captured_error = "OAuth state mismatch."
    -        elif error:
    -            handler_cls.captured_error = error
    -        elif not code:
    -            handler_cls.captured_error = "OAuth callback did not include a code."
    -        else:
    -            handler_cls.captured_code = code
    -
    -        ok = not handler_cls.captured_error
    -        self.send_response(200 if ok else 400)
    -        self.send_header("Content-Type", "text/html; charset=utf-8")
    -        self.end_headers()
    -        msg = "Antigravity OAuth complete. You can return to Hermes." if ok else handler_cls.captured_error
    -        self.wfile.write(f"<html><body><p>{msg}</p></body></html>".encode("utf-8"))
    -        if handler_cls.ready is not None:
    -            handler_cls.ready.set()
    -
    -
    -class _ReusableHTTPServer(http.server.HTTPServer):
    -    allow_reuse_address = True
    -
    -
    -def resolve_project_id_from_env() -> str:
    -    for key in ("HERMES_ANTIGRAVITY_PROJECT_ID", "GOOGLE_CLOUD_PROJECT", "GOOGLE_CLOUD_PROJECT_ID"):
    -        value = (os.getenv(key) or "").strip()
    -        if value:
    -            return value
    -    return ""
    -
    -
    -def start_oauth_flow(
    -    *,
    -    force_relogin: bool = False,
    -    open_browser: bool = True,
    -    port: int = DEFAULT_REDIRECT_PORT,
    -    project_id: str = "",
    -) -> AntigravityCredentials:
    -    if not force_relogin:
    -        existing = load_credentials()
    -        if existing and not existing.access_token_expired():
    -            return existing
    -
    -    verifier, challenge = _generate_pkce_pair()
    -    state = secrets.token_urlsafe(24)
    -    client_id, _ = _require_client_credentials()
    -
    -    ready = threading.Event()
    -    handler_cls = type("AntigravityOAuthCallbackHandler", (_OAuthCallbackHandler,), {})
    -    handler_cls.expected_state = state
    -    handler_cls.captured_code = None
    -    handler_cls.captured_error = None
    -    handler_cls.ready = ready
    -
    -    try:
    -        server = _ReusableHTTPServer((REDIRECT_HOST, int(port)), handler_cls)
    -    except OSError:
    -        server = _ReusableHTTPServer((REDIRECT_HOST, 0), handler_cls)
    -    actual_port = int(server.server_address[1])
    -    redirect_uri = f"http://{REDIRECT_HOST}:{actual_port}{CALLBACK_PATH}"
    -
    -    thread = threading.Thread(target=server.serve_forever, daemon=True)
    -    thread.start()
    -    try:
    -        params = {
    -            "client_id": client_id,
    -            "redirect_uri": redirect_uri,
    -            "response_type": "code",
    -            "scope": OAUTH_SCOPES,
    -            "access_type": "offline",
    -            "prompt": "consent",
    -            "state": state,
    -            "code_challenge": challenge,
    -            "code_challenge_method": "S256",
    -        }
    -        auth_url = AUTH_ENDPOINT + "?" + urllib.parse.urlencode(params)
    -        print("Open this URL to authorize Antigravity OAuth:")
    -        print(auth_url)
    -        if open_browser:
    -            webbrowser.open(auth_url)
    -        if not ready.wait(timeout=CALLBACK_WAIT_SECONDS):
    -            raise AntigravityOAuthError(
    -                "Timed out waiting for Antigravity OAuth callback.",
    -                code="antigravity_oauth_callback_timeout",
    -            )
    -        if handler_cls.captured_error:
    -            raise AntigravityOAuthError(
    -                handler_cls.captured_error,
    -                code="antigravity_oauth_callback_error",
    -            )
    -        code = handler_cls.captured_code or ""
    -        token = [redacted:secret-assignment] verifier, redirect_uri)
    -    finally:
    -        server.shutdown()
    -        server.server_close()
    -
    -    access_token = [redacted:secret-assignment]"access_token", "") or "").strip()
    -    refresh_token = [redacted:secret-assignment]"refresh_token", "") or "").strip()
    -    if not access_token or not refresh_token:
    -        raise AntigravityOAuthError(
    -            "Antigravity OAuth response did not include both access_token and refresh_token.",
    -            code="antigravity_oauth_missing_token",
    -        )
    -    expires_in = int(token.get("expires_in", 0) or 0)
    -    creds = AntigravityCredentials(
    -        access_token=[redacted:secret-assignment]
    -        refresh_token=[redacted:secret-assignment]
    -        expires_ms=int((time.time() + max(60, expires_in)) * 1000),
    -        email=_fetch_user_email(access_token),
    -        project_id=project_id,
    -    )
    -    save_credentials(creds)
    -    return creds
    -
    -
    -def run_antigravity_oauth_login_pure() -> Dict[str, Any]:
    -    creds = start_oauth_flow(
    -        force_relogin=True,
    -        project_id=resolve_project_id_from_env(),
    -    )
    -    return {
    -        "access_token": creds.access_token,
    -        "refresh_token": creds.refresh_token,
    -        "expires_at_ms": creds.expires_ms,
    -        "email": creds.email,
    -        "project_id": creds.project_id,
    -    }
    ```
  - `/dev/null`

    ```diff
    @@ -1,915 +0,0 @@
    -"""OpenAI-compatible facade that talks to Google's Cloud Code Assist backend.
    -
    -This adapter lets Hermes use the ``google-gemini-cli`` provider as if it were
    -a standard OpenAI-shaped chat completion endpoint, while the underlying HTTP
    -traffic goes to ``cloudcode-pa.googleapis.com/v1internal:{generateContent,
    -streamGenerateContent}`` with a Bearer access token obtained via OAuth PKCE.
    -
    -Architecture
    -------------
    -- ``GeminiCloudCodeClient`` exposes ``.chat.completions.create(**kwargs)``
    -  mirroring the subset of the OpenAI SDK that ``run_agent.py`` uses.
    -- Incoming OpenAI ``messages[]`` / ``tools[]`` / ``tool_choice`` are translated
    -  to Gemini's native ``contents[]`` / ``tools[].functionDeclarations`` /
    -  ``toolConfig`` / ``systemInstruction`` shape.
    -- The request body is wrapped ``{project, model, user_prompt_id, request}``
    -  per Code Assist API expectations.
    -- Responses (``candidates[].content.parts[]``) are converted back to
    -  OpenAI ``choices[0].message`` shape with ``content`` + ``tool_calls``.
    -- Streaming uses SSE (``?alt=sse``) and yields OpenAI-shaped delta chunks.
    -
    -Attribution
    ------------
    -Translation semantics follow jenslys/opencode-gemini-auth (MIT) and the public
    -Gemini API docs. Request envelope shape
    -(``{project, model, user_prompt_id, request}``) is documented nowhere; it is
    -reverse-engineered from the opencode-gemini-auth and clawdbot implementations.
    -"""
    -
    -from __future__ import annotations
    -
    -import json
    -import logging
    -import time
    -import uuid
    -from types import SimpleNamespace
    -from typing import Any, Dict, Iterator, List, Optional
    -
    -import httpx
    -
    -from agent import google_oauth
    -from agent.gemini_schema import sanitize_gemini_tool_parameters
    -from agent.google_code_assist import (
    -    CODE_ASSIST_ENDPOINT,
    -    CodeAssistError,
    -    ProjectContext,
    -    resolve_project_context,
    -)
    -
    -logger = logging.getLogger(__name__)
    -
    -
    -# =============================================================================
    -# Request translation: OpenAI → Gemini
    -# =============================================================================
    -
    -_ROLE_MAP_OPENAI_TO_GEMINI = {
    -    "user": "user",
    -    "assistant": "model",
    -    "system": "user",   # handled separately via systemInstruction
    -    "tool": "user",     # functionResponse is wrapped in a user-role turn
    -    "function": "user",
    -}
    -
    -
    -def _coerce_content_to_text(content: Any) -> str:
    -    """OpenAI content may be str or a list of parts; reduce to plain text."""
    -    if content is None:
    -        return ""
    -    if isinstance(content, str):
    -        return content
    -    if isinstance(content, list):
    -        pieces: List[str] = []
    -        for p in content:
    -            if isinstance(p, str):
    -                pieces.append(p)
    -            elif isinstance(p, dict):
    -                if p.get("type") == "text" and isinstance(p.get("text"), str):
    -                    pieces.append(p["text"])
    -                # Multimodal (image_url, etc.) — stub for now; log and skip
    -                elif p.get("type") in {"image_url", "input_audio"}:
    -                    logger.debug("Dropping multimodal part (not yet supported): %s", p.get("type"))
    -        return "\n".join(pieces)
    -    return str(content)
    -
    -
    -def _translate_tool_call_to_gemini(tool_call: Dict[str, Any]) -> Dict[str, Any]:
    -    """OpenAI tool_call -> Gemini functionCall part."""
    -    fn = tool_call.get("function") or {}
    -    args_raw = fn.get("arguments", "")
    -    try:
    -        args = json.loads(args_raw) if isinstance(args_raw, str) and args_raw else {}
    -    except json.JSONDecodeError:
    -        args = {"_raw": args_raw}
    -    if not isinstance(args, dict):
    -        args = {"_value": args}
    -    function_call = {
    -        "name": fn.get("name") or "",
    -        "args": args,
    -    }
    -    if tool_call.get("id"):
    -        function_call["id"] = str(tool_call["id"])
    -    return {
    -        "functionCall": function_call,
    -        # Sentinel signature — matches opencode-gemini-auth's approach.
    -        # Without this, Code Assist rejects function calls that originated
    -        # outside its own chain.
    -        "thoughtSignature": "skip_thought_signature_validator",
    -    }
    -
    -
    -def _translate_tool_result_to_gemini(message: Dict[str, Any]) -> Dict[str, Any]:
    -    """OpenAI tool-role message -> Gemini functionResponse part.
    -
    -    The function name isn't in the OpenAI tool message directly; it must be
    -    passed via the assistant message that issued the call. For simplicity we
    -    look up ``name`` on the message (OpenAI SDK copies it there) or on the
    -    ``tool_call_id`` cross-reference.
    -    """
    -    name = str(message.get("name") or message.get("tool_call_id") or "tool")
    -    content = _coerce_content_to_text(message.get("content"))
    -    # Gemini expects the response as a dict under `response`. We wrap plain
    -    # text in {"output": "..."}.
    -    try:
    -        parsed = json.loads(content) if content.strip().startswith(("{", "[")) else None
    -    except json.JSONDecodeError:
    -        parsed = None
    -    response = parsed if isinstance(parsed, dict) else {"output": content}
    -    function_response = {
    -        "name": name,
    -        "response": response,
    -    }
    -    if message.get("tool_call_id"):
    -        function_response["id"] = str(message["tool_call_id"])
    -    return {"functionResponse": function_response}
    -
    -
    -def _build_gemini_contents(
    -    messages: List[Dict[str, Any]],
    -) -> tuple[List[Dict[str, Any]], Optional[Dict[str, Any]]]:
    -    """Convert OpenAI messages[] to Gemini contents[] + systemInstruction."""
    -    system_text_parts: List[str] = []
    -    contents: List[Dict[str, Any]] = []
    -
    -    for msg in messages:
    -        if not isinstance(msg, dict):
    -            continue
    -        role = str(msg.get("role") or "user")
    -
    -        if role == "system":
    -            system_text_parts.append(_coerce_content_to_text(msg.get("content")))
    -            continue
    -
    -        # Tool result message — emit a user-role turn with functionResponse
    -        if role == "tool" or role == "function":
    -            contents.append({
    -                "role": "user",
    -                "parts": [_translate_tool_result_to_gemini(msg)],
    -            })
    -            continue
    -
    -        gemini_role = _ROLE_MAP_OPENAI_TO_GEMINI.get(role, "user")
    -        parts: List[Dict[str, Any]] = []
    -
    -        text = _coerce_content_to_text(msg.get("content"))
    -        if text:
    -            parts.append({"text": text})
    -
    -        # Assistant messages can carry tool_calls
    -        tool_calls = msg.get("tool_calls") or []
    -        if isinstance(tool_calls, list):
    -            for tc in tool_calls:
    -                if isinstance(tc, dict):
    -                    parts.append(_translate_tool_call_to_gemini(tc))
    -
    -        if not parts:
    -            # Gemini rejects empty parts; skip the turn entirely
    -            continue
    -
    -        contents.append({"role": gemini_role, "parts": parts})
    -
    -    system_instruction: Optional[Dict[str, Any]] = None
    -    joined_system = "\n".join(p for p in system_text_parts if p).strip()
    -    if joined_system:
    -        system_instruction = {
    -            "role": "system",
    -            "parts": [{"text": joined_system}],
    -        }
    -
    -    return contents, system_instruction
    -
    -
    -def _translate_tools_to_gemini(tools: Any) -> List[Dict[str, Any]]:
    -    """OpenAI tools[] -> Gemini tools[].functionDeclarations[]."""
    -    if not isinstance(tools, list) or not tools:
    -        return []
    -    declarations: List[Dict[str, Any]] = []
    -    for t in tools:
    -        if not isinstance(t, dict):
    -            continue
    -        fn = t.get("function") or {}
    -        if not isinstance(fn, dict):
    -            continue
    -        name = fn.get("name")
    -        if not name:
    -            continue
    -        decl = {"name": str(name)}
    -        if fn.get("description"):
    -            decl["description"] = str(fn["description"])
    -        params = fn.get("parameters")
    -        if isinstance(params, dict):
    -            decl["parameters"] = sanitize_gemini_tool_parameters(params)
    -        declarations.append(decl)
    -    if not declarations:
    -        return []
    -    return [{"functionDeclarations": declarations}]
    -
    -
    -def _translate_tool_choice_to_gemini(tool_choice: Any) -> Optional[Dict[str, Any]]:
    -    """OpenAI tool_choice -> Gemini toolConfig.functionCallingConfig."""
    -    if tool_choice is None:
    -        return None
    -    if isinstance(tool_choice, str):
    -        if tool_choice == "auto":
    -            return {"functionCallingConfig": {"mode": "AUTO"}}
    -        if tool_choice == "required":
    -            return {"functionCallingConfig": {"mode": "ANY"}}
    -        if tool_choice == "none":
    -            return {"functionCallingConfig": {"mode": "NONE"}}
    -    if isinstance(tool_choice, dict):
    -        fn = tool_choice.get("function") or {}
    -        name = fn.get("name")
    -        if name:
    -            return {
    -                "functionCallingConfig": {
    -                    "mode": "ANY",
    -                    "allowedFunctionNames": [str(name)],
    -                },
    -            }
    -    return None
    -
    -
    -def _normalize_thinking_config(config: Any) -> Optional[Dict[str, Any]]:
    -    """Accept thinkingBudget / thinkingLevel / includeThoughts (+ snake_case)."""
    -    if not isinstance(config, dict) or not config:
    -        return None
    -    budget = config.get("thinkingBudget", config.get("thinking_budget"))
    -    level = config.get("thinkingLevel", config.get("thinking_level"))
    -    include = config.get("includeThoughts", config.get("include_thoughts"))
    -    normalized: Dict[str, Any] = {}
    -    if isinstance(budget, (int, float)):
    -        normalized["thinkingBudget"] = int(budget)
    -    if isinstance(level, str) and level.strip():
    -        normalized["thinkingLevel"] = level.strip().lower()
    -    if isinstance(include, bool):
    -        normalized["includeThoughts"] = include
    -    return normalized or None
    -
    -
    -def build_gemini_request(
    -    *,
    -    messages: List[Dict[str, Any]],
    -    tools: Any = None,
    -    tool_choice: Any = None,
    -    temperature: Optional[float] = None,
    -    max_tokens: [redacted:secret-assignment] = None,
    -    top_p: Optional[float] = None,
    -    stop: Any = None,
    -    thinking_config: Any = None,
    -) -> Dict[str, Any]:
    -    """Build the inner Gemini request body (goes inside ``request`` wrapper)."""
    -    contents, system_instruction = _build_gemini_contents(messages)
    -
    -    body: Dict[str, Any] = {"contents": contents}
    -    if system_instruction is not None:
    -        body["systemInstruction"] = system_instruction
    -
    -    gemini_tools = _translate_tools_to_gemini(tools)
    -    if gemini_tools:
    -        body["tools"] = gemini_tools
    -    tool_cfg = _translate_tool_choice_to_gemini(tool_choice)
    -    if tool_cfg is not None:
    -        body["toolConfig"] = tool_cfg
    -
    -    generation_config: Dict[str, Any] = {}
    -    if isinstance(temperature, (int, float)):
    -        generation_config["temperature"] = float(temperature)
    -    if isinstance(max_tokens, int) and max_tokens > 0:
    -        generation_config["maxOutputTokens"] = max_tokens
    -    if isinstance(top_p, (int, float)):
    -        generation_config["topP"] = float(top_p)
    -    if isinstance(stop, str) and stop:
    -        generation_config["stopSequences"] = [stop]
    -    elif isinstance(stop, list) and stop:
    -        generation_config["stopSequences"] = [str(s) for s in stop if s]
    -    normalized_thinking = _normalize_thinking_config(thinking_config)
    -    if normalized_thinking:
    -        generation_config["thinkingConfig"] = normalized_thinking
    -    if generation_config:
    -        body["generationConfig"] = generation_config
    -
    -    return body
    -
    -
    -def wrap_code_assist_request(
    -    *,
    -    project_id: str,
    -    model: str,
    -    inner_request: Dict[str, Any],
    -    user_prompt_id: Optional[str] = None,
    -) -> Dict[str, Any]:
    -    """Wrap the inner Gemini request in the Code Assist envelope."""
    -    return {
    -        "project": project_id,
    -        "model": model,
    -        "user_prompt_id": user_prompt_id or str(uuid.uuid4()),
    -        "request": inner_request,
    -    }
    -
    -
    -# =============================================================================
    -# Response translation: Gemini → OpenAI
    -# =============================================================================
    -
    -def _translate_gemini_response(
    -    resp: Dict[str, Any],
    -    model: str,
    -) -> SimpleNamespace:
    -    """Non-streaming Gemini response -> OpenAI-shaped SimpleNamespace.
    -
    -    Code Assist wraps the actual Gemini response inside ``response``, so we
    -    unwrap it first if present.
    -    """
    -    inner = resp.get("response") if isinstance(resp.get("response"), dict) else resp
    -
    -    candidates = inner.get("candidates") or []
    -    if not isinstance(candidates, list) or not candidates:
    -        return _empty_response(model)
    -
    -    cand = candidates[0]
    -    content_obj = cand.get("content") if isinstance(cand, dict) else {}
    -    parts = content_obj.get("parts") if isinstance(content_obj, dict) else []
    -
    -    text_pieces: List[str] = []
    -    reasoning_pieces: List[str] = []
    -    tool_calls: List[SimpleNamespace] = []
    -
    -    for i, part in enumerate(parts or []):
    -        if not isinstance(part, dict):
    -            continue
    -        # Thought parts are model's internal reasoning — surface as reasoning,
    -        # don't mix into content.
    -        if part.get("thought") is True:
    -            if isinstance(part.get("text"), str):
    -                reasoning_pieces.append(part["text"])
    -            continue
    -        if isinstance(part.get("text"), str):
    -            text_pieces.append(part["text"])
    -            continue
    -        fc = part.get("functionCall")
    -        if isinstance(fc, dict) and fc.get("name"):
    -            try:
    -                args_str = json.dumps(fc.get("args") or {}, ensure_ascii=False)
    -            except (TypeError, ValueError):
    -                args_str = "{}"
    -            call_id = str(fc.get("id") or "").strip() or f"call_{uuid.uuid4().hex[:12]}"
    -            tool_calls.append(SimpleNamespace(
    -                id=call_id,
    -                type="function",
    -                index=i,
    -                function=SimpleNamespace(name=str(fc["name"]), arguments=args_str),
    -            ))
    -
    -    finish_reason = "tool_calls" if tool_calls else _map_gemini_finish_reason(
    -        str(cand.get("finishReason") or "")
    -    )
    -
    -    usage_meta = inner.get("usageMetadata") or {}
    -    usage = SimpleNamespace(
    -        prompt_tokens=[redacted:secret-assignment]"promptTokenCount") or 0),
    -        completion_tokens=[redacted:secret-assignment]"candidatesTokenCount") or 0),
    -        total_tokens=[redacted:secret-assignment]"totalTokenCount") or 0),
    -        prompt_tokens_details=[redacted:secret-assignment]
    -            cached_tokens=[redacted:secret-assignment]"cachedContentTokenCount") or 0),
    -        ),
    -    )
    -
    -    message = SimpleNamespace(
    -        role="assistant",
    -        content="".join(text_pieces) if text_pieces else None,
    -        tool_calls=tool_calls or None,
    -        reasoning="".join(reasoning_pieces) or None,
    -        reasoning_content="".join(reasoning_pieces) or None,
    -        reasoning_details=None,
    -    )
    -    choice = SimpleNamespace(
    -        index=0,
    -        message=message,
    -        finish_reason=finish_reason,
    -    )
    -    return SimpleNamespace(
    -        id=f"chatcmpl-{uuid.uuid4().hex[:12]}",
    -        object="chat.completion",
    -        created=int(time.time()),
    -        model=model,
    -        choices=[choice],
    -        usage=usage,
    -    )
    -
    -
    -def _empty_response(model: str) -> SimpleNamespace:
    -    message = SimpleNamespace(
    -        role="assistant", content="", tool_calls=None,
    -        reasoning=None, reasoning_content=None, reasoning_details=None,
    -    )
    -    choice = SimpleNamespace(index=0, message=message, finish_reason="stop")
    -    usage = SimpleNamespace(
    -        prompt_tokens=0, completion_tokens=0, total_tokens=0,
    -        prompt_tokens_details=[redacted:secret-assignment]
    -    )
    -    return SimpleNamespace(
    -        id=f"chatcmpl-{uuid.uuid4().hex[:12]}",
    -        object="chat.completion",
    -        created=int(time.time()),
    -        model=model,
    -        choices=[choice],
    -        usage=usage,
    -    )
    -
    -
    -def _map_gemini_finish_reason(reason: str) -> str:
    -    mapping = {
    -        "STOP": "stop",
    -        "MAX_TOKENS": "length",
    -        "SAFETY": "content_filter",
    -        "RECITATION": "content_filter",
    -        "OTHER": "stop",
    -    }
    -    return mapping.get(reason.upper(), "stop")
    -
    -
    -# =============================================================================
    -# Streaming SSE iterator
    -# =============================================================================
    -
    -class _GeminiStreamChunk(SimpleNamespace):
    -    """Mimics an OpenAI ChatCompletionChunk with .choices[0].delta."""
    -    pass
    -
    -
    -def _make_stream_chunk(
    -    *,
    -    model: str,
    -    content: str = "",
    -    tool_call_delta: Optional[Dict[str, Any]] = None,
    -    finish_reason: Optional[str] = None,
    -    reasoning: str = "",
    -) -> _GeminiStreamChunk:
    -    delta_kwargs: Dict[str, Any] = {
    -        "role": "assistant",
    -        "content": None,
    -        "tool_calls": None,
    -        "reasoning": None,
    -        "reasoning_content": None,
    -    }
    -    if content:
    -        delta_kwargs["content"] = content
    -    if tool_call_delta is not None:
    -        delta_kwargs["tool_calls"] = [SimpleNamespace(
    -            index=tool_call_delta.get("index", 0),
    -            id=tool_call_delta.get("id") or f"call_{uuid.uuid4().hex[:12]}",
    -            type="function",
    -            function=SimpleNamespace(
    -                name=tool_call_delta.get("name") or "",
    -                arguments=tool_call_delta.get("arguments") or "",
    -            ),
    -        )]
    -    if reasoning:
    -        delta_kwargs["reasoning"] = reasoning
    -        delta_kwargs["reasoning_content"] = reasoning
    -    delta = SimpleNamespace(**delta_kwargs)
    -    choice = SimpleNamespace(index=0, delta=delta, finish_reason=finish_reason)
    -    return _GeminiStreamChunk(
    -        id=f"chatcmpl-{uuid.uuid4().hex[:12]}",
    -        object="chat.completion.chunk",
    -        created=int(time.time()),
    -        model=model,
    -        choices=[choice],
    -        usage=None,
    -    )
    -
    -
    -def _iter_sse_events(response: httpx.Response) -> Iterator[Dict[str, Any]]:
    -    """Parse Server-Sent Events from an httpx streaming response."""
    -    buffer = ""
    -    for chunk in response.iter_text():
    -        if not chunk:
    -            continue
    -        buffer += chunk
    -        while "\n" in buffer:
    -            line, buffer = buffer.split("\n", 1)
    -            line = line.rstrip("\r")
    -            if not line:
    -                continue
    -            if line.startswith("data: "):
    -                data = line[6:]
    -                if data == "[DONE]":
    -                    return
    -                try:
    -                    yield json.loads(data)
    -                except json.JSONDecodeError:
    -                    logger.debug("Non-JSON SSE line: %s", data[:200])
    -
    -
    -def _translate_stream_event(
    -    event: Dict[str, Any],
    -    model: str,
    -    tool_call_counter: List[int],
    -) -> List[_GeminiStreamChunk]:
    -    """Unwrap Code Assist envelope and emit OpenAI-shaped chunk(s).
    -
    -    ``tool_call_counter`` is a single-element list used as a mutable counter
    -    across events in the same stream. Each ``functionCall`` part gets a
    -    fresh, unique OpenAI ``index`` — keying by function name would collide
    -    whenever the model issues parallel calls to the same tool (e.g. reading
    -    three files in one turn).
    -    """
    -    inner = event.get("response") if isinstance(event.get("response"), dict) else event
    -    candidates = inner.get("candidates") or []
    -    if not candidates:
    -        return []
    -    cand = candidates[0]
    -    if not isinstance(cand, dict):
    -        return []
    -
    -    chunks: List[_GeminiStreamChunk] = []
    -
    -    content = cand.get("content") or {}
    -    parts = content.get("parts") if isinstance(content, dict) else []
    -    for part in parts or []:
    -        if not isinstance(part, dict):
    -            continue
    -        if part.get("thought") is True and isinstance(part.get("text"), str):
    -            chunks.append(_make_stream_chunk(
    -                model=model, reasoning=part["text"],
    -            ))
    -            continue
    -        if isinstance(part.get("text"), str) and part["text"]:
    -            chunks.append(_make_stream_chunk(model=model, content=part["text"]))
    -        fc = part.get("functionCall")
    -        if isinstance(fc, dict) and fc.get("name"):
    -            name = str(fc["name"])
    -            idx = tool_call_counter[0]
    -            tool_call_counter[0] += 1
    -            try:
    -                args_str = json.dumps(fc.get("args") or {}, ensure_ascii=False)
    -            except (TypeError, ValueError):
    -                args_str = "{}"
    -            chunks.append(_make_stream_chunk(
    -                model=model,
    -                tool_call_delta={
    -                    "index": idx,
    -                    "id": str(fc.get("id") or "").strip(),
    -                    "name": name,
    -                    "arguments": args_str,
    -                },
    -            ))
    -
    -    finish_reason_raw = str(cand.get("finishReason") or "")
    -    if finish_reason_raw:
    -        mapped = _map_gemini_finish_reason(finish_reason_raw)
    -        if tool_call_counter[0] > 0:
    -            mapped = "tool_calls"
    -        chunks.append(_make_stream_chunk(model=model, finish_reason=mapped))
    -    return chunks
    -
    -
    -# =============================================================================
    -# GeminiCloudCodeClient — OpenAI-compatible facade
    -# =============================================================================
    -
    -MARKER_BASE_URL = "cloudcode-pa://google"
    -
    -
    -class _GeminiChatCompletions:
    -    def __init__(self, client: "GeminiCloudCodeClient"):
    -        self._client = client
    -
    -    def create(self, **kwargs: Any) -> Any:
    -        return self._client._create_chat_completion(**kwargs)
    -
    -
    -class _GeminiChatNamespace:
    -    def __init__(self, client: "GeminiCloudCodeClient"):
    -        self.completions = _GeminiChatCompletions(client)
    -
    -
    -class GeminiCloudCodeClient:
    -    """Minimal OpenAI-SDK-compatible facade over Code Assist v1internal."""
    -
    -    def __init__(
    -        self,
    -        *,
    -        api_key: [redacted:secret-assignment] = None,
    -        base_url: Optional[str] = None,
    -        default_headers: Optional[Dict[str, str]] = None,
    -        project_id: str = "",
    -        **_: Any,
    -    ):
    -        # `api_key` here is a dummy — real auth is the OAuth access token
    -        # fetched on every call via agent.google_oauth.get_valid_access_token().
    -        # We accept the kwarg for openai.OpenAI interface parity.
    -        self.api_key = [redacted:secret-assignment] or "google-oauth"
    -        self.base_url = base_url or MARKER_BASE_URL
    -        self._default_headers = dict(default_headers or {})
    -        self._configured_project_id = project_id
    -        self._project_context: Optional[ProjectContext] = None
    -        self._project_context_lock = False  # simple single-thread guard
    -        self.chat = _GeminiChatNamespace(self)
    -        self.is_closed = False
    -        self._http = httpx.Client(timeout=httpx.Timeout(connect=15.0, read=600.0, write=30.0, pool=30.0))
    -
    -    def close(self) -> None:
    -        self.is_closed = True
    -        try:
    -            self._http.close()
    -        except Exception:
    -            pass
    -
    -    # Implement the OpenAI SDK's context-manager-ish closure check
    -    def __enter__(self):
    -        return self
    -
    -    def __exit__(self, exc_type, exc_val, exc_tb):
    -        self.close()
    -
    -    def _ensure_project_context(self, access_token: str, model: str) -> ProjectContext:
    -        """Lazily resolve and cache the project context for this client."""
    -        if self._project_context is not None:
    -            return self._project_context
    -
    -        env_project = google_oauth.resolve_project_id_from_env()
    -        creds = google_oauth.load_credentials()
    -        stored_project = creds.project_id if creds else ""
    -
    -        # Prefer what's already baked into the creds
    -        if stored_project:
    -            self._project_context = ProjectContext(
    -                project_id=stored_project,
    -                managed_project_id=creds.managed_project_id if creds else "",
    -                tier_id="",
    -                source="stored",
    -            )
    -            return self._project_context
    -
    -        ctx = resolve_project_context(
    -            access_token,
    -            configured_project_id=self._configured_project_id,
    -            env_project_id=env_project,
    -            user_agent_model=model,
    -        )
    -        # Persist discovered project back to the creds file so the next
    -        # session doesn't re-run the discovery.
    -        if ctx.project_id or ctx.managed_project_id:
    -            google_oauth.update_project_ids(
    -                project_id=ctx.project_id,
    -                managed_project_id=ctx.managed_project_id,
    -            )
    -        self._project_context = ctx
    -        return ctx
    -
    -    def _create_chat_completion(
    -        self,
    -        *,
    -        model: str = "gemini-2.5-flash",
    -        messages: Optional[List[Dict[str, Any]]] = None,
    -        stream: bool = False,
    -        tools: Any = None,
    -        tool_choice: Any = None,
    -        temperature: Optional[float] = None,
    -        max_tokens: [redacted:secret-assignment] = None,
    -        top_p: Optional[float] = None,
    -        stop: Any = None,
    -        extra_body: Optional[Dict[str, Any]] = None,
    -        timeout: Any = None,
    -        **_: Any,
    -    ) -> Any:
    -        access_token = [redacted:secret-assignment]
    -        ctx = self._ensure_project_context(access_token, model)
    -
    -        thinking_config = None
    -        if isinstance(extra_body, dict):
    -            thinking_config = extra_body.get("thinking_config") or extra_body.get("thinkingConfig")
    -
    -        inner = build_gemini_request(
    -            messages=messages or [],
    -            tools=tools,
    -            tool_choice=tool_choice,
    -            temperature=temperature,
    -            max_tokens=[redacted:secret-assignment]
    -            top_p=top_p,
    -            stop=stop,
    -            thinking_config=thinking_config,
    -        )
    -        wrapped = wrap_code_assist_request(
    -            project_id=ctx.project_id,
    -            model=model,
    -            inner_request=inner,
    -        )
    -
    -        headers = {
    -            "Content-Type": "application/json",
    -            "Accept": "application/json",
    -            "Authorization": f"Bearer {access_token}",
    -            "User-Agent": "hermes-agent (gemini-cli-compat)",
    -            "X-Goog-Api-Client": "gl-python/hermes",
    -            "x-activity-request-id": str(uuid.uuid4()),
    -        }
    -        headers.update(self._default_headers)
    -
    -        if stream:
    -            return self._stream_completion(model=model, wrapped=wrapped, headers=headers)
    -
    -        url = f"{CODE_ASSIST_ENDPOINT}/v1internal:generateContent"
    -        response = self._http.post(url, json=wrapped, headers=headers)
    -        if response.status_code != 200:
    -            raise _gemini_http_error(response)
    -        try:
    -            payload = response.json()
    -        except ValueError as exc:
    -            raise CodeAssistError(
    -                f"Invalid JSON from Code Assist: {exc}",
    -                code="code_assist_invalid_json",
    -            ) from exc
    -        return _translate_gemini_response(payload, model=model)
    -
    -    def _stream_completion(
    -        self,
    -        *,
    -        model: str,
    -        wrapped: Dict[str, Any],
    -        headers: Dict[str, str],
    -    ) -> Iterator[_GeminiStreamChunk]:
    -        """Generator that yields OpenAI-shaped streaming chunks."""
    -        url = f"{CODE_ASSIST_ENDPOINT}/v1internal:streamGenerateContent?alt=sse"
    -        stream_headers = dict(headers)
    -        stream_headers["Accept"] = "text/event-stream"
    -
    -        def _generator() -> Iterator[_GeminiStreamChunk]:
    -            try:
    -                with self._http.stream("POST", url, json=wrapped, headers=stream_headers) as response:
    -                    if response.status_code != 200:
    -                        # Materialize error body for better diagnostics
    -                        response.read()
    -                        raise _gemini_http_error(response)
    -                    tool_call_counter: List[int] = [0]
    -                    for event in _iter_sse_events(response):
    -                        for chunk in _translate_stream_event(event, model, tool_call_counter):
    -                            yield chunk
    -            except httpx.HTTPError as exc:
    -                raise CodeAssistError(
    -                    f"Streaming request failed: {exc}",
    -                    code="code_assist_stream_error",
    -                ) from exc
    -
    -        return _generator()
    -
    -
    -def _gemini_http_error(response: httpx.Response) -> CodeAssistError:
    -    """Translate an httpx response into a CodeAssistError with rich metadata.
    -
    -    Parses Google's error envelope (``{"error": {"code", "message", "status",
    -    "details": [...]}}``) so the agent's error classifier can reason about
    -    the failure — ``status_code`` enables the rate_limit / auth classification
    -    paths, and ``response`` lets the main loop honor ``Retry-After`` just
    -    like it does for OpenAI SDK exceptions.
    -
    -    Also lifts a few recognizable Google conditions into human-readable
    -    messages so the user sees something better than a 500-char JSON dump:
    -
    -        MODEL_CAPACITY_EXHAUSTED → "Gemini model capacity exhausted for
    -            <model>. This is a Google-side throttle..."
    -        RESOURCE_EXHAUSTED w/o reason → quota-style message
    -        404 → "Model <name> not found at cloudcode-pa..."
    -    """
    -    status = response.status_code
    -
    -    # Parse the body once, surviving any weird encodings.
    -    body_text = ""
    -    body_json: Dict[str, Any] = {}
    -    try:
    -        body_text = response.text
    -    except Exception:
    -        body_text = ""
    -    if body_text:
    -        try:
    -            parsed = json.loads(body_text)
    -            if isinstance(parsed, dict):
    -                body_json = parsed
    -        except (ValueError, TypeError):
    -            body_json = {}
    -
    -    # Dig into Google's error envelope.  Shape is:
    -    #   {"error": {"code": 429, "message": "...", "status": "RESOURCE_EXHAUSTED",
    -    #              "details": [{"@type": ".../ErrorInfo", "reason": "MODEL_CAPACITY_EXHAUSTED",
    -    #                           "metadata": {...}},
    -    #                          {"@type": ".../RetryInfo", "retryDelay": "30s"}]}}
    -    err_obj = body_json.get("error") if isinstance(body_json, dict) else None
    -    if not isinstance(err_obj, dict):
    -        err_obj = {}
    -    err_status = str(err_obj.get("status") or "").strip()
    -    err_message = str(err_obj.get("message") or "").strip()
    -    _raw_details = err_obj.get("details")
    -    err_details_list = _raw_details if isinstance(_raw_details, list) else []
    -
    -    # Extract google.rpc.ErrorInfo reason + metadata.  There may be more
    -    # than one ErrorInfo (rare), so we pick the first one with a reason.
    -    error_reason = ""
    -    error_metadata: Dict[str, Any] = {}
    -    retry_delay_seconds: Optional[float] = None
    -    for detail in err_details_list:
    -        if not isinstance(detail, dict):
    -            continue
    -        type_url = str(detail.get("@type") or "")
    -        if not error_reason and type_url.endswith("/google.rpc.ErrorInfo"):
    -            reason = detail.get("reason")
    -            if isinstance(reason, str) and reason:
    -                error_reason = reason
    -            md = detail.get("metadata")
    -            if isinstance(md, dict):
    -                error_metadata = md
    -        elif retry_delay_seconds is None and type_url.endswith("/google.rpc.RetryInfo"):
    -            # retryDelay is a google.protobuf.Duration string like "30s" or "1.5s".
    -            delay_raw = detail.get("retryDelay")
    -            if isinstance(delay_raw, str) and delay_raw.endswith("s"):
    -                try:
    -                    retry_delay_seconds = float(delay_raw[:-1])
    -                except ValueError:
    -                    pass
    -            elif isinstance(delay_raw, (int, float)):
    -                retry_delay_seconds = float(delay_raw)
    -
    -    # Fall back to the Retry-After header if the body didn't include RetryInfo.
    -    if retry_delay_seconds is None:
    -        try:
    -            header_val = response.headers.get("Retry-After") or response.headers.get("retry-after")
    -        except Exception:
    -            header_val = None
    -        if header_val:
    -            try:
    -                retry_delay_seconds = float(header_val)
    -            except (TypeError, ValueError):
    -                retry_delay_seconds = None
    -
    -    # Classify the error code.  ``code_assist_rate_limited`` stays the default
    -    # for 429s; a more specific reason tag helps downstream callers (e.g. tests,
    -    # logs) without changing the rate_limit classification path.
    -    code = f"code_assist_http_{status}"
    -    if status == 401:
    -        code = "code_assist_unauthorized"
    -    elif status == 429:
    -        code = "code_assist_rate_limited"
    -        if error_reason == "MODEL_CAPACITY_EXHAUSTED":
    -            code = "code_assist_capacity_exhausted"
    -
    -    # Build a human-readable message.  Keep the status + a raw-body tail for
    -    # debugging, but lead with a friendlier summary when we recognize the
    -    # Google signal.
    -    model_hint = ""
    -    if isinstance(error_metadata, dict):
    -        model_hint = str(error_metadata.get("model") or error_metadata.get("modelId") or "").strip()
    -
    -    if status == 429 and error_reason == "MODEL_CAPACITY_EXHAUSTED":
    -        target = model_hint or "this Gemini model"
    -        message = (
    -            f"Gemini capacity exhausted for {target} (Google-side throttle, "
    -            f"not a Hermes issue). Try a different Gemini model or set a "
    -            f"fallback_providers entry to a non-Gemini provider."
    -        )
    -        if retry_delay_seconds is not None:
    -            message += f" Google suggests retrying in {retry_delay_seconds:g}s."
    -    elif status == 429 and err_status == "RESOURCE_EXHAUSTED":
    -        message = (
    -            f"Gemini quota exhausted ({err_message or 'RESOURCE_EXHAUSTED'}). "
    -            f"Check /gquota for remaining daily requests."
    -        )
    -        if retry_delay_seconds is not None:
    -            message += f" Retry suggested in {retry_delay_seconds:g}s."
    -    elif status == 404:
    -        # Google returns 404 when a model has been retired or renamed.
    -        target = model_hint or (err_message or "model")
    -        message = (
    -            f"Code Assist 404: {target} is not available at "
    -            f"cloudcode-pa.googleapis.com. It may have been renamed or "
    -            f"retired. Check hermes_cli/models.py for the current list."
    -        )
    -    elif err_message:
    -        # Generic fallback with the parsed message.
    -        message = f"Code Assist HTTP {status} ({err_status or 'error'}): {err_message}"
    -    else:
    -        # Last-ditch fallback — raw body snippet.
    -        message = f"Code Assist returned HTTP {status}: {body_text[:500]}"
    -
    -    return CodeAssistError(
    -        message,
    -        code=code,
    -        status_code=status,
    -        response=response,
    -        retry_after=retry_delay_seconds,
    -        details={
    -            "status": err_status,
    -            "reason": error_reason,
    -            "metadata": error_metadata,
    -            "message": err_message,
    -        },
    -    )
    ```
  - `/dev/null`

    ```diff
    @@ -1,451 +0,0 @@
    -"""Google Code Assist API client — project discovery, onboarding, quota.
    -
    -The Code Assist API powers Google's official gemini-cli. It sits at
    -``cloudcode-pa.googleapis.com`` and provides:
    -
    -- Free tier access (generous daily quota) for personal Google accounts
    -- Paid tier access via GCP projects with billing / Workspace / Standard / Enterprise
    -
    -This module handles the control-plane dance needed before inference:
    -
    -1. ``load_code_assist()`` — probe the user's account to learn what tier they're on
    -   and whether a ``cloudaicompanionProject`` is already assigned.
    -2. ``onboard_user()`` — if the user hasn't been onboarded yet (new account, fresh
    -   free tier, etc.), call this with the chosen tier + project id. Supports LRO
    -   polling for slow provisioning.
    -3. ``retrieve_user_quota()`` — fetch the ``buckets[]`` array showing remaining
    -   quota per model, used by the ``/gquota`` slash command.
    -
    -VPC-SC handling: enterprise accounts under a VPC Service Controls perimeter
    -will get ``SECURITY_POLICY_VIOLATED`` on ``load_code_assist``. We catch this
    -and force the account to ``standard-tier`` so the call chain still succeeds.
    -
    -Derived from opencode-gemini-auth (MIT) and clawdbot/extensions/google. The
    -request/response shapes are specific to Google's internal Code Assist API,
    -documented nowhere public — we copy them from the reference implementations.
    -"""
    -
    -from __future__ import annotations
    -
    -import json
    -import logging
    -import time
    -import urllib.error
    -import urllib.request
    -import uuid
    -from dataclasses import dataclass, field
    -from typing import Any, Dict, List, Optional
    -
    -logger = logging.getLogger(__name__)
    -
    -
    -# =============================================================================
    -# Constants
    -# =============================================================================
    -
    -CODE_ASSIST_ENDPOINT = "https://cloudcode-pa.googleapis.com"
    -
    -# Fallback endpoints tried when prod returns an error during project discovery
    -FALLBACK_ENDPOINTS = [
    -    "https://daily-cloudcode-pa.sandbox.googleapis.com",
    -    "https://autopush-cloudcode-pa.sandbox.googleapis.com",
    -]
    -
    -# Tier identifiers that Google's API uses
    -FREE_TIER_ID = "free-tier"
    -LEGACY_TIER_ID = "legacy-tier"
    -STANDARD_TIER_ID = "standard-tier"
    -
    -# Default HTTP headers matching gemini-cli's fingerprint.
    -# Google may reject unrecognized User-Agents on these internal endpoints.
    -_GEMINI_CLI_USER_AGENT = "google-api-nodejs-client/9.15.1 (gzip)"
    -_X_GOOG_API_CLIENT = "gl-node/24.0.0"
    -_DEFAULT_REQUEST_TIMEOUT = 30.0
    -_ONBOARDING_POLL_ATTEMPTS = 12
    -_ONBOARDING_POLL_INTERVAL_SECONDS = 5.0
    -
    -
    -class CodeAssistError(RuntimeError):
    -    """Exception raised by the Code Assist (``cloudcode-pa``) integration.
    -
    -    Carries HTTP status / response / retry-after metadata so the agent's
    -    ``error_classifier._extract_status_code`` and the main loop's Retry-After
    -    handling (which walks ``error.response.headers``) pick up the right
    -    signals.  Without these, 429s from the OAuth path look like opaque
    -    ``RuntimeError`` and skip the rate-limit path.
    -    """
    -
    -    def __init__(
    -        self,
    -        message: str,
    -        *,
    -        code: str = "code_assist_error",
    -        status_code: Optional[int] = None,
    -        response: Any = None,
    -        retry_after: Optional[float] = None,
    -        details: Optional[Dict[str, Any]] = None,
    -    ) -> None:
    -        super().__init__(message)
    -        self.code = code
    -        # ``status_code`` is picked up by ``agent.error_classifier._extract_status_code``
    -        # so a 429 from Code Assist classifies as FailoverReason.rate_limit and
    -        # triggers the main loop's fallback_providers chain the same way SDK
    -        # errors do.
    -        self.status_code = status_code
    -        # ``response`` is the underlying ``httpx.Response`` (or a shim with a
    -        # ``.headers`` mapping and ``.json()`` method).  The main loop reads
    -        # ``error.response.headers["Retry-After"]`` to honor Google's retry
    -        # hints when the backend throttles us.
    -        self.response = response
    -        # Parsed ``Retry-After`` seconds (kept separately for convenience —
    -        # Google returns retry hints in both the header and the error body's
    -        # ``google.rpc.RetryInfo`` details, and we pick whichever we found).
    -        self.retry_after = retry_after
    -        # Parsed structured error details from the Google error envelope
    -        # (e.g. ``{"reason": "MODEL_CAPACITY_EXHAUSTED", "status": "RESOURCE_EXHAUSTED"}``).
    -        # Useful for logging and for tests that want to assert on specifics.
    -        self.details = details or {}
    -
    -
    -class ProjectIdRequiredError(CodeAssistError):
    -    def __init__(self, message: str = "GCP project id required for this tier") -> None:
    -        super().__init__(message, code="code_assist_project_id_required")
    -
    -
    -# =============================================================================
    -# HTTP primitive (auth via Bearer token passed per-call)
    -# =============================================================================
    -
    -def _build_headers(access_token: str, *, user_agent_model: str = "") -> Dict[str, str]:
    -    ua = _GEMINI_CLI_USER_AGENT
    -    if user_agent_model:
    -        ua = f"{ua} model/{user_agent_model}"
    -    return {
    -        "Content-Type": "application/json",
    -        "Accept": "application/json",
    -        "Authorization": f"Bearer {access_token}",
    -        "User-Agent": ua,
    -        "X-Goog-Api-Client": _X_GOOG_API_CLIENT,
    -        "x-activity-request-id": str(uuid.uuid4()),
    -    }
    -
    -
    -def _client_metadata() -> Dict[str, str]:
    -    """Match Google's gemini-cli exactly — unrecognized metadata may be rejected."""
    -    return {
    -        "ideType": "IDE_UNSPECIFIED",
    -        "platform": "PLATFORM_UNSPECIFIED",
    -        "pluginType": "GEMINI",
    -    }
    -
    -
    -def _post_json(
    -    url: str,
    -    body: Dict[str, Any],
    -    access_token: str,
    -    *,
    -    timeout: float = _DEFAULT_REQUEST_TIMEOUT,
    -    user_agent_model: str = "",
    -) -> Dict[str, Any]:
    -    data = json.dumps(body).encode("utf-8")
    -    request = urllib.request.Request(
    -        url, data=data, method="POST",
    -        headers=_build_headers(access_token, user_agent_model=user_agent_model),
    -    )
    -    try:
    -        with urllib.request.urlopen(request, timeout=timeout) as response:
    -            raw = response.read().decode("utf-8", errors="replace")
    -            return json.loads(raw) if raw else {}
    -    except urllib.error.HTTPError as exc:
    -        detail = ""
    -        try:
    -            detail = exc.read().decode("utf-8", errors="replace")
    -        except Exception:
    -            pass
    -        # Special case: VPC-SC violation should be distinguishable
    -        if _is_vpc_sc_violation(detail):
    -            raise CodeAssistError(
    -                f"VPC-SC policy violation: {detail}",
    -                code="code_assist_vpc_sc",
    -            ) from exc
    -        raise CodeAssistError(
    -            f"Code Assist HTTP {exc.code}: {detail or exc.reason}",
    -            code=f"code_assist_http_{exc.code}",
    -        ) from exc
    -    except urllib.error.URLError as exc:
    -        raise CodeAssistError(
    -            f"Code Assist request failed: {exc}",
    -            code="code_assist_network_error",
    -        ) from exc
    -
    -
    -def _is_vpc_sc_violation(body: str) -> bool:
    -    """Detect a VPC Service Controls violation from a response body."""
    -    if not body:
    -        return False
    -    try:
    -        parsed = json.loads(body)
    -    except (json.JSONDecodeError, ValueError):
    -        return "SECURITY_POLICY_VIOLATED" in body
    -    # Walk the nested error structure Google uses
    -    error = parsed.get("error") if isinstance(parsed, dict) else None
    -    if not isinstance(error, dict):
    -        return False
    -    details = error.get("details") or []
    -    if isinstance(details, list):
    -        for item in details:
    -            if isinstance(item, dict):
    -                reason = item.get("reason") or ""
    -                if reason == "SECURITY_POLICY_VIOLATED":
    -                    return True
    -    msg = str(error.get("message", ""))
    -    return "SECURITY_POLICY_VIOLATED" in msg
    -
    -
    -# =============================================================================
    -# load_code_assist — discovers current tier + assigned project
    -# =============================================================================
    -
    -@dataclass
    -class CodeAssistProjectInfo:
    -    """Result from ``load_code_assist``."""
    -    current_tier_id: str = ""
    -    cloudaicompanion_project: str = ""   # Google-managed project (free tier)
    -    allowed_tiers: List[str] = field(default_factory=list)
    -    raw: Dict[str, Any] = field(default_factory=dict)
    -
    -
    -def load_code_assist(
    -    access_token: str,
    -    *,
    -    project_id: str = "",
    -    user_agent_model: str = "",
    -) -> CodeAssistProjectInfo:
    -    """Call ``POST /v1internal:loadCodeAssist`` with prod → sandbox fallback.
    -
    -    Returns whatever tier + project info Google reports. On VPC-SC violations,
    -    returns a synthetic ``standard-tier`` result so the chain can continue.
    -    """
    -    body: Dict[str, Any] = {
    -        "metadata": {
    -            "duetProject": project_id,
    -            **_client_metadata(),
    -        },
    -    }
    -    if project_id:
    -        body["cloudaicompanionProject"] = project_id
    -
    -    endpoints = [CODE_ASSIST_ENDPOINT] + FALLBACK_ENDPOINTS
    -    last_err: Optional[Exception] = None
    -    for endpoint in endpoints:
    -        url = f"{endpoint}/v1internal:loadCodeAssist"
    -        try:
    -            resp = _post_json(url, body, access_token, user_agent_model=user_agent_model)
    -            return _parse_load_response(resp)
    -        except CodeAssistError as exc:
    -            if exc.code == "code_assist_vpc_sc":
    -                logger.info("VPC-SC violation on %s — defaulting to standard-tier", endpoint)
    -                return CodeAssistProjectInfo(
    -                    current_tier_id=STANDARD_TIER_ID,
    -                    cloudaicompanion_project=project_id,
    -                )
    -            last_err = exc
    -            logger.warning("loadCodeAssist failed on %s: %s", endpoint, exc)
    -            continue
    -    if last_err:
    -        raise last_err
    -    return CodeAssistProjectInfo()
    -
    -
    -def _parse_load_response(resp: Dict[str, Any]) -> CodeAssistProjectInfo:
    -    current_tier = resp.get("currentTier") or {}
    -    tier_id = str(current_tier.get("id") or "") if isinstance(current_tier, dict) else ""
    -    project = str(resp.get("cloudaicompanionProject") or "")
    -    allowed = resp.get("allowedTiers") or []
    -    allowed_ids: List[str] = []
    -    if isinstance(allowed, list):
    -        for t in allowed:
    -            if isinstance(t, dict):
    -                tid = str(t.get("id") or "")
    -                if tid:
    -                    allowed_ids.append(tid)
    -    return CodeAssistProjectInfo(
    -        current_tier_id=tier_id,
    -        cloudaicompanion_project=project,
    -        allowed_tiers=allowed_ids,
    -        raw=resp,
    -    )
    -
    -
    -# =============================================================================
    -# onboard_user — provisions a new user on a tier (with LRO polling)
    -# =============================================================================
    -
    -def onboard_user(
    -    access_token: str,
    -    *,
    -    tier_id: str,
    -    project_id: str = "",
    -    user_agent_model: str = "",
    -) -> Dict[str, Any]:
    -    """Call ``POST /v1internal:onboardUser`` to provision the user.
    -
    -    For paid tiers, ``project_id`` is REQUIRED (raises ProjectIdRequiredError).
    -    For free tiers, ``project_id`` is optional — Google will assign one.
    -
    -    Returns the final operation response. Polls ``/v1internal/<name>`` for up
    -    to ``_ONBOARDING_POLL_ATTEMPTS`` × ``_ONBOARDING_POLL_INTERVAL_SECONDS``
    -    (default: 12 × 5s = 1 min).
    -    """
    -    if tier_id != FREE_TIER_ID and tier_id != LEGACY_TIER_ID and not project_id:
    -        raise ProjectIdRequiredError(
    -            f"Tier {tier_id!r} requires a GCP project id. "
    -            "Set HERMES_GEMINI_PROJECT_ID or GOOGLE_CLOUD_PROJECT."
    -        )
    -
    -    body: Dict[str, Any] = {
    -        "tierId": tier_id,
    -        "metadata": _client_metadata(),
    -    }
    -    if project_id:
    -        body["cloudaicompanionProject"] = project_id
    -
    -    endpoint = CODE_ASSIST_ENDPOINT
    -    url = f"{endpoint}/v1internal:onboardUser"
    -    resp = _post_json(url, body, access_token, user_agent_model=user_agent_model)
    -
    -    # Poll if LRO (long-running operation)
    -    if not resp.get("done"):
    -        op_name = resp.get("name", "")
    -        if not op_name:
    -            return resp
    -        for attempt in range(_ONBOARDING_POLL_ATTEMPTS):
    -            time.sleep(_ONBOARDING_POLL_INTERVAL_SECONDS)
    -            poll_url = f"{endpoint}/v1internal/{op_name}"
    -            try:
    -                poll_resp = _post_json(poll_url, {}, access_token, user_agent_model=user_agent_model)
    -            except CodeAssistError as exc:
    -                logger.warning("Onboarding poll attempt %d failed: %s", attempt + 1, exc)
    -                continue
    -            if poll_resp.get("done"):
    -                return poll_resp
    -        logger.warning("Onboarding did not complete within %d attempts", _ONBOARDING_POLL_ATTEMPTS)
    -    return resp
    -
    -
    -# =============================================================================
    -# retrieve_user_quota — for /gquota
    -# =============================================================================
    -
    -@dataclass
    -class QuotaBucket:
    -    model_id: str
    -    token_type: str = ""
    -    remaining_fraction: float = 0.0
    -    reset_time_iso: str = ""
    -    raw: Dict[str, Any] = field(default_factory=dict)
    -
    -
    -def retrieve_user_quota(
    -    access_token: str,
    -    *,
    -    project_id: str = "",
    -    user_agent_model: str = "",
    -) -> List[QuotaBucket]:
    -    """Call ``POST /v1internal:retrieveUserQuota`` and parse ``buckets[]``."""
    -    body: Dict[str, Any] = {}
    -    if project_id:
    -        body["project"] = project_id
    -    url = f"{CODE_ASSIST_ENDPOINT}/v1internal:retrieveUserQuota"
    -    resp = _post_json(url, body, access_token, user_agent_model=user_agent_model)
    -    raw_buckets = resp.get("buckets") or []
    -    buckets: List[QuotaBucket] = []
    -    if not isinstance(raw_buckets, list):
    -        return buckets
    -    for b in raw_buckets:
    -        if not isinstance(b, dict):
    -            continue
    -        buckets.append(QuotaBucket(
    -            model_id=str(b.get("modelId") or ""),
    -            token_type=[redacted:secret-assignment]"tokenType") or ""),
    -            remaining_fraction=float(b.get("remainingFraction") or 0.0),
    -            reset_time_iso=str(b.get("resetTime") or ""),
    -            raw=b,
    -        ))
    -    return buckets
    -
    -
    -# =============================================================================
    -# Project context resolution
    -# =============================================================================
    -
    -@dataclass
    -class ProjectContext:
    -    """Resolved state for a given OAuth session."""
    -    project_id: str = ""           # effective project id sent on requests
    -    managed_project_id: str = ""   # Google-assigned project (free tier)
    -    tier_id: str = ""
    -    source: str = ""               # "env", "config", "discovered", "onboarded"
    -
    -
    -def resolve_project_context(
    -    access_token: str,
    -    *,
    -    configured_project_id: str = "",
    -    env_project_id: str = "",
    -    user_agent_model: str = "",
    -) -> ProjectContext:
    -    """Figure out what project id + tier to use for requests.
    -
    -    Priority:
    -      1. If configured_project_id or env_project_id is set, use that directly
    -         and short-circuit (no discovery needed).
    -      2. Otherwise call loadCodeAssist to see what Google says.
    -      3. If no tier assigned yet, onboard the user (free tier default).
    -    """
    -    # Short-circuit: caller provided a project id
    -    if configured_project_id:
    -        return ProjectContext(
    -            project_id=configured_project_id,
    -            tier_id=STANDARD_TIER_ID,  # assume paid since they specified one
    -            source="config",
    -        )
    -    if env_project_id:
    -        return ProjectContext(
    -            project_id=env_project_id,
    -            tier_id=STANDARD_TIER_ID,
    -            source="env",
    -        )
    -
    -    # Discover via loadCodeAssist
    -    info = load_code_assist(access_token, user_agent_model=user_agent_model)
    -
    -    effective_project = info.cloudaicompanion_project
    -    tier = info.current_tier_id
    -
    -    if not tier:
    -        # User hasn't been onboarded — provision them on free tier
    -        onboard_resp = onboard_user(
    -            access_token,
    -            tier_id=FREE_TIER_ID,
    -            project_id="",
    -            user_agent_model=user_agent_model,
    -        )
    -        # Re-parse from the onboard response
    -        response_body = onboard_resp.get("response") or {}
    -        if isinstance(response_body, dict):
    -            effective_project = (
    -                effective_project
    -                or str(response_body.get("cloudaicompanionProject") or "")
    -            )
    -        tier = FREE_TIER_ID
    -        source = "onboarded"
    -    else:
    -        source = "discovered"
    -
    -    return ProjectContext(
    -        project_id=effective_project,
    -        managed_project_id=effective_project if tier == FREE_TIER_ID else "",
    -        tier_id=tier,
    -        source=source,
    -    )
    ```
  - `/dev/null`

    ```diff
    @@ -1,1067 +0,0 @@
    -"""Google OAuth PKCE flow for the Gemini (google-gemini-cli) inference provider.
    -
    -This module implements Authorization Code + PKCE (S256) OAuth against Google's
    -accounts.google.com endpoints. The resulting access token is used by
    -``agent.gemini_cloudcode_adapter`` to talk to ``cloudcode-pa.googleapis.com``
    -(Google's Code Assist backend that powers the Gemini CLI's free and paid tiers).
    -
    -Synthesized from:
    -- jenslys/opencode-gemini-auth (MIT) — overall flow shape, public OAuth creds, request format
    -- clawdbot/extensions/google/ — refresh-token rotation, VPC-SC handling reference
    -- PRs #10176 (@sliverp) and #10779 (@newarthur) — PKCE module structure, cross-process lock
    -
    -Storage (``~/.hermes/auth/google_oauth.json``, chmod 0o600):
    -
    -    {
    -      "refresh": "refreshToken|projectId|managedProjectId",
    -      "access": "...",
    -      "expires": 1744848000000,   // unix MILLIseconds
    -      "email": "[redacted:email]"
    -    }
    -
    -The ``refresh`` field packs the refresh_token together with the resolved GCP
    -project IDs so subsequent sessions don't need to re-discover the project.
    -This matches opencode-gemini-auth's storage contract exactly.
    -
    -The packed format stays parseable even if no project IDs are present — just
    -a bare refresh_token is treated as "packed with empty IDs".
    -
    -Public client credentials
    --------------------------
    -The client_id and client_secret below are Google's PUBLIC desktop OAuth client
    -for their own open-source gemini-cli. They are baked into every copy of the
    -gemini-cli npm package and are NOT confidential — desktop OAuth clients have
    -no secret-keeping requirement (PKCE provides the security). Shipping them here
    -is consistent with opencode-gemini-auth and the official Google gemini-cli.
    -
    -Policy note: Google considers using this OAuth client with third-party software
    -a policy violation. Users see an upfront warning with ``confirm(default=False)``
    -before authorization begins.
    -"""
    -
    -from __future__ import annotations
    -
    -import base64
    -import contextlib
    -import hashlib
    -import http.server
    -import json
    -import logging
    -import os
    -import secrets
    -import stat
    -import threading
    -import time
    -import urllib.error
    -import urllib.parse
    -import urllib.request
    -from dataclasses import dataclass
    -from pathlib import Path
    -from typing import Any, Dict, Optional, Tuple
    -
    -from hermes_constants import get_hermes_home, secure_parent_dir
    -
    -logger = logging.getLogger(__name__)
    -
    -
    -# =============================================================================
    -# OAuth client credential resolution.
    -#
    -# Resolution order:
    -#   1. HERMES_GEMINI_CLIENT_ID / HERMES_GEMINI_CLIENT_SECRET env vars (power users)
    -#   2. Shipped defaults — Google's public gemini-cli desktop OAuth client
    -#      (baked into every copy of Google's open-source gemini-cli; NOT
    -#      confidential — desktop OAuth clients use PKCE, not client_secret, for
    -#      security). Using these matches opencode-gemini-auth behavior.
    -#   3. Fallback: scrape from a locally installed gemini-cli binary (helps forks
    -#      that deliberately wipe the shipped defaults).
    -#   4. Fail with a helpful error.
    -# =============================================================================
    -
    -ENV_CLIENT_ID = "HERMES_GEMINI_CLIENT_ID"
    -ENV_CLIENT_SECRET = "[redacted:secret-assignment]"
    -
    -# Public gemini-cli desktop OAuth client (shipped in Google's open-source
    -# gemini-cli MIT repo). Composed piecewise to keep the constants readable and
    -# to pair each piece with an explicit comment about why it is non-confidential.
    -# See: https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/code_assist/oauth2.ts
    -_PUBLIC_CLIENT_ID_PROJECT_NUM = "681255809395"
    -_PUBLIC_CLIENT_ID_HASH = "oo8ft2oprdrnp9e3aqf6av3hmdib135j"
    -_PUBLIC_CLIENT_SECRET_SUFFIX = "[redacted:secret-assignment]"
    -
    -_DEFAULT_CLIENT_ID = (
    -    f"{_PUBLIC_CLIENT_ID_PROJECT_NUM}-{_PUBLIC_CLIENT_ID_HASH}"
    -    ".apps.googleusercontent.com"
    -)
    -_DEFAULT_CLIENT_SECRET = f"GOCSPX-{_PUBLIC_CLIENT_SECRET_SUFFIX}"
    -
    -# Regex patterns for fallback scraping from an installed gemini-cli.
    -import re as _re
    -from utils import atomic_replace
    -_CLIENT_ID_PATTERN = _re.compile(
    -    r"OAUTH_CLIENT_ID\s*=\s*['\"]([0-9]+-[a-z0-9]+\.apps\.googleusercontent\.com)['\"]"
    -)
    -_CLIENT_SECRET_PATTERN = [redacted:secret-assignment]
    -    r"OAUTH_CLIENT_SECRET\s*=\s*['\"](GOCSPX-[A-Za-z0-9_-]+)['\"]"
    -)
    -_CLIENT_ID_SHAPE = _re.compile(r"([0-9]{8,}-[a-z0-9]{20,}\.apps\.googleusercontent\.com)")
    -_CLIENT_SECRET_SHAPE = [redacted:secret-assignment]"(GOCSPX-[A-Za-z0-9_-]{20,})")
    -
    -
    -# =============================================================================
    -# Endpoints & constants
    -# =============================================================================
    -
    -AUTH_ENDPOINT = "https://accounts.google.com/o/oauth2/v2/auth"
    -TOKEN_ENDPOINT = "[redacted:secret-assignment]"
    -USERINFO_ENDPOINT = "https://www.googleapis.com/oauth2/v1/userinfo"
    -
    -OAUTH_SCOPES = (
    -    "https://www.googleapis.com/auth/cloud-platform "
    -    "https://www.googleapis.com/auth/userinfo.email "
    -    "https://www.googleapis.com/auth/userinfo.profile"
    -)
    -
    -DEFAULT_REDIRECT_PORT = 8085
    -REDIRECT_HOST = "127.0.0.1"
    -CALLBACK_PATH = "/oauth2callback"
    -
    -# 60-second clock skew buffer (matches opencode-gemini-auth).
    -REFRESH_SKEW_SECONDS = 60
    -
    -TOKEN_REQUEST_TIMEOUT_SECONDS = 20.0
    -CALLBACK_WAIT_SECONDS = 300
    -LOCK_TIMEOUT_SECONDS = 30.0
    -
    -# Headless env detection
    -_HEADLESS_ENV_VARS = ("SSH_CONNECTION", "SSH_CLIENT", "SSH_TTY", "HERMES_HEADLESS")
    -
    -
    -# =============================================================================
    -# Error type
    -# =============================================================================
    -
    -class GoogleOAuthError(RuntimeError):
    -    """Raised for any failure in the Google OAuth flow."""
    -
    -    def __init__(self, message: str, *, code: str = "google_oauth_error") -> None:
    -        super().__init__(message)
    -        self.code = code
    -
    -
    -# =============================================================================
    -# File paths & cross-process locking
    -# =============================================================================
    -
    -def _credentials_path() -> Path:
    -    return get_hermes_home() / "auth" / "google_oauth.json"
    -
    -
    -def _lock_path() -> Path:
    -    return _credentials_path().with_suffix(".json.lock")
    -
    -
    -_lock_state = threading.local()
    -
    -
    [redacted:email]
    -def _credentials_lock(timeout_seconds: float = LOCK_TIMEOUT_SECONDS):
    -    """Cross-process lock around the credentials file (fcntl POSIX / msvcrt Windows)."""
    -    depth = getattr(_lock_state, "depth", 0)
    -    if depth > 0:
    -        _lock_state.depth = depth + 1
    -        try:
    -            yield
    -        finally:
    -            _lock_state.depth -= 1
    -        return
    -
    -    lock_file_path = _lock_path()
    -    lock_file_path.parent.mkdir(parents=True, exist_ok=True)
    -    fd = os.open(str(lock_file_path), os.O_CREAT | os.O_RDWR, 0o600)
    -    acquired = False
    -    try:
    -        try:
    -            import fcntl
    -        except ImportError:
    -            fcntl = None
    -
    -        if fcntl is not None:
    -            deadline = time.monotonic() + max(0.0, float(timeout_seconds))
    -            while True:
    -                try:
    -                    fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
    -                    acquired = True
    -                    break
    -                except BlockingIOError:
    -                    if time.monotonic() >= deadline:
    -                        raise TimeoutError(
    -                            f"Timed out acquiring Google OAuth credentials lock at {lock_file_path}."
    -                        )
    -                    time.sleep(0.05)
    -        else:
    -            try:
    -                import msvcrt  # type: ignore[import-not-found]
    -
    -                deadline = time.monotonic() + max(0.0, float(timeout_seconds))
    -                while True:
    -                    try:
    -                        msvcrt.locking(fd, msvcrt.LK_NBLCK, 1)
    -                        acquired = True
    -                        break
    -                    except OSError:
    -                        if time.monotonic() >= deadline:
    -                            raise TimeoutError(
    -                                f"Timed out acquiring Google OAuth credentials lock at {lock_file_path}."
    -                            )
    -                        time.sleep(0.05)
    -            except ImportError:
    -                acquired = True
    -
    -        _lock_state.depth = 1
    -        yield
    -    finally:
    -        try:
    -            if acquired:
    -                try:
    -                    import fcntl
    -
    -                    fcntl.flock(fd, fcntl.LOCK_UN)
    -                except ImportError:
    -                    try:
    -                        import msvcrt  # type: ignore[import-not-found]
    -
    -                        try:
    -                            msvcrt.locking(fd, msvcrt.LK_UNLCK, 1)
    -                        except OSError:
    -                            pass
    -                    except ImportError:
    -                        pass
    -        finally:
    -            os.close(fd)
    -            _lock_state.depth = 0
    -
    -
    -# =============================================================================
    -# Client ID resolution
    -# =============================================================================
    -
    -_scraped_creds_cache: Dict[str, str] = {}
    -
    -
    -def _locate_gemini_cli_oauth_js() -> Optional[Path]:
    -    """Walk the user's gemini binary install to find its oauth2.js.
    -
    -    Returns None if gemini isn't installed. Supports both the npm install
    -    (``node_modules/@google/gemini-cli-core/dist/**/code_assist/oauth2.js``)
    -    and the Homebrew ``bundle/`` layout.
    -    """
    -    import shutil
    -
    -    gemini = shutil.which("gemini")
    -    if not gemini:
    -        return None
    -
    -    try:
    -        real = Path(gemini).resolve()
    -    except OSError:
    -        return None
    -
    -    # Walk up from the binary to find npm install root
    -    search_dirs: list[Path] = []
    -    cur = real.parent
    -    for _ in range(8):  # don't walk too far
    -        search_dirs.append(cur)
    -        if (cur / "node_modules").exists():
    -            search_dirs.append(cur / "node_modules" / "@google" / "gemini-cli-core")
    -            break
    -        if cur.parent == cur:
    -            break
    -        cur = cur.parent
    -
    -    for root in search_dirs:
    -        if not root.exists():
    -            continue
    -        # Common known paths
    -        candidates = [
    -            root / "dist" / "src" / "code_assist" / "oauth2.js",
    -            root / "dist" / "code_assist" / "oauth2.js",
    -            root / "src" / "code_assist" / "oauth2.js",
    -        ]
    -        for c in candidates:
    -            if c.exists():
    -                return c
    -        # Recursive fallback: look for oauth2.js within 10 dirs deep
    -        try:
    -            for path in root.rglob("oauth2.js"):
    -                return path
    -        except (OSError, ValueError):
    -            continue
    -
    -    return None
    -
    -
    -def _scrape_client_credentials() -> Tuple[str, str]:
    -    """Extract client_id + client_secret from the local gemini-cli install."""
    -    if _scraped_creds_cache.get("resolved"):
    -        return _scraped_creds_cache.get("client_id", ""), _scraped_creds_cache.get("client_secret", "")
    -
    -    oauth_js = _locate_gemini_cli_oauth_js()
    -    if oauth_js is None:
    -        _scraped_creds_cache["resolved"] = "1"  # Don't retry on every call
    -        return "", ""
    -
    -    try:
    -        content = oauth_js.read_text(encoding="utf-8", errors="replace")
    -    except OSError as exc:
    -        logger.debug("Failed to read oauth2.js at %s: %s", oauth_js, exc)
    -        _scraped_creds_cache["resolved"] = "1"
    -        return "", ""
    -
    -    # Precise pattern first, then fallback shape match
    -    cid_match = _CLIENT_ID_PATTERN.search(content) or _CLIENT_ID_SHAPE.search(content)
    -    cs_match = _CLIENT_SECRET_PATTERN.search(content) or _CLIENT_SECRET_SHAPE.search(content)
    -
    -    client_id = cid_match.group(1) if cid_match else ""
    -    client_secret = [redacted:secret-assignment] if cs_match else ""
    -
    -    _scraped_creds_cache["client_id"] = client_id
    -    _scraped_creds_cache["client_secret"] = client_secret
    -    _scraped_creds_cache["resolved"] = "1"
    -
    -    if client_id:
    -        logger.info("Scraped Gemini OAuth client from %s", oauth_js)
    -
    -    return client_id, client_secret
    -
    -
    -def _get_client_id() -> str:
    -    env_val = (os.getenv(ENV_CLIENT_ID) or "").strip()
    -    if env_val:
    -        return env_val
    -    if _DEFAULT_CLIENT_ID:
    -        return _DEFAULT_CLIENT_ID
    -    scraped, _ = _scrape_client_credentials()
    -    return scraped
    -
    -
    -def _get_client_secret() -> str:
    -    env_val = (os.getenv(ENV_CLIENT_SECRET) or "").strip()
    -    if env_val:
    -        return env_val
    -    if _DEFAULT_CLIENT_SECRET:
    -        return _DEFAULT_CLIENT_SECRET
    -    _, scraped = _scrape_client_credentials()
    -    return scraped
    -
    -
    -def _require_client_id() -> str:
    -    cid = _get_client_id()
    -    if not cid:
    -        raise GoogleOAuthError(
    -            "Google OAuth client ID is not available.\n"
    -            "Hermes looks for a locally installed gemini-cli to source the OAuth client. "
    -            "Either:\n"
    -            "  1. Install it: npm install -g @google/gemini-cli  (or brew install gemini-cli)\n"
    -            "  2. Set HERMES_GEMINI_CLIENT_ID and HERMES_GEMINI_CLIENT_SECRET in ~/.hermes/.env\n"
    -            "\n"
    -            "Register a Desktop OAuth client at:\n"
    -            "  https://console.cloud.google.com/apis/credentials\n"
    -            "(enable the Generative Language API on the project).",
    -            code="google_oauth_client_id_missing",
    -        )
    -    return cid
    -
    -
    -# =============================================================================
    -# PKCE
    -# =============================================================================
    -
    -def _generate_pkce_pair() -> Tuple[str, str]:
    -    """Generate a (verifier, challenge) pair using S256."""
    -    verifier = secrets.token_urlsafe(64)
    -    digest = hashlib.sha256(verifier.encode("ascii")).digest()
    -    challenge = base64.urlsafe_b64encode(digest).rstrip(b"=").decode("ascii")
    -    return verifier, challenge
    -
    -
    -# =============================================================================
    -# Packed refresh format:  refresh_token[|project_id[|managed_project_id]]
    -# =============================================================================
    -
    -@dataclass
    -class RefreshParts:
    -    refresh_token: str
    -    project_id: str = ""
    -    managed_project_id: str = ""
    -
    -    @classmethod
    -    def parse(cls, packed: str) -> "RefreshParts":
    -        if not packed:
    -            return cls(refresh_token="")
    -        parts = packed.split("|", 2)
    -        return cls(
    -            refresh_token=[redacted:secret-assignment]
    -            project_id=parts[1] if len(parts) > 1 else "",
    -            managed_project_id=parts[2] if len(parts) > 2 else "",
    -        )
    -
    -    def format(self) -> str:
    -        if not self.refresh_token:
    -            return ""
    -        if not self.project_id and not self.managed_project_id:
    -            return self.refresh_token
    -        return f"{self.refresh_token}|{self.project_id}|{self.managed_project_id}"
    -
    -
    -# =============================================================================
    -# Credentials (dataclass wrapping the on-disk format)
    -# =============================================================================
    -
    -@dataclass
    -class GoogleCredentials:
    -    access_token: str
    -    refresh_token: str
    -    expires_ms: int  # unix milliseconds
    -    email: str = ""
    -    project_id: str = ""
    -    managed_project_id: str = ""
    -
    -    def to_dict(self) -> Dict[str, Any]:
    -        return {
    -            "refresh": RefreshParts(
    -                refresh_token=[redacted:secret-assignment]
    -                project_id=self.project_id,
    -                managed_project_id=self.managed_project_id,
    -            ).format(),
    -            "access": self.access_token,
    -            "expires": int(self.expires_ms),
    -            "email": self.email,
    -        }
    -
    -    @classmethod
    -    def from_dict(cls, data: Dict[str, Any]) -> "GoogleCredentials":
    -        refresh_packed = str(data.get("refresh", "") or "")
    -        parts = RefreshParts.parse(refresh_packed)
    -        return cls(
    -            access_token=[redacted:secret-assignment]"access", "") or ""),
    -            refresh_token=[redacted:secret-assignment]
    -            expires_ms=int(data.get("expires", 0) or 0),
    -            email=str(data.get("email", "") or ""),
    -            project_id=parts.project_id,
    -            managed_project_id=parts.managed_project_id,
    -        )
    -
    -    def expires_unix_seconds(self) -> float:
    -        return self.expires_ms / 1000.0
    -
    -    def access_token_expired(self, skew_seconds: int = REFRESH_SKEW_SECONDS) -> bool:
    -        if not self.access_token or not self.expires_ms:
    -            return True
    -        return (time.time() + max(0, skew_seconds)) * 1000 >= self.expires_ms
    -
    -
    -# =============================================================================
    -# Credential I/O (atomic + locked)
    -# =============================================================================
    -
    -def load_credentials() -> Optional[GoogleCredentials]:
    -    """Load credentials from disk. Returns None if missing or corrupt."""
    -    path = _credentials_path()
    -    if not path.exists():
    -        return None
    -    try:
    -        with _credentials_lock():
    -            raw = path.read_text(encoding="utf-8")
    -        data = json.loads(raw)
    -    except (json.JSONDecodeError, OSError, IOError) as exc:
    -        logger.warning("Failed to read Google OAuth credentials at %s: %s", path, exc)
    -        return None
    -    if not isinstance(data, dict):
    -        return None
    -    creds = GoogleCredentials.from_dict(data)
    -    if not creds.access_token:
    -        return None
    -    return creds
    -
    -
    -def save_credentials(creds: GoogleCredentials) -> Path:
    -    """Atomically write creds to disk with 0o600 permissions."""
    -    path = _credentials_path()
    -    path.parent.mkdir(parents=True, exist_ok=True)
    -    # Tighten parent dir to 0o700 so siblings can't traverse to the creds file.
    -    # On Windows this is a no-op (POSIX mode bits aren't enforced); ignore failures.
    -    # secure_parent_dir refuses to chmod / or top-level dirs (#25821).
    -    secure_parent_dir(path)
    -    payload = json.dumps(creds.to_dict(), indent=2, sort_keys=True) + "\n"
    -
    -    with _credentials_lock():
    -        tmp_path = path.with_suffix(f".tmp.{os.getpid()}.{secrets.token_hex(4)}")
    -        try:
    -            # Create with 0o600 atomically to close the TOCTOU window where the
    -            # default umask (often 0o644) would briefly expose tokens to other
    -            # local users between open() and chmod().
    -            fd = os.open(
    -                str(tmp_path),
    -                os.O_WRONLY | os.O_CREAT | os.O_EXCL,
    -                stat.S_IRUSR | stat.S_IWUSR,
    -            )
    -            with os.fdopen(fd, "w", encoding="utf-8") as fh:
    -                fh.write(payload)
    -                fh.flush()
    -                os.fsync(fh.fileno())
    -            atomic_replace(tmp_path, path)
    -        finally:
    -            try:
    -                if tmp_path.exists():
    -                    tmp_path.unlink()
    -            except OSError:
    -                pass
    -    return path
    -
    -
    -def clear_credentials() -> None:
    -    """Remove the creds file. Idempotent."""
    -    path = _credentials_path()
    -    with _credentials_lock():
    -        try:
    -            path.unlink()
    -        except FileNotFoundError:
    -            pass
    -        except OSError as exc:
    -            logger.warning("Failed to remove Google OAuth credentials at %s: %s", path, exc)
    -
    -
    -# =============================================================================
    -# HTTP helpers
    -# =============================================================================
    -
    -def _post_form(url: str, data: Dict[str, str], timeout: float) -> Dict[str, Any]:
    -    """POST x-www-form-urlencoded and return parsed JSON response."""
    -    body = urllib.parse.urlencode(data).encode("ascii")
    -    request = urllib.request.Request(
    -        url,
    -        data=body,
    -        method="POST",
    -        headers={
    -            "Content-Type": "application/x-www-form-urlencoded",
    -            "Accept": "application/json",
    -        },
    -    )
    -    try:
    -        with urllib.request.urlopen(request, timeout=timeout) as response:
    -            raw = response.read().decode("utf-8", errors="replace")
    -            return json.loads(raw)
    -    except urllib.error.HTTPError as exc:
    -        detail = ""
    -        try:
    -            detail = exc.read().decode("utf-8", errors="replace")
    -        except Exception:
    -            pass
    -        # Detect invalid_grant to signal credential revocation
    -        code = "google_oauth_token_http_error"
    -        if "invalid_grant" in detail.lower():
    -            code = "google_oauth_invalid_grant"
    -        raise GoogleOAuthError(
    -            f"Google OAuth token endpoint returned HTTP {exc.code}: {detail or exc.reason}",
    -            code=code,
    -        ) from exc
    -    except urllib.error.URLError as exc:
    -        raise GoogleOAuthError(
    -            f"Google OAuth token request failed: {exc}",
    -            code="google_oauth_token_network_error",
    -        ) from exc
    -
    -
    -def exchange_code(
    -    code: str,
    -    verifier: str,
    -    redirect_uri: str,
    -    *,
    -    client_id: Optional[str] = None,
    -    client_secret: [redacted:secret-assignment] = None,
    -    timeout: float = TOKEN_REQUEST_TIMEOUT_SECONDS,
    -) -> Dict[str, Any]:
    -    """Exchange authorization code for access + refresh tokens."""
    -    cid = client_id if client_id is not None else _get_client_id()
    -    csecret = [redacted:secret-assignment] if client_secret is not None else _get_client_secret()
    -    data = {
    -        "grant_type": "authorization_code",
    -        "code": code,
    -        "code_verifier": verifier,
    -        "client_id": cid,
    -        "redirect_uri": redirect_uri,
    -    }
    -    if csecret:
    -        data["client_secret"] = csecret
    -    return _post_form(TOKEN_ENDPOINT, data, timeout)
    -
    -
    -def refresh_access_token(
    -    refresh_token: str,
    -    *,
    -    client_id: Optional[str] = None,
    -    client_secret: [redacted:secret-assignment] = None,
    -    timeout: float = TOKEN_REQUEST_TIMEOUT_SECONDS,
    -) -> Dict[str, Any]:
    -    """Refresh the access token."""
    -    if not refresh_token:
    -        raise GoogleOAuthError(
    -            "Cannot refresh: refresh_token is empty. Re-run OAuth login.",
    -            code="google_oauth_refresh_token_missing",
    -        )
    -    cid = client_id if client_id is not None else _get_client_id()
    -    csecret = [redacted:secret-assignment] if client_secret is not None else _get_client_secret()
    -    data = {
    -        "grant_type": "refresh_token",
    -        "refresh_token": refresh_token,
    -        "client_id": cid,
    -    }
    -    if csecret:
    -        data["client_secret"] = csecret
    -    return _post_form(TOKEN_ENDPOINT, data, timeout)
    -
    -
    -def _fetch_user_email(access_token: str, timeout: float = TOKEN_REQUEST_TIMEOUT_SECONDS) -> str:
    -    """Best-effort userinfo fetch for display. Failures return empty string."""
    -    try:
    -        request = urllib.request.Request(
    -            USERINFO_ENDPOINT + "?alt=json",
    -            headers={"Authorization": f"Bearer {access_token}"},
    -        )
    -        with urllib.request.urlopen(request, timeout=timeout) as response:
    -            raw = response.read().decode("utf-8", errors="replace")
    -        data = json.loads(raw)
    -        return str(data.get("email", "") or "")
    -    except Exception as exc:
    -        logger.debug("Userinfo fetch failed (non-fatal): %s", exc)
    -        return ""
    -
    -
    -# =============================================================================
    -# In-flight refresh deduplication
    -# =============================================================================
    -
    -_refresh_inflight: Dict[str, threading.Event] = {}
    -_refresh_inflight_lock = threading.Lock()
    -
    -
    -def get_valid_access_token(*, force_refresh: bool = False) -> str:
    -    """Load creds, refreshing if near expiry, and return a valid bearer token.
    -
    -    Dedupes concurrent refreshes by refresh_token. On ``invalid_grant``, the
    -    credential file is wiped and a ``google_oauth_invalid_grant`` error is raised
    -    (caller is expected to trigger a re-login flow).
    -    """
    -    creds = load_credentials()
    -    if creds is None:
    -        raise GoogleOAuthError(
    -            "No Google OAuth credentials found. Run `hermes auth add google-gemini-cli` first.",
    -            code="google_oauth_not_logged_in",
    -        )
    -
    -    if not force_refresh and not creds.access_token_expired():
    -        return creds.access_token
    -
    -    # Dedupe concurrent refreshes by refresh_token
    -    rt = creds.refresh_token
    -    with _refresh_inflight_lock:
    -        event = _refresh_inflight.get(rt)
    -        if event is None:
    -            event = threading.Event()
    -            _refresh_inflight[rt] = event
    -            owner = True
    -        else:
    -            owner = False
    -
    -    if not owner:
    -        # Another thread is refreshing — wait, then re-read from disk.
    -        event.wait(timeout=LOCK_TIMEOUT_SECONDS)
    -        fresh = load_credentials()
    -        if fresh is not None and not fresh.access_token_expired():
    -            return fresh.access_token
    -        # Fall through to do our own refresh if the other attempt failed
    -
    -    try:
    -        try:
    -            resp = refresh_access_token(rt)
    -        except GoogleOAuthError as exc:
    -            if exc.code == "google_oauth_invalid_grant":
    -                logger.warning(
    -                    "Google OAuth refresh token invalid (revoked/expired). "
    -                    "Clearing credentials at %s — user must re-login.",
    -                    _credentials_path(),
    -                )
    -                clear_credentials()
    -            raise
    -
    -        new_access = str(resp.get("access_token", "") or "").strip()
    -        if not new_access:
    -            raise GoogleOAuthError(
    -                "Refresh response did not include an access_token.",
    -                code="google_oauth_refresh_empty",
    -            )
    -        # Google sometimes rotates refresh_token; preserve existing if omitted.
    -        new_refresh = str(resp.get("refresh_token", "") or "").strip() or creds.refresh_token
    -        expires_in = int(resp.get("expires_in", 0) or 0)
    -
    -        creds.access_token = [redacted:secret-assignment]
    -        creds.refresh_token = [redacted:secret-assignment]
    -        creds.expires_ms = int((time.time() + max(60, expires_in)) * 1000)
    -        save_credentials(creds)
    -        return creds.access_token
    -    finally:
    -        if owner:
    -            with _refresh_inflight_lock:
    -                _refresh_inflight.pop(rt, None)
    -            event.set()
    -
    -
    -# =============================================================================
    -# Update project IDs on stored creds
    -# =============================================================================
    -
    -def update_project_ids(project_id: str = "", managed_project_id: str = "") -> None:
    -    """Persist resolved/discovered project IDs back into the credential file."""
    -    creds = load_credentials()
    -    if creds is None:
    -        return
    -    if project_id:
    -        creds.project_id = project_id
    -    if managed_project_id:
    -        creds.managed_project_id = managed_project_id
    -    save_credentials(creds)
    -
    -
    -# =============================================================================
    -# Callback server
    -# =============================================================================
    -
    -class _OAuthCallbackHandler(http.server.BaseHTTPRequestHandler):
    -    expected_state: str = ""
    -    captured_code: Optional[str] = None
    -    captured_error: Optional[str] = None
    -    ready: Optional[threading.Event] = None
    -
    -    def log_message(self, format: str, *args: Any) -> None:  # noqa: A002, N802
    -        logger.debug("OAuth callback: " + format, *args)
    -
    -    def do_GET(self) -> None:  # noqa: N802
    -        parsed = urllib.parse.urlparse(self.path)
    -        if parsed.path != CALLBACK_PATH:
    -            self.send_response(404)
    -            self.end_headers()
    -            return
    -
    -        params = urllib.parse.parse_qs(parsed.query)
    -        state = (params.get("state") or [""])[0]
    -        error = (params.get("error") or [""])[0]
    -        code = (params.get("code") or [""])[0]
    -
    -        if state != type(self).expected_state:
    -            type(self).captured_error = "state_mismatch"
    -            self._respond_html(400, _ERROR_PAGE.format(message="State mismatch — aborting for safety."))
    -        elif error:
    -            type(self).captured_error = error
    -            # Simple HTML-escape of the error value
    -            safe_err = (
    -                str(error)
    -                .replace("&", "&amp;")
    -                .replace("<", "&lt;")
    -                .replace(">", "&gt;")
    -            )
    -            self._respond_html(400, _ERROR_PAGE.format(message=f"Authorization denied: {safe_err}"))
    -        elif code:
    -            type(self).captured_code = code
    -            self._respond_html(200, _SUCCESS_PAGE)
    -        else:
    -            type(self).captured_error = "no_code"
    -            self._respond_html(400, _ERROR_PAGE.format(message="Callback received no authorization code."))
    -
    -        if type(self).ready is not None:
    -            type(self).ready.set()
    -
    -    def _respond_html(self, status: int, body: str) -> None:
    -        payload = body.encode("utf-8")
    -        self.send_response(status)
    -        self.send_header("Content-Type", "text/html; charset=utf-8")
    -        self.send_header("Content-Length", str(len(payload)))
    -        self.end_headers()
    -        self.wfile.write(payload)
    -
    -
    -_SUCCESS_PAGE = """<!doctype html>
    -<html><head><meta charset="utf-8"><title>Hermes — signed in</title>
    -<style>
    -body { font: 16px/1.5 system-ui, sans-serif; margin: 10vh auto; max-width: 32rem; text-align: center; color: #222; }
    -h1 { color: #1a7f37; } p { color: #555; }
    -</style></head>
    -<body><h1>Signed in to Google.</h1>
    -<p>You can close this tab and return to your terminal.</p></body></html>
    -"""
    -
    -_ERROR_PAGE = """<!doctype html>
    -<html><head><meta charset="utf-8"><title>Hermes — sign-in failed</title>
    -<style>
    -body {{ font: 16px/1.5 system-ui, sans-serif; margin: 10vh auto; max-width: 32rem; text-align: center; color: #222; }}
    -h1 {{ color: #b42318; }} p {{ color: #555; }}
    -</style></head>
    -<body><h1>Sign-in failed</h1><p>{message}</p>
    -<p>Return to your terminal — Hermes will walk you through a manual paste fallback.</p></body></html>
    -"""
    -
    -
    -def _bind_callback_server(preferred_port: int = DEFAULT_REDIRECT_PORT) -> Tuple[http.server.HTTPServer, int]:
    -    try:
    -        server = http.server.HTTPServer((REDIRECT_HOST, preferred_port), _OAuthCallbackHandler)
    -        return server, preferred_port
    -    except OSError as exc:
    -        logger.info(
    -            "Preferred OAuth callback port %d unavailable (%s); requesting ephemeral port",
    -            preferred_port, exc,
    -        )
    -    server = http.server.HTTPServer((REDIRECT_HOST, 0), _OAuthCallbackHandler)
    -    return server, server.server_address[1]
    -
    -
    -def _is_headless() -> bool:
    -    return any(os.getenv(k) for k in _HEADLESS_ENV_VARS)
    -
    -
    -# =============================================================================
    -# Main login flow
    -# =============================================================================
    -
    -def start_oauth_flow(
    -    *,
    -    force_relogin: bool = False,
    -    open_browser: bool = True,
    -    callback_wait_seconds: float = CALLBACK_WAIT_SECONDS,
    -    project_id: str = "",
    -) -> GoogleCredentials:
    -    """Run the interactive browser OAuth flow and persist credentials.
    -
    -    Args:
    -        force_relogin: If False and valid creds already exist, return them.
    -        open_browser: If False, skip webbrowser.open and print the URL only.
    -        callback_wait_seconds: Max seconds to wait for the browser callback.
    -        project_id: Initial GCP project ID to bake into the stored creds.
    -                    Can be discovered/updated later via update_project_ids().
    -    """
    -    if not force_relogin:
    -        existing = load_credentials()
    -        if existing and existing.access_token:
    -            logger.info("Google OAuth credentials already present; skipping login.")
    -            return existing
    -
    -    client_id = _require_client_id()  # raises GoogleOAuthError with install hints
    -    client_secret = [redacted:secret-assignment]
    -
    -    verifier, challenge = _generate_pkce_pair()
    -    state = secrets.token_urlsafe(16)
    -
    -    # If headless, skip the listener and go straight to paste mode
    -    if _is_headless() and open_browser:
    -        logger.info("Headless environment detected; using paste-mode OAuth fallback.")
    -        return _paste_mode_login(verifier, challenge, state, client_id, client_secret, project_id)
    -
    -    server, port = _bind_callback_server(DEFAULT_REDIRECT_PORT)
    -    redirect_uri = f"http://{REDIRECT_HOST}:{port}{CALLBACK_PATH}"
    -
    -    _OAuthCallbackHandler.expected_state = state
    -    _OAuthCallbackHandler.captured_code = None
    -    _OAuthCallbackHandler.captured_error = None
    -    ready = threading.Event()
    -    _OAuthCallbackHandler.ready = ready
    -
    -    params = {
    -        "client_id": client_id,
    -        "redirect_uri": redirect_uri,
    -        "response_type": "code",
    -        "scope": OAUTH_SCOPES,
    -        "state": state,
    -        "code_challenge": challenge,
    -        "code_challenge_method": "S256",
    -        "access_type": "offline",
    -        "prompt": "consent",
    -    }
    -    auth_url = AUTH_ENDPOINT + "?" + urllib.parse.urlencode(params) + "#hermes"
    -
    -    server_thread = threading.Thread(target=server.serve_forever, daemon=True)
    -    server_thread.start()
    -
    -    print()
    -    print("Opening your browser to sign in to Google…")
    -    print(f"If it does not open automatically, visit:\n  {auth_url}")
    -    print()
    -
    -    if open_browser:
    -        try:
    -            import webbrowser
    -
    -            try:
    -                from hermes_cli.auth import (
    -                    _can_open_graphical_browser as _can_open_gui,
    -                )
    -            except Exception:
    -                _can_open_gui = lambda: True  # noqa: E731
    -
    -            if _can_open_gui():
    -                webbrowser.open(auth_url, new=1, autoraise=True)
    -        except Exception as exc:
    -            logger.debug("webbrowser.open failed: %s", exc)
    -
    -    code: Optional[str] = None
    -    try:
    -        if ready.wait(timeout=callback_wait_seconds):
    -            code = _OAuthCallbackHandler.captured_code
    -            error = _OAuthCallbackHandler.captured_error
    -            if error:
    -                raise GoogleOAuthError(
    -                    f"Authorization failed: {error}",
    -                    code="google_oauth_authorization_failed",
    -                )
    -        else:
    -            logger.info("Callback server timed out — offering manual paste fallback.")
    -            code = _prompt_paste_fallback()
    -    finally:
    -        try:
    -            server.shutdown()
    -        except Exception:
    -            pass
    -        try:
    -            server.server_close()
    -        except Exception:
    -            pass
    -        server_thread.join(timeout=2.0)
    -
    -    if not code:
    -        raise GoogleOAuthError(
    -            "No authorization code received. Aborting.",
    -            code="google_oauth_no_code",
    -        )
    -
    -    token_resp = [redacted:secret-assignment]
    -        code, verifier, redirect_uri,
    -        client_id=client_id, client_secret=[redacted:secret-assignment]
    -    )
    -    return _persist_token_response(token_resp, project_id=project_id)
    -
    -
    -def _paste_mode_login(
    -    verifier: str,
    -    challenge: str,
    -    state: str,
    -    client_id: str,
    -    client_secret: str,
    -    project_id: str,
    -) -> GoogleCredentials:
    -    """Run OAuth flow without a local callback server."""
    -    # Use a placeholder redirect URI; user will paste the full URL back
    -    redirect_uri = f"http://{REDIRECT_HOST}:{DEFAULT_REDIRECT_PORT}{CALLBACK_PATH}"
    -    params = {
    -        "client_id": client_id,
    -        "redirect_uri": redirect_uri,
    -        "response_type": "code",
    -        "scope": OAUTH_SCOPES,
    -        "state": state,
    -        "code_challenge": challenge,
    -        "code_challenge_method": "S256",
    -        "access_type": "offline",
    -        "prompt": "consent",
    -    }
    -    auth_url = AUTH_ENDPOINT + "?" + urllib.parse.urlencode(params) + "#hermes"
    -
    -    print()
    -    print("Open this URL in a browser on any device:")
    -    print(f"  {auth_url}")
    -    print()
    -    print("After signing in, Google will redirect to localhost (which won't load).")
    -    print("Copy the full URL from your browser and paste it below.")
    -    print()
    -
    -    code = _prompt_paste_fallback()
    -    if not code:
    -        raise GoogleOAuthError("No authorization code provided.", code="google_oauth_no_code")
    -
    -    token_resp = [redacted:secret-assignment]
    -        code, verifier, redirect_uri,
    -        client_id=client_id, client_secret=[redacted:secret-assignment]
    -    )
    -    return _persist_token_response(token_resp, project_id=project_id)
    -
    -
    -def _prompt_paste_fallback() -> Optional[str]:
    -    print()
    -    print("Paste the full redirect URL Google showed you, OR just the 'code=' parameter value.")
    -    raw = input("Callback URL or code: ").strip()
    -    if not raw:
    -        return None
    -    if raw.startswith("http://") or raw.startswith("https://"):
    -        parsed = urllib.parse.urlparse(raw)
    -        params = urllib.parse.parse_qs(parsed.query)
    -        return (params.get("code") or [""])[0] or None
    -    # Accept a bare query string as well
    -    if raw.startswith("?"):
    -        params = urllib.parse.parse_qs(raw[1:])
    -        return (params.get("code") or [""])[0] or None
    -    return raw
    -
    -
    -def _persist_token_response(
    -    token_resp: [redacted:secret-assignment] Any],
    -    *,
    -    project_id: str = "",
    -) -> GoogleCredentials:
    -    access_token = [redacted:secret-assignment]"access_token", "") or "").strip()
    -    refresh_token = [redacted:secret-assignment]"refresh_token", "") or "").strip()
    -    expires_in = int(token_resp.get("expires_in", 0) or 0)
    -    if not access_token or not refresh_token:
    -        raise GoogleOAuthError(
    -            "Google token response missing access_token or refresh_token.",
    -            code="google_oauth_incomplete_token_response",
    -        )
    -    creds = GoogleCredentials(
    -        access_token=[redacted:secret-assignment]
    -        refresh_token=[redacted:secret-assignment]
    -        expires_ms=int((time.time() + max(60, expires_in)) * 1000),
    -        email=_fetch_user_email(access_token),
    -        project_id=project_id,
    -        managed_project_id="",
    -    )
    -    save_credentials(creds)
    -    logger.info("Google OAuth credentials saved to %s", _credentials_path())
    -    return creds
    -
    -
    -# =============================================================================
    -# Pool-compatible variant
    -# =============================================================================
    -
    -def run_gemini_oauth_login_pure() -> Dict[str, Any]:
    -    """Run the login flow and return a dict matching the credential pool shape."""
    -    creds = start_oauth_flow(force_relogin=True)
    -    return {
    -        "access_token": creds.access_token,
    -        "refresh_token": creds.refresh_token,
    -        "expires_at_ms": creds.expires_ms,
    -        "email": creds.email,
    -        "project_id": creds.project_id,
    -    }
    -
    -
    -# =============================================================================
    -# Project ID resolution
    -# =============================================================================
    -
    -def resolve_project_id_from_env() -> str:
    -    """Return a GCP project ID from env vars, in priority order."""
    -    for var in (
    -        "HERMES_GEMINI_PROJECT_ID",
    -        "GOOGLE_CLOUD_PROJECT",
    -        "GOOGLE_CLOUD_PROJECT_ID",
    -    ):
    -        val = (os.getenv(var) or "").strip()
    -        if val:
    -            return val
    -    return ""
    ```
  - `agent/transports/chat_completions.py`

    ```diff
    @@ -437,10 +437,6 @@ def build_kwargs(
                         extra_body["extra_body"] = openai_compat_extra
                 elif raw_thinking_config:
                     extra_body["thinking_config"] = raw_thinking_config
    -        elif provider_name in {"google-gemini-cli", "google-antigravity"}:
    -            thinking_config = _build_gemini_thinking_config(model, reasoning_config)
    -            if thinking_config:
    -                extra_body["thinking_config"] = thinking_config
     
             # Merge any pre-built extra_body additions
             additions = params.get("extra_body_additions")
    ```
  - `apps/desktop/src/app/settings/constants.ts`

    ```diff
    @@ -74,7 +74,6 @@ export const PROVIDER_GROUPS: ProviderPrefix[] = [
         priority: 4
       },
       { prefix: 'GEMINI_', name: 'Gemini', priority: 4 },
    -  { prefix: 'HERMES_GEMINI_', name: 'Gemini', priority: 4 },
       {
         prefix: 'DEEPSEEK_',
         name: 'DeepSeek',
    ```
  - `apps/desktop/src/app/settings/helpers.test.ts`

    ```diff
    @@ -132,9 +132,9 @@ describe('settings helpers', () => {
           // KIMI_CN_ likewise must beat KIMI_.
           expect(providerGroup('KIMI_CN_API_KEY')).toBe('Kimi (China)')
           expect(providerGroup('KIMI_API_KEY')).toBe('Kimi / Moonshot')
    -      // HERMES_QWEN_ and HERMES_GEMINI_ both share the HERMES_ stem.
    +      // HERMES_QWEN_ shares the HERMES_ stem with other integrations.
           expect(providerGroup('HERMES_QWEN_BASE_URL')).toBe('DashScope (Qwen)')
    -      expect(providerGroup('HERMES_GEMINI_CLIENT_ID')).toBe('Gemini')
    +      expect(providerGroup('GEMINI_API_KEY')).toBe('Gemini')
         })
     
         it('falls back to "Other" for un-grouped env vars', () => {
    ```
  - `apps/desktop/src/lib/desktop-slash-commands.ts`

    ```diff
    @@ -150,7 +150,7 @@ const DESKTOP_COMMAND_SPECS: readonly DesktopCommandSpec[] = [
     const NO_DESKTOP_SURFACE: Record<DesktopUnavailableReason, readonly string[]> = {
       terminal: [
         '/busy', '/clear', '/compact', '/config', '/copy', '/cron', '/details',
    -    '/exit', '/footer', '/gateway', '/gquota', '/history', '/image', '/indicator', '/logs',
    +    '/exit', '/footer', '/gateway', '/history', '/image', '/indicator', '/logs',
         '/mouse', '/paste', '/platforms', '/plugins', '/quit', '/redraw', '/reload', '/restart',
         '/sb', '/set-home', '/sethome', '/snap', '/snapshot', '/statusbar', '/toolsets', '/update', '/verbose'
       ],
    ```
  - `cli.py`

    ```diff
    @@ -7837,8 +7837,6 @@ def process_command(self, command: str) -> bool:
                 self._handle_model_switch(cmd_original)
             elif canonical == "codex-runtime":
                 self._handle_codex_runtime(cmd_original)
    -        elif canonical == "gquota":
    -            self._handle_gquota_command(cmd_original)
     
             elif canonical == "personality":
                 # Use original case (handler lowercases the personality name itself)
    ```
  - `hermes_cli/auth.py`

    ```diff
    @@ -138,13 +138,6 @@
         "spotify": "Spotify",
     }
     
    -# Google Gemini OAuth (google-gemini-cli provider, Cloud Code Assist backend)
    -DEFAULT_GEMINI_CLOUDCODE_BASE_URL = "cloudcode-pa://google"
    -GEMINI_OAUTH_ACCESS_TOKEN_REFRESH_SKEW_SECONDS = 60  # refresh 60s before expiry
    -
    -# Google Antigravity OAuth (Antigravity Code Assist backend)
    -DEFAULT_ANTIGRAVITY_CLOUDCODE_BASE_URL = "antigravity-pa://google"
    -
     # LM Studio's default no-auth mode still requires *some* non-empty bearer for
     # the API-key code paths (auxiliary_client, runtime resolver) to treat the
     # provider as configured. This sentinel is sent only to LM Studio, never to
    ```

    ```diff
    @@ -209,18 +202,6 @@ class ProviderConfig:
             auth_type="oauth_external",
             inference_base_url=DEFAULT_QWEN_BASE_URL,
         ),
    -    "google-gemini-cli": ProviderConfig(
    -        id="google-gemini-cli",
    -        name="Google Gemini (OAuth)",
    -        auth_type="oauth_external",
    -        inference_base_url=DEFAULT_GEMINI_CLOUDCODE_BASE_URL,
    -    ),
    -    "google-antigravity": ProviderConfig(
    -        id="google-antigravity",
    -        name="Google Antigravity (OAuth)",
    -        auth_type="oauth_external",
    -        inference_base_url=DEFAULT_ANTIGRAVITY_CLOUDCODE_BASE_URL,
    -    ),
         "lmstudio": ProviderConfig(
             id="lmstudio",
             name="LM Studio",
    ```

    ```diff
    @@ -1538,8 +1519,7 @@ def resolve_provider(
             "github-models": "copilot", "github-model": "copilot",
             "github-copilot-acp": "copilot-acp", "copilot-acp-agent": "copilot-acp",
             "opencode": "opencode-zen", "zen": "opencode-zen",
    -        "qwen-portal": "qwen-oauth", "qwen-cli": "qwen-oauth", "qwen-oauth": "qwen-oauth", "google-gemini-cli": "google-gemini-cli", "gemini-cli": "google-gemini-cli", "gemini-oauth": "google-gemini-cli",
    -        "google-antigravity": "google-antigravity", "google-antigravity-oauth": "google-antigravity", "antigravity": "google-antigravity", "antigravity-oauth": "google-antigravity", "antigravity-cli": "google-antigravity", "agy": "google-antigravity", "agy-cli": "google-antigravity",
    +        "qwen-portal": "qwen-oauth", "qwen-cli": "qwen-oauth", "qwen-oauth": "qwen-oauth",
             "hf": "huggingface", "hugging-face": "huggingface", "huggingface-hub": "huggingface",
             "mimo": "xiaomi", "xiaomi-mimo": "xiaomi",
             "tencent": "tencent-tokenhub", "tokenhub": "tencent-tokenhub",
    ```

    ```diff
    @@ -2165,163 +2145,6 @@ def get_qwen_auth_status() -> Dict[str, Any]:
     
     
     # =============================================================================
    -# Google Gemini OAuth (google-gemini-cli) — PKCE flow + Cloud Code Assist.
    -#
    -# Tokens live in ~/.hermes/auth/google_oauth.json (managed by agent.google_oauth).
    -# The `base_url` here is the marker "cloudcode-pa://google" that run_agent.py
    -# uses to construct a GeminiCloudCodeClient instead of the default OpenAI SDK.
    -# Actual HTTP traffic goes to https://cloudcode-pa.googleapis.com/v1internal:*.
    -# =============================================================================
    -
    -def _mark_google_gemini_cli_active(creds: Dict[str, Any]) -> None:
    -    """Set active_provider to google-gemini-cli in auth.json.
    -
    -    The actual OAuth tokens live in the Google credential file managed by
    -    agent.google_oauth. This function only writes a minimal provider-state
    -    entry (email for display) and sets active_provider so that
    -    get_active_provider() and _model_section_has_credentials() detect the
    -    provider for the setup wizard and status commands.
    -    """
    -    with _auth_store_lock():
    -        auth_store = _load_auth_store()
    -        state: Dict[str, Any] = {}
    -        if creds.get("email"):
    -            state["email"] = str(creds["email"])
    -        _save_provider_state(auth_store, "google-gemini-cli", state)
    -        _save_auth_store(auth_store)
    -
    -
    -def resolve_gemini_oauth_runtime_credentials(
    -    *,
    -    force_refresh: bool = False,
    -) -> Dict[str, Any]:
    -    """Resolve runtime OAuth creds for google-gemini-cli."""
    -    try:
    -        from agent.google_oauth import (
    -            GoogleOAuthError,
    -            _credentials_path,
    -            get_valid_access_token,
    -            load_credentials,
    -        )
    -    except ImportError as exc:
    -        raise AuthError(
    -            f"agent.google_oauth is not importable: {exc}",
    -            provider="google-gemini-cli",
    -            code="google_oauth_module_missing",
    -        ) from exc
    -
    -    try:
    -        access_token = [redacted:secret-assignment]
    -    except GoogleOAuthError as exc:
    -        raise AuthError(
    -            str(exc),
    -            provider="google-gemini-cli",
    -            code=exc.code,
    -        ) from exc
    -
    -    creds = load_credentials()
    -    base_url = DEFAULT_GEMINI_CLOUDCODE_BASE_URL
    -    return {
    -        "provider": "google-gemini-cli",
    -        "base_url": base_url,
    -        "api_key": access_token,
    -        "source": "google-oauth",
    -        "expires_at_ms": (creds.expires_ms if creds else None),
    -        "auth_file": str(_credentials_path()),
    -        "email": (creds.email if creds else "") or "",
    -        "project_id": (creds.project_id if creds else "") or "",
    -    }
    -
    -
    -def get_gemini_oauth_auth_status() -> Dict[str, Any]:
    -    """Return a status dict for `hermes auth list` / `hermes status`."""
    -    try:
    -        from agent.google_oauth import _credentials_path, load_credentials
    -    except ImportError:
    -        return {"logged_in": False, "error": "agent.google_oauth unavailable"}
    -    auth_path = _credentials_path()
    -    creds = load_credentials()
    -    if creds is None or not creds.access_token:
    -        return {
    -            "logged_in": False,
    -            "auth_file": str(auth_path),
    -            "error": "not logged in",
    -        }
    -    return {
    -        "logged_in": True,
    -        "auth_file": str(auth_path),
    -        "source": "google-oauth",
    -        "api_key": creds.access_token,
    -        "expires_at_ms": creds.expires_ms,
    -        "email": creds.email,
    -        "project_id": creds.project_id,
    -    }
    -
    -
    -def resolve_antigravity_oauth_runtime_credentials(
    -    *,
    -    force_refresh: bool = False,
    -) -> Dict[str, Any]:
    -    """Resolve runtime OAuth creds for google-antigravity."""
    -    try:
    -        from agent.antigravity_oauth import (
    -            AntigravityOAuthError,
    -            _credentials_path,
    -            get_valid_access_token,
    -            load_credentials,
    -        )
    -    except ImportError as exc:
    -        raise AuthError(
    -            f"agent.antigravity_oauth is not importable: {exc}",
    -            provider="google-antigravity",
    -            code="antigravity_oauth_module_missing",
    -        ) from exc
    -
    -    try:
    -        access_token = [redacted:secret-assignment]
    -    except AntigravityOAuthError as exc:
    -        raise AuthError(
    -            str(exc),
    -            provider="google-antigravity",
    -            code=exc.code,
    -        ) from exc
    -
    -    creds = load_credentials()
    -    return {
    -        "provider": "google-antigravity",
    -        "base_url": DEFAULT_ANTIGRAVITY_CLOUDCODE_BASE_URL,
    -        "api_key": access_token,
    -        "source": "antigravity-oauth",
    -        "expires_at_ms": (creds.expires_ms if creds else None),
    -        "auth_file": str(_credentials_path()),
    -        "email": (creds.email if creds else "") or "",
    -        "project_id": (creds.project_id if creds else "") or "",
    -    }
    -
    -
    -def get_antigravity_oauth_auth_status() -> Dict[str, Any]:
    -    """Return a status dict for `hermes auth list` / `hermes status`."""
    -    try:
    -        from agent.antigravity_oauth import _credentials_path, load_credentials
    -    except ImportError:
    -        return {"logged_in": False, "error": "agent.antigravity_oauth unavailable"}
    -    auth_path = _credentials_path()
    -    creds = load_credentials()
    -    if creds is None or not creds.access_token:
    -        return {
    -            "logged_in": False,
    -            "auth_file": str(auth_path),
    -            "error": "not logged in",
    -        }
    -    return {
    -        "logged_in": True,
    -        "auth_file": str(auth_path),
    -        "source": "antigravity-oauth",
    -        "api_key": creds.access_token,
    -        "expires_at_ms": creds.expires_ms,
    -        "email": creds.email,
    -        "project_id": creds.project_id,
    -    }
     # Spotify auth — PKCE tokens stored in ~/.hermes/auth.json
     # =============================================================================
     
    ```

    ```diff
    @@ -6265,10 +6088,6 @@ def get_auth_status(provider_id: Optional[str] = None) -> Dict[str, Any]:
             return get_xai_oauth_auth_status()
         if target == "qwen-oauth":
             return get_qwen_auth_status()
    -    if target == "google-gemini-cli":
    -        return get_gemini_oauth_auth_status()
    -    if target == "google-antigravity":
    -        return get_antigravity_oauth_auth_status()
         if target == "minimax-oauth":
             return get_minimax_oauth_auth_status()
         if target == "copilot-acp":
    ```
  - `hermes_cli/auth_commands.py`

    ```diff
    @@ -34,7 +34,7 @@
     
     
     # Providers that support OAuth login in addition to API keys.
    -_OAUTH_CAPABLE_PROVIDERS = {"anthropic", "nous", "openai-codex", "xai-oauth", "qwen-oauth", "google-gemini-cli", "google-antigravity", "minimax-oauth"}
    +_OAUTH_CAPABLE_PROVIDERS = {"anthropic", "nous", "openai-codex", "xai-oauth", "qwen-oauth", "minimax-oauth"}
     
     
     def _get_custom_provider_names() -> list:
    ```

    ```diff
    @@ -314,7 +314,7 @@ def auth_add_command(args) -> None:
                 _oauth_default_label(provider, len(pool.entries()) + 1),
             )
             # Add a distinct, self-contained pool entry per account (matching the
    -        # xai-oauth / google-gemini-cli / qwen-oauth patterns) instead of
    +        # xai-oauth / qwen-oauth patterns) instead of
             # routing through the singleton ``_save_codex_tokens`` save path.
             # The singleton round-trip collapsed every added account into the
             # latest login: a second ``hermes auth add openai-codex`` overwrote
    ```

    ```diff
    @@ -364,49 +364,6 @@ def auth_add_command(args) -> None:
             print(f'Saved {provider} OAuth credentials: "[redacted:secret-assignment]"')
             return
     
    -    if provider == "google-gemini-cli":
    -        from agent.google_oauth import run_gemini_oauth_login_pure
    -
    -        creds = run_gemini_oauth_login_pure()
    -        auth_mod._mark_google_gemini_cli_active(creds)
    -        label = (getattr(args, "label", None) or "").strip() or (
    -            creds.get("email") or _oauth_default_label(provider, len(pool.entries()) + 1)
    -        )
    -        entry = PooledCredential(
    -            provider=provider,
    -            id=uuid.uuid4().hex[:6],
    -            label=label,
    -            auth_type=AUTH_TYPE_OAUTH,
    -            priority=0,
    -            source=f"{SOURCE_MANUAL}:google_pkce",
    -            access_token=[redacted:secret-assignment]"access_token"],
    -            refresh_token=[redacted:secret-assignment]"refresh_token"),
    -        )
    -        pool.add_entry(entry)
    -        print(f'Added {provider} OAuth credential #{len(pool.entries())}: "{entry.label}"')
    -        return
    -
    -    if provider == "google-antigravity":
    -        from agent.antigravity_oauth import run_antigravity_oauth_login_pure
    -
    -        creds = run_antigravity_oauth_login_pure()
    -        label = (getattr(args, "label", None) or "").strip() or (
    -            creds.get("email") or _oauth_default_label(provider, len(pool.entries()) + 1)
    -        )
    -        entry = PooledCredential(
    -            provider=provider,
    -            id=uuid.uuid4().hex[:6],
    -            label=label,
    -            auth_type=AUTH_TYPE_OAUTH,
    -            priority=0,
    -            source=f"{SOURCE_MANUAL}:antigravity_pkce",
    -            access_token=[redacted:secret-assignment]"access_token"],
    -            refresh_token=[redacted:secret-assignment]"refresh_token"),
    -        )
    -        pool.add_entry(entry)
    -        print(f'Added {provider} OAuth credential #{len(pool.entries())}: "{entry.label}"')
    -        return
    -
         if provider == "qwen-oauth":
             creds = auth_mod.resolve_qwen_runtime_credentials(refresh_if_expiring=False)
             auth_mod._mark_qwen_oauth_active(creds)
    ```
  - `hermes_cli/cli_commands_mixin.py`

    ```diff
    @@ -947,52 +947,6 @@ def _handle_branch_command(self, cmd_original: str) -> None:
             _cprint(f"  Original session: {parent_session_id}")
             _cprint(f"  Branch session:   {new_session_id}")
     
    -    def _handle_gquota_command(self, cmd_original: str) -> None:
    -        """Show Google Gemini Code Assist quota usage for the current OAuth account."""
    -        try:
    -            from agent.google_oauth import get_valid_access_token, GoogleOAuthError, load_credentials
    -            from agent.google_code_assist import retrieve_user_quota, CodeAssistError
    -        except ImportError as exc:
    -            self._console_print(f"  [red]Gemini modules unavailable: {exc}[/]")
    -            return
    -
    -        try:
    -            access_token = [redacted:secret-assignment]
    -        except GoogleOAuthError as exc:
    -            self._console_print(f"  [yellow]{exc}[/]")
    -            self._console_print("  Run [bold]/model[/] and pick 'Google Gemini (OAuth)' to sign in.")
    -            return
    -
    -        creds = load_credentials()
    -        project_id = (creds.project_id if creds else "") or ""
    -
    -        try:
    -            buckets = retrieve_user_quota(access_token, project_id=project_id)
    -        except CodeAssistError as exc:
    -            self._console_print(f"  [red]Quota lookup failed:[/] {exc}")
    -            return
    -
    -        if not buckets:
    -            self._console_print("  [dim]No quota buckets reported (account may be on legacy/unmetered tier).[/]")
    -            return
    -
    -        # Sort for stable display, group by model
    -        buckets.sort(key=lambda b: (b.model_id, b.token_type))
    -        self._console_print()
    -        self._console_print(f"  [bold]Gemini Code Assist quota[/]  (project: {project_id or '(auto / free-tier)'})")
    -        self._console_print()
    -        for b in buckets:
    -            pct = max(0.0, min(1.0, b.remaining_fraction))
    -            width = 20
    -            filled = int(round(pct * width))
    -            bar = "▓" * filled + "░" * (width - filled)
    -            pct_str = f"{int(pct * 100):3d}%"
    -            header = b.model_id
    -            if b.token_type:
    -                header += f" [{b.token_type}]"
    -            self._console_print(f"    {header:40s}  {bar}  {pct_str}")
    -        self._console_print()
    -
         def _handle_personality_command(self, cmd: str):
             """Handle the /personality command to set predefined personalities."""
             from cli import save_config_value
    ```
  - `hermes_cli/commands.py`

    ```diff
    @@ -128,8 +128,6 @@ class CommandDef:
         CommandDef("codex-runtime", "Toggle codex app-server runtime for OpenAI/Codex models",
                    "Configuration", aliases=("codex_runtime",),
                    args_hint="[auto|codex_app_server]"),
    -    CommandDef("gquota", "Show Google Gemini Code Assist quota usage", "Info",
    -               cli_only=True),
     
         CommandDef("personality", "Set a predefined personality", "Configuration",
                    args_hint="[name]"),
    ```
  - `hermes_cli/config.py`

    ```diff
    @@ -169,8 +169,8 @@ def _warn_config_parse_failure(config_path: Path, exc: Exception) -> None:
     #   the dashboard. ``config.yaml`` is the supported surface for these.
     #
     # IMPORTANT: ``HERMES_*`` overall is NOT blocked. Many legitimate
    -# integration credentials follow that prefix (HERMES_GEMINI_CLIENT_ID,
    -# HERMES_LANGFUSE_PUBLIC_KEY, HERMES_SPOTIFY_CLIENT_ID, ...). The
    +# integration credentials follow that prefix (HERMES_LANGFUSE_PUBLIC_KEY,
    +# HERMES_SPOTIFY_CLIENT_ID, ...). The
     # denylist is name-by-name on purpose so the gate stays narrow and
     # doesn't accidentally break provider setup wizards.
     #
    ```

    ```diff
    @@ -3082,62 +3082,6 @@ def _ensure_hermes_home_managed(home: Path):
             "category": "provider",
             "advanced": True,
         },
    -    "HERMES_GEMINI_CLIENT_ID": {
    -        "description": "Google OAuth client ID for google-gemini-cli (optional; defaults to Google's public gemini-cli client)",
    -        "prompt": "Google OAuth client ID (optional — leave empty to use the public default)",
    -        "url": "https://console.cloud.google.com/apis/credentials",
    -        "password": False,
    -        "category": "provider",
    -        "advanced": True,
    -    },
    -    "HERMES_GEMINI_CLIENT_SECRET": {
    -        "description": "Google OAuth client secret for google-gemini-cli (optional)",
    -        "prompt": "Google OAuth client secret (optional)",
    -        "url": "https://console.cloud.google.com/apis/credentials",
    -        "password": True,
    -        "category": "provider",
    -        "advanced": True,
    -    },
    -    "HERMES_GEMINI_PROJECT_ID": {
    -        "description": "GCP project ID for paid Gemini tiers (free tier auto-provisions)",
    -        "prompt": "GCP project ID for Gemini OAuth (leave empty for free tier)",
    -        "url": None,
    -        "password": False,
    -        "category": "provider",
    -        "advanced": True,
    -    },
    -    "HERMES_ANTIGRAVITY_CLIENT_ID": {
    -        "description": "Google OAuth client ID for google-antigravity (optional; discovered from agy when omitted)",
    -        "prompt": "Antigravity OAuth client ID (optional — leave empty to discover from agy)",
    -        "url": "https://console.cloud.google.com/apis/credentials",
    -        "password": False,
    -        "category": "provider",
    -        "advanced": True,
    -    },
    -    "HERMES_ANTIGRAVITY_CLIENT_SECRET": {
    -        "description": "Google OAuth client secret for google-antigravity (optional)",
    -        "prompt": "Antigravity OAuth client secret (optional)",
    -        "url": "https://console.cloud.google.com/apis/credentials",
    -        "password": True,
    -        "category": "provider",
    -        "advanced": True,
    -    },
    -    "HERMES_ANTIGRAVITY_CLI_PATH": {
    -        "description": "Path to agy/Antigravity CLI for OAuth client credential discovery",
    -        "prompt": "Antigravity CLI path (leave empty to search PATH/default locations)",
    -        "url": None,
    -        "password": False,
    -        "category": "provider",
    -        "advanced": True,
    -    },
    -    "HERMES_ANTIGRAVITY_PROJECT_ID": {
    -        "description": "GCP project ID for Antigravity OAuth (auto-discovered when omitted)",
    -        "prompt": "GCP project ID for Antigravity OAuth (leave empty to auto-discover)",
    -        "url": None,
    -        "password": False,
    -        "category": "provider",
    -        "advanced": True,
    -    },
         "OPENCODE_ZEN_API_KEY": {
             "description": "OpenCode Zen API key (pay-as-you-go access to curated models)",
             "prompt": "OpenCode Zen API key",
    ```
  - `hermes_cli/doctor.py`

    ```diff
    @@ -158,12 +158,6 @@ def _has_healthy_oauth_fallback_for_apikey_provider(provider_label: str) -> bool
         that direct-key problem into the final blocking summary.
         """
         normalized = (provider_label or "").strip().lower()
    -    if normalized in {"google / gemini", "gemini"}:
    -        try:
    -            from hermes_cli.auth import get_gemini_oauth_auth_status
    -            return bool((get_gemini_oauth_auth_status() or {}).get("logged_in"))
    -        except Exception:
    -            return False
         if normalized == "minimax":
             try:
                 from hermes_cli.auth import get_minimax_oauth_auth_status
    ```

    ```diff
    @@ -1077,7 +1071,6 @@ def run_doctor(args):
             from hermes_cli.auth import (
                 get_nous_auth_status,
                 get_codex_auth_status,
    -            get_gemini_oauth_auth_status,
                 get_minimax_oauth_auth_status,
             )
     
    ```

    ```diff
    @@ -1105,20 +1098,6 @@ def run_doctor(args):
                         "from an existing Codex CLI login)"
                     )
     
    -        gemini_status = get_gemini_oauth_auth_status()
    -        if gemini_status.get("logged_in"):
    -            email = gemini_status.get("email") or ""
    -            project = gemini_status.get("project_id") or ""
    -            pieces = []
    -            if email:
    -                pieces.append(email)
    -            if project:
    -                pieces.append(f"project={project}")
    -            suffix = f" ({', '.join(pieces)})" if pieces else ""
    -            check_ok("Google Gemini OAuth", f"(logged in{suffix})")
    -        else:
    -            check_warn("Google Gemini OAuth", "(not logged in)")
    -
             minimax_status = get_minimax_oauth_auth_status()
             if minimax_status.get("logged_in"):
                 region = minimax_status.get("region", "global")
    ```
  - `hermes_cli/main.py`

    ```diff
    @@ -602,8 +602,6 @@ def _resolve_sudo_user_profile_env(name: str) -> str | None:
         _model_flow_xai_oauth,
         _model_flow_qwen_oauth,
         _model_flow_minimax_oauth,
    -    _model_flow_google_gemini_cli,
    -    _model_flow_google_antigravity,
         _model_flow_custom,
         _model_flow_azure_foundry,
         _model_flow_named_custom,
    ```

    ```diff
    @@ -3073,10 +3071,6 @@ def _active_custom_key_from_base_url() -> str:
             _model_flow_qwen_oauth(config, current_model)
         elif selected_provider == "minimax-oauth":
             _model_flow_minimax_oauth(config, current_model, args=args)
    -    elif selected_provider == "google-gemini-cli":
    -        _model_flow_google_gemini_cli(config, current_model)
    -    elif selected_provider == "google-antigravity":
    -        _model_flow_google_antigravity(config, current_model)
         elif selected_provider == "copilot-acp":
             _model_flow_copilot_acp(config, current_model)
         elif selected_provider == "copilot":
    ```

    ```diff
    @@ -11254,7 +11248,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", "google-gemini-cli", "google-antigravity", "xai", "bedrock", "azure-foundry",
    +            "anthropic", "gemini", "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
    @@ -633,142 +633,6 @@ def _model_flow_minimax_oauth(config, current_model="", args=None):
         _update_config_for_provider("minimax-oauth", creds["base_url"])
         print(f"\u2713 Using MiniMax model: {selected}")
     
    -def _model_flow_google_gemini_cli(_config, current_model=""):
    -    """Google Gemini OAuth (PKCE) via Cloud Code Assist — supports free AND paid tiers.
    -
    -    Flow:
    -      1. Show upfront warning about Google's ToS stance (per opencode-gemini-auth).
    -      2. If creds missing, run PKCE browser OAuth via agent.google_oauth.
    -      3. Resolve project context (env -> config -> auto-discover -> free tier).
    -      4. Prompt user to pick a model.
    -      5. Save to ~/.hermes/config.yaml.
    -    """
    -    from hermes_cli.auth import (
    -        DEFAULT_GEMINI_CLOUDCODE_BASE_URL,
    -        get_gemini_oauth_auth_status,
    -        resolve_gemini_oauth_runtime_credentials,
    -        _prompt_model_selection,
    -        _save_model_choice,
    -        _update_config_for_provider,
    -    )
    -    from hermes_cli.models import _PROVIDER_MODELS
    -
    -    print()
    -    print("⚠  Google considers using the Gemini CLI OAuth client with third-party")
    -    print("   software a policy violation. Some users have reported account")
    -    print("   restrictions. You can use your own API key via 'gemini' provider")
    -    print("   for the lowest-risk experience.")
    -    print()
    -    try:
    -        proceed = input("Continue with OAuth login? [y/N]: ").strip().lower()
    -    except (EOFError, KeyboardInterrupt):
    -        print("Cancelled.")
    -        return
    -    if proceed not in {"y", "yes"}:
    -        print("Cancelled.")
    -        return
    -
    -    status = get_gemini_oauth_auth_status()
    -    if not status.get("logged_in"):
    -        try:
    -            from agent.google_oauth import resolve_project_id_from_env, start_oauth_flow
    -
    -            env_project = resolve_project_id_from_env()
    -            start_oauth_flow(force_relogin=True, project_id=env_project)
    -        except Exception as exc:
    -            print(f"OAuth login failed: {exc}")
    -            return
    -
    -    # Verify creds resolve + trigger project discovery
    -    try:
    -        creds = resolve_gemini_oauth_runtime_credentials(force_refresh=False)
    -        project_id = creds.get("project_id", "")
    -        if project_id:
    -            print(f"  Using GCP project: {project_id}")
    -        else:
    -            print(
    -                "  No GCP project configured — free tier will be auto-provisioned on first request."
    -            )
    -    except Exception as exc:
    -        print(f"Failed to resolve Gemini credentials: {exc}")
    -        return
    -
    -    models = list(_PROVIDER_MODELS.get("google-gemini-cli") or [])
    -    default = current_model or (models[0] if models else "gemini-3-flash-preview")
    -    selected = _prompt_model_selection(
    -        models,
    -        current_model=default,
    -        confirm_provider="google-gemini-cli",
    -        confirm_base_url=DEFAULT_GEMINI_CLOUDCODE_BASE_URL,
    -    )
    -    if selected:
    -        _save_model_choice(selected)
    -        _update_config_for_provider(
    -            "google-gemini-cli", DEFAULT_GEMINI_CLOUDCODE_BASE_URL
    -        )
    -        print(
    -            f"Default model set to: {selected} (via Google Gemini OAuth / Code Assist)"
    -        )
    -    else:
    -        print("No change.")
    -
    -
    -def _model_flow_google_antigravity(_config, current_model=""):
    -    """Google Antigravity OAuth via Antigravity Code Assist.
    -
    -    Antigravity is Google's consumer successor to the Gemini CLI. It reuses the
    -    Code Assist backend with a distinct OAuth client + scopes. Leaves the
    -    `google-gemini-cli` provider (Enterprise Code Assist) untouched.
    -    """
    -    from hermes_cli.auth import (
    -        DEFAULT_ANTIGRAVITY_CLOUDCODE_BASE_URL,
    -        get_antigravity_oauth_auth_status,
    -        resolve_antigravity_oauth_runtime_credentials,
    -        _prompt_model_selection,
    -        _save_model_choice,
    -        _update_config_for_provider,
    -    )
    -    from hermes_cli.models import provider_model_ids
    -
    -    status = get_antigravity_oauth_auth_status()
    -    if not status.get("logged_in"):
    -        try:
    -            from agent.antigravity_oauth import resolve_project_id_from_env, start_oauth_flow
    -
    -            env_project = resolve_project_id_from_env()
    -            start_oauth_flow(force_relogin=True, project_id=env_project)
    -        except Exception as exc:
    -            print(f"OAuth login failed: {exc}")
    -            return
    -
    -    try:
    -        creds = resolve_antigravity_oauth_runtime_credentials(force_refresh=False)
    -        project_id = creds.get("project_id", "")
    -        if project_id:
    -            print(f"  Using Antigravity project: {project_id}")
    -    except Exception as exc:
    -        print(f"Failed to resolve Antigravity credentials: {exc}")
    -        return
    -
    -    models = provider_model_ids("google-antigravity")
    -    default = current_model or (models[0] if models else "gemini-3-flash-agent")
    -    selected = _prompt_model_selection(
    -        models,
    -        current_model=default,
    -        confirm_provider="google-antigravity",
    -        confirm_base_url=DEFAULT_ANTIGRAVITY_CLOUDCODE_BASE_URL,
    -    )
    -    if selected:
    -        _save_model_choice(selected)
    -        _update_config_for_provider(
    -            "google-antigravity", DEFAULT_ANTIGRAVITY_CLOUDCODE_BASE_URL
    -        )
    -        print(
    -            f"Default model set to: {selected} (via Google Antigravity OAuth / Code Assist)"
    -        )
    -    else:
    -        print("No change.")
    -
     
     def _model_flow_custom(config):
         """Custom endpoint: collect URL, API key, and model name.
    ```
  - `hermes_cli/models.py`

    ```diff
    @@ -265,26 +265,6 @@ def _xai_curated_models() -> list[str]:
             "gemini-3.5-flash",
             "gemini-3.1-flash-lite-preview",
         ],
    -    "google-gemini-cli": [
    -        "gemini-3.1-pro-preview",
    -        "gemini-3-pro-preview",
    -        # Code Assist serves two flash slugs with different access gates
    -        # (gemini-cli models.ts): gemini-3-flash-preview is the preview flash
    -        # that subscription/free-tier OAuth users actually reach, while
    -        # gemini-3.5-flash is GA-channel-gated. Offer both so non-GA users
    -        # aren't stuck with a slug cloudcode-pa 404s for them.
    -        "gemini-3-flash-preview",
    -        "gemini-3.5-flash",
    -    ],
    -    "google-antigravity": [
    -        "gemini-3-flash-agent",
    -        "gemini-3.5-flash-low",
    -        "gemini-pro-agent",
    -        "gemini-3.1-pro-low",
    -        "claude-sonnet-4-6",
    -        "claude-opus-4-6-thinking",
    -        "gpt-oss-120b-medium",
    -    ],
         "zai": [
             "glm-5.2",
             "glm-5.1",
    ```

    ```diff
    @@ -1037,8 +1017,6 @@ 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("google-gemini-cli", "Google Gemini (OAuth)",   "Google Gemini via OAuth + Code Assist (Code Assist OAuth flow)"),
    -    ProviderEntry("google-antigravity", "Google Antigravity (OAuth)", "Google Antigravity via OAuth + Code Assist (Gemini 3.5/3.1, Claude, GPT-OSS where entitled)"),
         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
    @@ -1109,7 +1087,7 @@ class ProviderEntry(NamedTuple):
         "kimi":     ("Kimi / Moonshot", "Coding Plan, Moonshot global & China endpoints", ["kimi-coding", "kimi-coding-cn"]),
         "minimax":  ("MiniMax",         "Global, OAuth Coding Plan & China endpoints",     ["minimax", "minimax-oauth", "minimax-cn"]),
         "xai":      ("xAI Grok",        "Direct API or SuperGrok / Premium+ OAuth",        ["xai", "xai-oauth"]),
    -    "google":   ("Google Gemini",   "AI Studio API or OAuth + Code Assist",            ["gemini", "google-gemini-cli"]),
    +    "google":   ("Google Gemini",   "Google AI Studio (API key)",                     ["gemini"]),
         "openai":   ("OpenAI",          "Codex CLI or direct OpenAI API",                  ["openai-codex", "openai-api"]),
         "opencode": ("OpenCode",        "Zen pay-as-you-go or Go subscription",            ["opencode-zen", "opencode-go"]),
         "copilot":  ("GitHub Copilot",  "GitHub token API or copilot --acp process",       ["copilot", "copilot-acp"]),
    ```

    ```diff
    @@ -1230,14 +1208,6 @@ def group_providers(slugs):
         "qwen": "alibaba",
         "alibaba-cloud": "alibaba",
         "qwen-portal": "qwen-oauth",
    -    "gemini-cli": "google-gemini-cli",
    -    "gemini-oauth": "google-gemini-cli",
    -    "antigravity": "google-antigravity",
    -    "antigravity-oauth": "google-antigravity",
    -    "antigravity-cli": "google-antigravity",
    -    "google-antigravity-oauth": "google-antigravity",
    -    "agy": "google-antigravity",
    -    "agy-cli": "google-antigravity",
         "hf": "huggingface",
         "hugging-face": "huggingface",
         "huggingface-hub": "huggingface",
    ```

    ```diff
    @@ -1805,13 +1775,10 @@ def _model_in_provider_catalog(name_lower: str, providers: set[str]) -> bool:
     )
     
     # Subscription/OAuth providers whose catalogs RE-EXPOSE other vendors' models
    -# (e.g. google-antigravity serves Claude / Gemini / GPT-OSS where the account
    -# is entitled). For bare short-alias resolution (`sonnet`, `opus`, ...) these
    -# must NOT hijack the alias away from the model's native vendor provider
    -# (`anthropic`, `gemini`, ...). They're tried only as a last resort, after
    -# every native-vendor catalog. They are NOT aggregators (an explicit switch TO
    -# them is still valid), so they stay out of _AGGREGATOR_PROVIDERS.
    -_BORROWED_MODEL_PROVIDERS = frozenset({"google-antigravity"})
    +# would be listed here (tried only as a last resort for bare short-alias
    +# resolution, after every native-vendor catalog, so they never hijack an alias
    +# away from the model's native vendor). None are currently defined.
    +_BORROWED_MODEL_PROVIDERS: frozenset[str] = frozenset()
     
     
     def _resolve_static_model_alias(
    ```

    ```diff
    @@ -1863,9 +1830,9 @@ def _match(provider: str) -> Optional[str]:
             if provider in current_keys and (matched := _match(provider)):
                 return provider, matched
     
    -    # Last resort: providers that re-expose other vendors' models (e.g.
    -    # google-antigravity serving Claude). Only reached when no native-vendor
    -    # catalog matched — so `sonnet` resolves to anthropic, not antigravity.
    +    # Last resort: providers that re-expose other vendors' models. Only reached
    +    # when no native-vendor catalog matched — so `sonnet` resolves to anthropic.
    +    # None are currently defined (_BORROWED_MODEL_PROVIDERS is empty).
         for provider in _BORROWED_MODEL_PROVIDERS:
             if provider in current_keys and (matched := _match(provider)):
                 return provider, matched
    ```

    ```diff
    @@ -2240,32 +2207,6 @@ def _merge_with_models_dev(provider: str, curated: list[str]) -> list[str]:
         return merged
     
     
    -def _fetch_antigravity_models(*, force_refresh: bool = False) -> list[str]:
    -    try:
    -        from agent import antigravity_oauth
    -        from agent.antigravity_code_assist import (
    -            fetch_available_models_with_fallbacks,
    -            load_code_assist,
    -            parse_agent_model_ids,
    -        )
    -        from hermes_cli.auth import resolve_antigravity_oauth_runtime_credentials
    -
    -        creds = resolve_antigravity_oauth_runtime_credentials(force_refresh=force_refresh)
    -        access_token = [redacted:secret-assignment]"api_key") or "").strip()
    -        project_id = str(creds.get("project_id") or "").strip()
    -        if not access_token:
    -            return []
    -        if not project_id:
    -            info = load_code_assist(access_token)
    -            project_id = info.project_id
    -            if project_id:
    -                antigravity_oauth.update_project_ids(project_id=project_id, managed_project_id=project_id)
    -        payload = fetch_available_models_with_fallbacks(access_token, project_id=project_id)
    -        return parse_agent_model_ids(payload)
    -    except Exception:
    -        return []
    -
    -
     def provider_model_ids(provider: Optional[str], *, force_refresh: bool = False) -> list[str]:
         """Return the best known model catalog for a provider.
     
    ```

    ```diff
    @@ -2296,10 +2237,6 @@ def provider_model_ids(provider: Optional[str], *, force_refresh: bool = False)
             return get_codex_model_ids(access_token=[redacted:secret-assignment]
         if normalized == "xai-oauth":
             return list(_PROVIDER_MODELS.get("xai-oauth", _PROVIDER_MODELS.get("xai", [])))
    -    if normalized == "google-antigravity":
    -        live = _fetch_antigravity_models(force_refresh=force_refresh)
    -        if live:
    -            return live
         if normalized in {"copilot", "copilot-acp"}:
             try:
                 live = _fetch_github_models(_resolve_copilot_catalog_api_key())
    ```
  - `hermes_cli/provider_catalog.py`

    ```diff
    @@ -57,7 +57,7 @@
     class ProviderDescriptor:
         """One provider, as seen by every surface (CLI picker + both GUI tabs)."""
     
    -    slug: str                      # canonical id, e.g. "google-gemini-cli"
    +    slug: str                      # canonical id, e.g. "openai-codex"
         label: str                     # human display name
         description: str               # one-line description
         auth_type: str                 # api_key | oauth_* | external_process | copilot | aws_sdk
    ```
  - `hermes_cli/providers.py`

    ```diff
    @@ -76,16 +76,6 @@ class HermesOverlay:
             base_url_override="https://portal.qwen.ai/v1",
             base_url_env_var="HERMES_QWEN_BASE_URL",
         ),
    -    "google-gemini-cli": HermesOverlay(
    -        transport="openai_chat",
    -        auth_type="oauth_external",
    -        base_url_override="cloudcode-pa://google",
    -    ),
    -    "google-antigravity": HermesOverlay(
    -        transport="openai_chat",
    -        auth_type="oauth_external",
    -        base_url_override="antigravity-pa://google",
    -    ),
         "lmstudio": HermesOverlay(
             transport="openai_chat",
             auth_type="api_key",
    ```

    ```diff
    @@ -315,18 +305,6 @@ class ProviderDef:
         "alibaba-coding": "alibaba-coding-plan",
         "alibaba_coding_plan": "alibaba-coding-plan",
     
    -    # google-gemini-cli (OAuth + Code Assist)
    -    "gemini-cli": "google-gemini-cli",
    -    "gemini-oauth": "google-gemini-cli",
    -
    -    # google-antigravity (OAuth + Antigravity Code Assist)
    -    "antigravity": "google-antigravity",
    -    "antigravity-oauth": "google-antigravity",
    -    "antigravity-cli": "google-antigravity",
    -    "google-antigravity-oauth": "google-antigravity",
    -    "agy": "google-antigravity",
    -    "agy-cli": "google-antigravity",
    -
         # huggingface
         "hf": "huggingface",
         "hugging-face": "huggingface",
    ```
  - `hermes_cli/runtime_provider.py`

    ```diff
    @@ -26,8 +26,6 @@
         resolve_codex_runtime_credentials,
         resolve_xai_oauth_runtime_credentials,
         resolve_qwen_runtime_credentials,
    -    resolve_gemini_oauth_runtime_credentials,
    -    resolve_antigravity_oauth_runtime_credentials,
         resolve_api_key_provider_credentials,
         resolve_external_process_provider_credentials,
         has_usable_secret,
    ```

    ```diff
    @@ -332,12 +330,6 @@ def _resolve_runtime_from_pool_entry(
         elif provider == "qwen-oauth":
             api_mode = "chat_completions"
             base_url = base_url or DEFAULT_QWEN_BASE_URL
    -    elif provider == "google-gemini-cli":
    -        api_mode = "chat_completions"
    -        base_url = base_url or "cloudcode-pa://google"
    -    elif provider == "google-antigravity":
    -        api_mode = "chat_completions"
    -        base_url = base_url or "antigravity-pa://google"
         elif provider == "minimax-oauth":
             # MiniMax OAuth tokens are valid only against the Anthropic Messages
             # compatible endpoint. Do not honor stale model.api_mode values from a
    ```

    ```diff
    @@ -1618,46 +1610,6 @@ def resolve_runtime_provider(
                     "requested_provider": requested_provider,
                 }
     
    -    if provider == "google-gemini-cli":
    -        try:
    -            creds = resolve_gemini_oauth_runtime_credentials()
    -            return {
    -                "provider": "google-gemini-cli",
    -                "api_mode": "chat_completions",
    -                "base_url": creds.get("base_url", ""),
    -                "api_key": creds.get("api_key", ""),
    -                "source": creds.get("source", "google-oauth"),
    -                "expires_at_ms": creds.get("expires_at_ms"),
    -                "email": creds.get("email", ""),
    -                "project_id": creds.get("project_id", ""),
    -                "requested_provider": requested_provider,
    -            }
    -        except AuthError:
    -            if requested_provider != "auto":
    -                raise
    -            logger.info("Google Gemini OAuth credentials failed; "
    -                        "falling through to next provider.")
    -
    -    if provider == "google-antigravity":
    -        try:
    -            creds = resolve_antigravity_oauth_runtime_credentials()
    -            return {
    -                "provider": "google-antigravity",
    -                "api_mode": "chat_completions",
    -                "base_url": creds.get("base_url", ""),
    -                "api_key": creds.get("api_key", ""),
    -                "source": creds.get("source", "antigravity-oauth"),
    -                "expires_at_ms": creds.get("expires_at_ms"),
    -                "email": creds.get("email", ""),
    -                "project_id": creds.get("project_id", ""),
    -                "requested_provider": requested_provider,
    -            }
    -        except AuthError:
    -            if requested_provider != "auto":
    -                raise
    -            logger.info("Google Antigravity OAuth credentials failed; "
    -                        "falling through to next provider.")
    -
         if provider == "copilot-acp":
             creds = resolve_external_process_provider_credentials(provider)
             return {
    ```
  - `hermes_cli/tips.py`

    ```diff
    @@ -420,7 +420,6 @@
         '/platforms shows gateway and messaging-platform connection status right from inside chat.',
         '/commands paginates the full slash-command + installed-skill list — useful on platforms without tab completion.',
         '/toolsets lists every available toolset so you know what -t/--toolsets accepts.',
    -    '/gquota shows Google Gemini Code Assist quota usage with progress bars when that provider is active.',
         '/voice tts toggles TTS-only mode — agent replies out loud but you still type your prompts.',
         '/reload-skills re-scans ~/.hermes/skills/ so drop-in skills appear without restarting the session.',
         '/indicator kaomoji|emoji|unicode|ascii picks the TUI busy-indicator style shown during agent runs.',
    ```
  - `hermes_cli/web_server.py`

    ```diff
    @@ -5640,23 +5640,6 @@ def _claude_code_only_status() -> Dict[str, Any]:
         return {"logged_in": False, "source": None}
     
     
    -def _gemini_cli_status() -> Dict[str, Any]:
    -    """Status for the google-gemini-cli OAuth provider (Code Assist login)."""
    -    try:
    -        from hermes_cli import auth as hauth
    -        raw = hauth.get_gemini_oauth_auth_status()
    -    except Exception as e:
    -        return {"logged_in": False, "error": str(e)}
    -    return {
    -        "logged_in": bool(raw.get("logged_in")),
    -        "source": raw.get("source") or "google_oauth",
    -        "source_label": raw.get("email") or raw.get("auth_file") or "Google Code Assist",
    -        "token_preview": _truncate_token(raw.get("api_key")),
    -        "expires_at": None,
    -        "has_refresh_token": True,
    -    }
    -
    -
     def _copilot_acp_status() -> Dict[str, Any]:
         """Status for copilot-acp — credentials are owned by the Copilot CLI.
     
    ```

    ```diff
    @@ -5736,14 +5719,6 @@ def _copilot_acp_status() -> Dict[str, Any]:
             "docs_url": "https://hermes-agent.nousresearch.com/docs/guides/xai-grok-oauth",
             "status_fn": None,  # dispatched via auth.get_xai_oauth_auth_status
         },
    -    {
    -        "id": "google-gemini-cli",
    -        "name": "Google Gemini (OAuth + Code Assist)",
    -        "flow": "external",
    -        "cli_command": "hermes auth add google-gemini-cli",
    -        "docs_url": "https://ai.google.dev/gemini-api/docs",
    -        "status_fn": _gemini_cli_status,
    -    },
         {
             "id": "copilot-acp",
             "name": "GitHub Copilot (ACP)",
    ```
  - `/dev/null`

    ```diff
    @@ -1,80 +0,0 @@
    -# Gemini OAuth Provider — Implementation Plan
    -
    -## Goal
    -Add a first-class `gemini` provider that authenticates via Google OAuth, using the standard Gemini API (not Cloud Code Assist). Users who have a Google AI subscription or Gemini API access can authenticate through the browser without needing to manually copy API keys.
    -
    -## Architecture Decision
    -- **Path A (chosen):** Standard Gemini API at `generativelanguage.googleapis.com/v1beta`
    -- **NOT Path B:** Cloud Code Assist (`cloudcode-pa.googleapis.com`) — rate-limited free tier, internal API, account ban risk
    -- Standard `chat_completions` api_mode via OpenAI SDK — no new api_mode needed
    -- Our own OAuth credentials — NOT sharing tokens with Gemini CLI
    -
    -## OAuth Flow
    -- **Type:** Authorization Code + PKCE (S256) — same pattern as clawdbot/pi-mono
    -- **Auth URL:** `https://accounts.google.com/o/oauth2/v2/auth`
    -- **Token URL:** `https://oauth2.googleapis.com/token`
    -- **Redirect:** `http://localhost:8085/oauth2callback` (localhost callback server)
    -- **Fallback:** Manual URL paste for remote/WSL/headless environments
    -- **Scopes:** `https://www.googleapis.com/auth/cloud-platform`, `https://www.googleapis.com/auth/userinfo.email`
    -- **PKCE:** S256 code challenge, 32-byte random verifier
    -
    -## Client ID
    -- Need to register a "Desktop app" OAuth client on a Nous Research GCP project
    -- Ship client_id + client_secret in code (Google considers installed app secrets non-confidential)
    -- Alternatively: accept user-provided client_id via env vars as override
    -
    -## Token Lifecycle
    -- Store at `~/.hermes/gemini_oauth.json` (NOT sharing with `~/.gemini/oauth_creds.json`)
    -- Fields: `client_id`, `client_secret`, `refresh_token`, `access_token`, `expires_at`, `email`
    -- File permissions: 0o600
    -- Before each API call: check expiry, refresh if within 5 min of expiration
    -- Refresh: POST to token URL with `grant_type=refresh_token`
    -- File locking for concurrent access (multiple agent sessions)
    -
    -## API Integration
    -- Base URL: `https://generativelanguage.googleapis.com/v1beta`
    -- Auth: native Gemini API authentication handled by the provider adapter
    -- api_mode: `chat_completions` (standard facade over native transport)
    -- Models: gemini-2.5-pro, gemini-2.5-flash, gemini-2.0-flash, etc.
    -
    -## Files to Create/Modify
    -
    -### New files
    -1. `agent/google_oauth.py` — OAuth flow (PKCE, localhost server, token exchange, refresh)
    -   - `start_oauth_flow()` — opens browser, starts callback server
    -   - `exchange_code()` — code → tokens
    -   - `refresh_access_token()` — refresh flow
    -   - `load_credentials()` / `save_credentials()` — file I/O with locking
    -   - `get_valid_access_token()` — check expiry, refresh if needed
    -   - ~200 lines
    -
    -### Existing files to modify
    -2. `hermes_cli/auth.py` — Add ProviderConfig for "gemini" with auth_type="oauth_google"
    -3. `hermes_cli/models.py` — Add Gemini model catalog
    -4. `hermes_cli/runtime_provider.py` — Add gemini branch (read OAuth token, build OpenAI client)
    -5. `hermes_cli/main.py` — Add `_model_flow_gemini()`, add to provider choices
    -6. `hermes_cli/setup.py` — Add gemini auth flow (trigger browser OAuth)
    -7. `run_agent.py` — Token refresh before API calls (like Copilot pattern)
    -8. `agent/auxiliary_client.py` — Add gemini to aux resolution chain
    -9. `agent/model_metadata.py` — Add Gemini model context lengths
    -
    -### Tests
    -10. `tests/agent/test_google_oauth.py` — OAuth flow unit tests
    -11. `tests/test_api_key_providers.py` — Add gemini provider test
    -
    -### Docs
    -12. `website/docs/getting-started/quickstart.md` — Add gemini to provider table
    -13. `website/docs/user-guide/configuration.md` — Gemini setup section
    -14. `website/docs/reference/environment-variables.md` — New env vars
    -
    -## Estimated scope
    -~400 lines new code, ~150 lines modifications, ~100 lines tests, ~50 lines docs = ~700 lines total
    -
    -## Prerequisites
    -- Nous Research GCP project with Desktop OAuth client registered
    -- OR: accept user-provided client_id via HERMES_GEMINI_CLIENT_ID env var
    -
    -## Reference implementations
    -- clawdbot: `extensions/google/oauth.flow.ts` (PKCE + localhost server)
    -- pi-mono: `packages/ai/src/utils/oauth/google-gemini-cli.ts` (same flow)
    -- hermes-agent Copilot OAuth: `hermes_cli/main.py` `_copilot_device_flow()` (different flow type but same lifecycle pattern)
    ```
  - `plugins/model-providers/gemini/__init__.py`

    ```diff
    @@ -1,11 +1,9 @@
     """Google Gemini provider profiles.
     
     gemini:            Google AI Studio (API key) — uses GeminiNativeClient
    -google-gemini-cli: Google Cloud Code Assist (OAuth) — uses GeminiCloudCodeClient
    -google-antigravity: Google Antigravity Code Assist (OAuth) — uses AntigravityCloudCodeClient
     
    -Both report api_mode="chat_completions" but use custom native clients
    -that bypass the standard OpenAI transport. The profile captures auth
    +Reports api_mode="chat_completions" but uses a custom native client
    +that bypasses the standard OpenAI transport. The profile captures auth
     and endpoint metadata for auth.py / runtime_provider.py migration, and
     carries the thinking_config translation hook so the transport's profile
     path produces the same extra_body shape the legacy flag path did.
    ```

    ```diff
    @@ -60,31 +58,4 @@ def build_extra_body(
         default_aux_model="gemini-3.5-flash",
     )
     
    -google_gemini_cli = GeminiProfile(
    -    name="google-gemini-cli",
    -    aliases=("gemini-cli", "gemini-oauth"),
    -    api_mode="chat_completions",
    -    env_vars=(),  # OAuth — no API key
    -    base_url="cloudcode-pa://google",  # Cloud Code Assist internal scheme
    -    auth_type="oauth_external",
    -)
    -
    -google_antigravity = GeminiProfile(
    -    name="google-antigravity",
    -    aliases=(
    -        "antigravity",
    -        "antigravity-oauth",
    -        "antigravity-cli",
    -        "google-antigravity-oauth",
    -        "agy",
    -        "agy-cli",
    -    ),
    -    api_mode="chat_completions",
    -    env_vars=(),  # OAuth — no API key
    -    base_url="antigravity-pa://google",  # Antigravity Code Assist internal scheme
    -    auth_type="oauth_external",
    -)
    -
     register_provider(gemini)
    -register_provider(google_gemini_cli)
    -register_provider(google_antigravity)
    ```
  - `run_agent.py`

    ```diff
    @@ -273,7 +273,7 @@ def _pool_may_recover_from_rate_limit(
             return False
         # CloudCode / Gemini CLI quotas are account-wide — all pool entries share
         # the same throttle window, so rotation can't recover.  Prefer fallback.
    -    if provider == "google-gemini-cli" or str(base_url or "").startswith("cloudcode-pa://"):
    +    if str(base_url or "").startswith("cloudcode-pa://"):
             return False
         return len(pool.entries()) > 1
     
    ```

    ```diff
    @@ -4093,8 +4093,7 @@ def _credential_pool_may_recover_rate_limit(self) -> bool:
             if pool is None:
                 return False
             if (
    -            self.provider == "google-gemini-cli"
    -            or str(getattr(self, "base_url", "")).startswith("cloudcode-pa://")
    +            str(getattr(self, "base_url", "")).startswith("cloudcode-pa://")
             ):
                 # CloudCode/Gemini quota windows are usually account-level throttles.
                 # Prefer the configured fallback immediately instead of waiting out
    ```
  - `skills/autonomous-ai-agents/hermes-agent/SKILL.md`

    ```diff
    @@ -336,7 +336,6 @@ The registry of record is `hermes_cli/commands.py` — every consumer
     /commands [page]     Browse all commands (gateway)
     /usage               Token usage
     /insights [days]     Usage analytics
    -/gquota              Show Google Gemini Code Assist quota usage (CLI)
     /status              Session info (gateway)
     /profile             Active profile info
     /debug               Upload debug report (system info + logs) and get shareable links
    ```
  - `/dev/null`

    ```diff
    @@ -1,405 +0,0 @@
    -"""Tests for the google-antigravity OAuth + Antigravity Code Assist provider."""
    -
    -from __future__ import annotations
    -
    -import json
    -import os
    -import stat
    -import time
    -import threading
    -import urllib.parse
    -from io import BytesIO
    -from pathlib import Path
    -
    -import pytest
    -
    -
    [redacted:email](autouse=True)
    -def _isolate_env(monkeypatch, tmp_path):
    -    home = tmp_path / ".hermes"
    -    home.mkdir(parents=True)
    -    monkeypatch.setattr(Path, "home", lambda: tmp_path)
    -    monkeypatch.setenv("HERMES_HOME", str(home))
    -    for key in (
    -        "HERMES_ANTIGRAVITY_CLIENT_ID",
    -        "HERMES_ANTIGRAVITY_CLIENT_SECRET",
    -        "HERMES_ANTIGRAVITY_CLI_PATH",
    -        "HERMES_ANTIGRAVITY_PROJECT_ID",
    -        "GOOGLE_CLOUD_PROJECT",
    -        "GOOGLE_CLOUD_PROJECT_ID",
    -        "LOCALAPPDATA",
    -        "APPDATA",
    -        "ProgramFiles",
    -        "ProgramFiles(x86)",
    -    ):
    -        monkeypatch.delenv(key, raising=False)
    -    monkeypatch.setattr("shutil.which", lambda _: None)
    -    try:
    -        from agent import antigravity_oauth
    -
    -        antigravity_oauth._discovered_creds_cache.clear()
    -    except Exception:
    -        pass
    -    return home
    -
    -
    -class TestAntigravityCredentials:
    -    def test_save_load_uses_separate_file_and_0600_permissions(self):
    -        from agent.antigravity_oauth import (
    -            AntigravityCredentials,
    -            _credentials_path,
    -            load_credentials,
    -            save_credentials,
    -        )
    -
    -        save_credentials(AntigravityCredentials(
    -            access_token="at",
    -            refresh_token="rt",
    -            expires_ms=int((time.time() + 3600) * 1000),
    -            email="[redacted:email]",
    -            project_id="proj-123",
    -        ))
    -
    -        assert _credentials_path().name == "antigravity_oauth.json"
    -        loaded = load_credentials()
    -        assert loaded is not None
    -        assert loaded.refresh_token == "rt"
    -        assert loaded.project_id == "proj-123"
    -        if os.name != "nt":
    -            assert stat.S_IMODE(_credentials_path().stat().st_mode) == 0o600
    -
    -    def test_env_override_client_id(self, monkeypatch):
    -        from agent.antigravity_oauth import _get_client_id
    -
    -        monkeypatch.setenv("HERMES_ANTIGRAVITY_CLIENT_ID", "custom.apps.googleusercontent.com")
    -        assert _get_client_id() == "custom.apps.googleusercontent.com"
    -
    -    def test_env_override_client_secret(self, monkeypatch):
    -        from agent.antigravity_oauth import _get_client_secret
    -
    -        monkeypatch.setenv("HERMES_ANTIGRAVITY_CLIENT_SECRET", "custom-secret")
    -        assert _get_client_secret() == "custom-secret"
    -
    -    def test_discovers_client_credentials_from_configured_agy_path(self, tmp_path, monkeypatch):
    -        from agent import antigravity_oauth
    -
    -        fake_client_id = (
    -            "1071006060591-"
    -            + "fakefakefakefakefakefakefake"
    -            + ".apps.google"
    -            + "usercontent.com"
    -        )
    -        fake_client_secret = "GOC" + "SPX-" + "fake-secret-value-placeholde"
    -        fake_agy = tmp_path / "agy.exe"
    -        fake_agy.write_text(
    -            f'oauthClientId="{fake_client_id}";\n'
    -            f'oauthClientSecret="[redacted:secret-assignment]";\n',
    -            encoding="utf-8",
    -        )
    -        monkeypatch.setenv("HERMES_ANTIGRAVITY_CLI_PATH", str(fake_agy))
    -        antigravity_oauth._discovered_creds_cache.clear()
    -
    -        assert antigravity_oauth._get_client_id().startswith("1071006060591-")
    -        assert antigravity_oauth._get_client_secret() == fake_client_secret
    -
    -    def test_missing_discovery_falls_back_to_public_default(self, monkeypatch):
    -        # With no env override and no discoverable agy install, the public
    -        # baked-in Antigravity desktop OAuth client is used as the floor so
    -        # users without `agy` installed can still authenticate (PKCE makes the
    -        # installed-app "secret" non-confidential, same as gemini-cli).
    -        from agent import antigravity_oauth
    -        from agent.antigravity_oauth import (
    -            _DEFAULT_CLIENT_ID,
    -            _DEFAULT_CLIENT_SECRET,
    -            _require_client_id,
    -        )
    -
    -        monkeypatch.delenv("HERMES_ANTIGRAVITY_CLIENT_ID", raising=False)
    -        monkeypatch.delenv("HERMES_ANTIGRAVITY_CLIENT_SECRET", raising=False)
    -        monkeypatch.delenv("HERMES_ANTIGRAVITY_CLI_PATH", raising=False)
    -        antigravity_oauth._discovered_creds_cache.clear()
    -
    -        assert _require_client_id() == _DEFAULT_CLIENT_ID
    -        assert antigravity_oauth._get_client_secret() == _DEFAULT_CLIENT_SECRET
    -        assert _DEFAULT_CLIENT_ID.startswith("1071006060591-")
    -
    -    def test_pkce_challenge_is_s256(self):
    -        import base64
    -        import hashlib
    -
    -        from agent.antigravity_oauth import _generate_pkce_pair
    -
    -        verifier, challenge = _generate_pkce_pair()
    -        expected = base64.urlsafe_b64encode(
    -            hashlib.sha256(verifier.encode("ascii")).digest()
    -        ).rstrip(b"=").decode("ascii")
    -        assert challenge == expected
    -        assert 43 <= len(verifier) <= 128
    -
    -    def test_exchange_code_posts_pkce_payload(self, monkeypatch):
    -        from agent import antigravity_oauth
    -
    -        captured = {}
    -
    -        def fake_post(url, data, timeout):
    -            captured.update({"url": url, "data": data, "timeout": timeout})
    -            return {"access_token": "at"}
    -
    -        monkeypatch.setattr(antigravity_oauth, "_post_form", fake_post)
    -        monkeypatch.setenv("HERMES_ANTIGRAVITY_CLIENT_ID", "client.apps.googleusercontent.com")
    -        monkeypatch.setenv("HERMES_ANTIGRAVITY_CLIENT_SECRET", "secret")
    -
    -        assert antigravity_oauth.exchange_code("code", "verifier", "http://localhost/cb") == {
    -            "access_token": "at"
    -        }
    -        assert captured["url"] == antigravity_oauth.TOKEN_ENDPOINT
    -        assert captured["data"]["grant_type"] == "authorization_code"
    -        assert captured["data"]["code_verifier"] == "verifier"
    -        assert captured["data"]["redirect_uri"] == "http://localhost/cb"
    -        assert captured["data"]["client_id"] == "client.apps.googleusercontent.com"
    -        assert captured["data"]["client_secret"] == "secret"
    -
    -    def test_refresh_tries_discovered_client_secret_candidates(self, monkeypatch):
    -        from agent import antigravity_oauth
    -        from agent.antigravity_oauth import AntigravityOAuthError
    -
    -        calls = []
    -        monkeypatch.setattr(
    -            antigravity_oauth,
    -            "_iter_client_credential_candidates",
    -            lambda: [
    -                ("client.apps.googleusercontent.com", "wrong-secret"),
    -                ("client.apps.googleusercontent.com", "right-secret"),
    -            ],
    -        )
    -
    -        def fake_post(url, data, timeout):
    -            calls.append(data["client_secret"])
    -            if data["client_secret"] == "wrong-secret":
    -                raise AntigravityOAuthError(
    -                    "invalid client",
    -                    code="antigravity_oauth_invalid_client",
    -                )
    -            return {"access_token": "new-token", "expires_in": 3600}
    -
    -        monkeypatch.setattr(antigravity_oauth, "_post_form", fake_post)
    -
    -        assert antigravity_oauth.refresh_access_token("refresh-token")["access_token"] == "new-token"
    -        assert calls == ["wrong-secret", "right-secret"]
    -
    -    def test_invalid_grant_refresh_clears_credentials(self, monkeypatch):
    -        from agent import antigravity_oauth
    -        from agent.antigravity_oauth import (
    -            AntigravityCredentials,
    -            AntigravityOAuthError,
    -            load_credentials,
    -            save_credentials,
    -        )
    -
    -        save_credentials(AntigravityCredentials(
    -            access_token="[redacted:secret-assignment]",
    -            refresh_token="rt",
    -            expires_ms=int((time.time() - 3600) * 1000),
    -        ))
    -
    -        def invalid_grant(_refresh_token):
    -            raise AntigravityOAuthError("revoked", code="antigravity_oauth_invalid_grant")
    -
    -        monkeypatch.setattr(antigravity_oauth, "refresh_access_token", invalid_grant)
    -        with pytest.raises(AntigravityOAuthError, match="revoked"):
    -            antigravity_oauth.get_valid_access_token()
    -        assert load_credentials() is None
    -
    -    def test_callback_handler_captures_code_on_handler_class(self):
    -        from agent.antigravity_oauth import CALLBACK_PATH, _OAuthCallbackHandler
    -
    -        handler_cls = type("TestAntigravityOAuthCallbackHandler", (_OAuthCallbackHandler,), {})
    -        handler_cls.expected_state = "state-123"
    -        handler_cls.captured_code = None
    -        handler_cls.captured_error = None
    -        handler_cls.ready = threading.Event()
    -
    -        handler = handler_cls.__new__(handler_cls)
    -        handler.path = CALLBACK_PATH + "?" + urllib.parse.urlencode({
    -            "state": "state-123",
    -            "code": "auth-code",
    -        })
    -        handler.wfile = BytesIO()
    -        responses = []
    -        headers = []
    -        handler.send_response = lambda code: responses.append(code)
    -        handler.send_header = lambda key, value: headers.append((key, value))
    -        handler.end_headers = lambda: None
    -
    -        handler.do_GET()
    -
    -        assert responses == [200]
    -        assert handler_cls.captured_code == "auth-code"
    -        assert handler_cls.captured_error is None
    -        assert handler_cls.ready.is_set()
    -        assert "captured_code" not in handler.__dict__
    -
    -
    -class TestAntigravityModelCatalog:
    -    def test_parse_agent_model_ids_prefers_recommended_group(self):
    -        from agent.antigravity_code_assist import parse_agent_model_ids
    -
    -        payload = {
    -            "defaultAgentModelId": "gemini-3-flash-agent",
    -            "agentModelSorts": [
    -                {
    -                    "displayName": "Experimental",
    -                    "modelIds": ["tab_flash_lite_preview", "chat_23310"],
    -                },
    -                {
    -                    "displayName": "Recommended",
    -                    "modelIds": [
    -                        "gemini-3-flash-agent",
    -                        "gemini-3.5-flash-low",
    -                        "gemini-3.1-pro-high",
    -                        "gemini-pro-agent",
    -                        "claude-sonnet-4-6",
    -                    ],
    -                },
    -            ],
    -            "models": [{"id": "gpt-oss-120b-medium"}],
    -        }
    -
    -        assert parse_agent_model_ids(payload) == [
    -            "gemini-3-flash-agent",
    -            "gemini-3.5-flash-low",
    -            "gemini-pro-agent",
    -            "claude-sonnet-4-6",
    -        ]
    -
    -    def test_headers_include_antigravity_metadata(self):
    -        from agent.antigravity_code_assist import build_headers
    -
    -        headers = build_headers("tok")
    -        assert headers["Authorization"] == "Bearer tok"
    -        assert headers["User-Agent"].startswith("antigravity/")
    -        assert headers["X-Goog-Api-Client"] == "google-cloud-sdk vscode_cloudshelleditor/0.1"
    -        metadata = json.loads(headers["Client-Metadata"])
    -        assert metadata["ideType"] == "ANTIGRAVITY"
    -        assert metadata["platform"] == "PLATFORM_UNSPECIFIED"
    -
    -
    -class TestAntigravityClient:
    -    def test_client_exposes_openai_interface(self):
    -        from agent.antigravity_cloudcode_adapter import AntigravityCloudCodeClient
    -
    -        client = AntigravityCloudCodeClient(api_key="dummy")
    -        try:
    -            assert hasattr(client, "chat")
    -            assert hasattr(client.chat, "completions")
    -            assert callable(client.chat.completions.create)
    -        finally:
    -            client.close()
    -
    -    def test_create_uses_antigravity_endpoint_and_headers(self, monkeypatch):
    -        from agent import antigravity_oauth
    -        from agent.antigravity_cloudcode_adapter import AntigravityCloudCodeClient
    -        from agent.antigravity_code_assist import ANTIGRAVITY_CODE_ASSIST_ENDPOINT
    -
    -        monkeypatch.setattr(antigravity_oauth, "get_valid_access_token", lambda: "live-token")
    -
    -        class _Response:
    -            status_code = 200
    -
    -            def json(self):
    -                return {
    -                    "response": {
    -                        "candidates": [{
    -                            "content": {"parts": [{"text": "ok"}]},
    -                            "finishReason": "STOP",
    -                        }]
    -                    }
    -                }
    -
    -        class _Http:
    -            def __init__(self):
    -                self.calls = []
    -
    -            def post(self, url, json=None, headers=None):
    -                self.calls.append((url, json, headers))
    -                return _Response()
    -
    -            def close(self):
    -                pass
    -
    -        client = AntigravityCloudCodeClient(project_id="proj-123")
    -        client._http = _Http()
    -        try:
    -            result = client.chat.completions.create(
    -                model="gemini-3-flash-agent",
    -                messages=[{"role": "user", "content": "hi"}],
    -            )
    -        finally:
    -            client.close()
    -
    -        assert result.choices[0].message.content == "ok"
    -        url, body, headers = client._http.calls[0]
    -        assert url == f"{ANTIGRAVITY_CODE_ASSIST_ENDPOINT}/v1internal:generateContent"
    -        assert body["project"] == "proj-123"
    -        assert body["model"] == "gemini-3-flash-agent"
    -        assert headers["Authorization"] == "Bearer live-token"
    -        assert json.loads(headers["Client-Metadata"])["ideType"] == "ANTIGRAVITY"
    -
    -
    -class TestAntigravityRegistration:
    -    def test_registry_entry_and_aliases(self):
    -        from hermes_cli.auth import PROVIDER_REGISTRY, resolve_provider
    -
    -        assert "google-antigravity" in PROVIDER_REGISTRY
    -        assert PROVIDER_REGISTRY["google-antigravity"].auth_type == "oauth_external"
    -        assert resolve_provider("antigravity") == "google-antigravity"
    -        assert resolve_provider("antigravity-oauth") == "google-antigravity"
    -        assert resolve_provider("google-antigravity-oauth") == "google-antigravity"
    -        assert resolve_provider("agy") == "google-antigravity"
    -
    -    def test_runtime_provider_raises_when_not_logged_in(self):
    -        from hermes_cli.auth import AuthError
    -        from hermes_cli.runtime_provider import resolve_runtime_provider
    -
    -        with pytest.raises(AuthError) as exc_info:
    -            resolve_runtime_provider(requested="google-antigravity")
    -        assert exc_info.value.code == "antigravity_oauth_not_logged_in"
    -
    -    def test_runtime_provider_returns_correct_shape_when_logged_in(self):
    -        from agent.antigravity_oauth import AntigravityCredentials, save_credentials
    -        from hermes_cli.runtime_provider import resolve_runtime_provider
    -
    -        save_credentials(AntigravityCredentials(
    -            access_token="[redacted:secret-assignment]",
    -            refresh_token="rt",
    -            expires_ms=int((time.time() + 3600) * 1000),
    -            project_id="my-proj",
    -            email="[redacted:email]",
    -        ))
    -
    -        result = resolve_runtime_provider(requested="google-antigravity")
    -        assert result["provider"] == "google-antigravity"
    -        assert result["api_mode"] == "chat_completions"
    -        assert result["api_key"] == "live-tok"
    -        assert result["base_url"] == "antigravity-pa://google"
    -        assert result["project_id"] == "my-proj"
    -        assert result["email"] == "[redacted:email]"
    -
    -    def test_provider_model_ids_uses_live_antigravity_catalog(self, monkeypatch):
    -        from hermes_cli import models
    -
    -        monkeypatch.setattr(
    -            models,
    -            "_fetch_antigravity_models",
    -            lambda force_refresh=False: ["gemini-3-flash-agent", "claude-sonnet-4-6"],
    -        )
    -
    -        assert models.provider_model_ids("agy") == [
    -            "gemini-3-flash-agent",
    -            "claude-sonnet-4-6",
    -        ]
    -
    -    def test_oauth_capable_set_includes_antigravity(self):
    -        from hermes_cli.auth_commands import _OAUTH_CAPABLE_PROVIDERS
    -
    -        assert "google-antigravity" in _OAUTH_CAPABLE_PROVIDERS
    ```
  - `/dev/null`

    ```diff
    @@ -1,1228 +0,0 @@
    -"""Tests for the google-gemini-cli OAuth + Code Assist inference provider.
    -
    -Covers:
    -- agent/google_oauth.py — PKCE, credential I/O with packed refresh format,
    -  token refresh dedup, invalid_grant handling, headless paste fallback
    -- agent/google_code_assist.py — project discovery, VPC-SC fallback, onboarding
    -  with LRO polling, quota retrieval
    -- agent/gemini_cloudcode_adapter.py — OpenAI↔Gemini translation, request
    -  envelope wrapping, response unwrapping, tool calls bidirectional, streaming
    -- Provider registration — registry entry, aliases, runtime dispatch, auth
    -  status, _OAUTH_CAPABLE_PROVIDERS regression guard
    -"""
    -from __future__ import annotations
    -
    -import base64
    -import hashlib
    -import json
    -import stat
    -import time
    -from pathlib import Path
    -
    -import pytest
    -
    -
    -# =============================================================================
    -# Fixtures
    -# =============================================================================
    -
    [redacted:email](autouse=True)
    -def _isolate_env(monkeypatch, tmp_path):
    -    home = tmp_path / ".hermes"
    -    home.mkdir(parents=True)
    -    monkeypatch.setattr(Path, "home", lambda: tmp_path)
    -    monkeypatch.setenv("HERMES_HOME", str(home))
    -    for key in (
    -        "HERMES_GEMINI_CLIENT_ID",
    -        "HERMES_GEMINI_CLIENT_SECRET",
    -        "HERMES_GEMINI_PROJECT_ID",
    -        "GOOGLE_CLOUD_PROJECT",
    -        "GOOGLE_CLOUD_PROJECT_ID",
    -        "SSH_CONNECTION",
    -        "SSH_CLIENT",
    -        "SSH_TTY",
    -        "HERMES_HEADLESS",
    -    ):
    -        monkeypatch.delenv(key, raising=False)
    -    return home
    -
    -
    -# =============================================================================
    -# google_oauth.py — PKCE + packed refresh format
    -# =============================================================================
    -
    -class TestPkce:
    -    def test_verifier_and_challenge_s256_roundtrip(self):
    -        from agent.google_oauth import _generate_pkce_pair
    -
    -        verifier, challenge = _generate_pkce_pair()
    -        expected = base64.urlsafe_b64encode(
    -            hashlib.sha256(verifier.encode("ascii")).digest()
    -        ).rstrip(b"=").decode("ascii")
    -        assert challenge == expected
    -        assert 43 <= len(verifier) <= 128
    -
    -
    -class TestRefreshParts:
    -    def test_parse_bare_token(self):
    -        from agent.google_oauth import RefreshParts
    -
    -        p = RefreshParts.parse("abc-token")
    -        assert p.refresh_token == "abc-token"
    -        assert p.project_id == ""
    -        assert p.managed_project_id == ""
    -
    -    def test_parse_packed(self):
    -        from agent.google_oauth import RefreshParts
    -
    -        p = RefreshParts.parse("rt|proj-123|mgr-456")
    -        assert p.refresh_token == "rt"
    -        assert p.project_id == "proj-123"
    -        assert p.managed_project_id == "mgr-456"
    -
    -    def test_format_bare_token(self):
    -        from agent.google_oauth import RefreshParts
    -
    -        assert RefreshParts(refresh_token="rt").format() == "rt"
    -
    -    def test_format_with_project(self):
    -        from agent.google_oauth import RefreshParts
    -
    -        packed = RefreshParts(
    -            refresh_token="rt", project_id="p1", managed_project_id="m1",
    -        ).format()
    -        assert packed == "rt|p1|m1"
    -        # Roundtrip
    -        parsed = RefreshParts.parse(packed)
    -        assert parsed.refresh_token == "rt"
    -        assert parsed.project_id == "p1"
    -        assert parsed.managed_project_id == "m1"
    -
    -    def test_format_empty_refresh_token_returns_empty(self):
    -        from agent.google_oauth import RefreshParts
    -
    -        assert RefreshParts(refresh_token="").format() == ""
    -
    -
    -class TestClientCredResolution:
    -    def test_env_override(self, monkeypatch):
    -        from agent.google_oauth import _get_client_id
    -
    -        monkeypatch.setenv("HERMES_GEMINI_CLIENT_ID", "custom-id.apps.googleusercontent.com")
    -        assert _get_client_id() == "custom-id.apps.googleusercontent.com"
    -
    -    def test_shipped_default_used_when_no_env(self):
    -        """Out of the box, the public gemini-cli desktop client is used."""
    -        from agent.google_oauth import _get_client_id, _DEFAULT_CLIENT_ID
    -
    -        # Confirmed PUBLIC: baked into Google's open-source gemini-cli
    -        assert _DEFAULT_CLIENT_ID.endswith(".apps.googleusercontent.com")
    -        assert _DEFAULT_CLIENT_ID.startswith("681255809395-")
    -        assert _get_client_id() == _DEFAULT_CLIENT_ID
    -
    -    def test_shipped_default_secret_present(self):
    -        from agent.google_oauth import _DEFAULT_CLIENT_SECRET, _get_client_secret
    -
    -        assert _DEFAULT_CLIENT_SECRET.startswith("GOCSPX-")
    -        assert len(_DEFAULT_CLIENT_SECRET) >= 20
    -        assert _get_client_secret() == _DEFAULT_CLIENT_SECRET
    -
    -    def test_falls_back_to_scrape_when_defaults_wiped(self, tmp_path, monkeypatch):
    -        """Forks that wipe the shipped defaults should still work with gemini-cli."""
    -        from agent import google_oauth
    -
    -        monkeypatch.setattr(google_oauth, "_DEFAULT_CLIENT_ID", "")
    -        monkeypatch.setattr(google_oauth, "_DEFAULT_CLIENT_SECRET", "")
    -
    -        fake_bin = tmp_path / "bin" / "gemini"
    -        fake_bin.parent.mkdir(parents=True)
    -        fake_bin.write_text("#!/bin/sh\n")
    -        oauth_dir = tmp_path / "node_modules" / "@google" / "gemini-cli-core" / "dist" / "src" / "code_assist"
    -        oauth_dir.mkdir(parents=True)
    -        (oauth_dir / "oauth2.js").write_text(
    -            'const OAUTH_CLIENT_ID = "99999-fakescrapedxyz.apps.googleusercontent.com";\n'
    -            'const OAUTH_CLIENT_SECRET = "[redacted:secret-assignment]";\n'
    -        )
    -
    -        monkeypatch.setattr("shutil.which", lambda _: str(fake_bin))
    -        google_oauth._scraped_creds_cache.clear()
    -
    -        assert google_oauth._get_client_id().startswith("99999-")
    -
    -    def test_missing_everything_raises_with_install_hint(self, monkeypatch):
    -        """When env + defaults + scrape all fail, raise with install instructions."""
    -        from agent import google_oauth
    -
    -        monkeypatch.setattr(google_oauth, "_DEFAULT_CLIENT_ID", "")
    -        monkeypatch.setattr(google_oauth, "_DEFAULT_CLIENT_SECRET", "")
    -        google_oauth._scraped_creds_cache.clear()
    -        monkeypatch.setattr("shutil.which", lambda _: None)
    -
    -        with pytest.raises(google_oauth.GoogleOAuthError) as exc_info:
    -            google_oauth._require_client_id()
    -        assert exc_info.value.code == "google_oauth_client_id_missing"
    -
    -    def test_locate_gemini_cli_oauth_js_when_absent(self, monkeypatch):
    -        from agent import google_oauth
    -
    -        monkeypatch.setattr("shutil.which", lambda _: None)
    -        assert google_oauth._locate_gemini_cli_oauth_js() is None
    -
    -    def test_scrape_client_credentials_parses_id_and_secret(self, tmp_path, monkeypatch):
    -        from agent import google_oauth
    -
    -        # Create a fake gemini binary and oauth2.js
    -        fake_gemini_bin = tmp_path / "bin" / "gemini"
    -        fake_gemini_bin.parent.mkdir(parents=True)
    -        fake_gemini_bin.write_text("#!/bin/sh\necho gemini\n")
    -
    -        oauth_js_dir = tmp_path / "node_modules" / "@google" / "gemini-cli-core" / "dist" / "src" / "code_assist"
    -        oauth_js_dir.mkdir(parents=True)
    -        oauth_js = oauth_js_dir / "oauth2.js"
    -        # Synthesize a harmless test fingerprint (valid shape, obvious test values)
    -        oauth_js.write_text(
    -            'const OAUTH_CLIENT_ID = "12345678-testfakenotrealxyz.apps.googleusercontent.com";\n'
    -            'const OAUTH_CLIENT_SECRET = "[redacted:secret-assignment]";\n'
    -        )
    -
    -        monkeypatch.setattr("shutil.which", lambda _: str(fake_gemini_bin))
    -        google_oauth._scraped_creds_cache.clear()
    -
    -        cid, cs = google_oauth._scrape_client_credentials()
    -        assert cid == "12345678-testfakenotrealxyz.apps.googleusercontent.com"
    -        assert cs.startswith("GOCSPX-")
    -
    -
    -class TestCredentialIo:
    -    def _make(self):
    -        from agent.google_oauth import GoogleCredentials
    -
    -        return GoogleCredentials(
    -            access_token="at-1",
    -            refresh_token="rt-1",
    -            expires_ms=int((time.time() + 3600) * 1000),
    -            email="[redacted:email]",
    -            project_id="proj-abc",
    -        )
    -
    -    def test_save_and_load_packed_refresh(self):
    -        from agent.google_oauth import load_credentials, save_credentials
    -
    -        creds = self._make()
    -        save_credentials(creds)
    -        loaded = load_credentials()
    -        assert loaded is not None
    -        assert loaded.refresh_token == "rt-1"
    -        assert loaded.project_id == "proj-abc"
    -
    -    def test_save_uses_0600_permissions(self):
    -        from agent.google_oauth import _credentials_path, save_credentials
    -
    -        save_credentials(self._make())
    -        mode = stat.S_IMODE(_credentials_path().stat().st_mode)
    -        assert mode == 0o600
    -
    -    def test_disk_format_is_packed(self):
    -        from agent.google_oauth import _credentials_path, save_credentials
    -
    -        save_credentials(self._make())
    -        data = json.loads(_credentials_path().read_text())
    -        # The refresh field on disk is the packed string, not a dict
    -        assert data["refresh"] == "rt-1|proj-abc|"
    -
    -    def test_update_project_ids(self):
    -        from agent.google_oauth import (
    -            load_credentials, save_credentials, update_project_ids,
    -        )
    -        from agent.google_oauth import GoogleCredentials
    -
    -        save_credentials(GoogleCredentials(
    -            access_token="at", refresh_token="rt",
    -            expires_ms=int((time.time() + 3600) * 1000),
    -        ))
    -        update_project_ids(project_id="new-proj", managed_project_id="mgr-xyz")
    -
    -        loaded = load_credentials()
    -        assert loaded.project_id == "new-proj"
    -        assert loaded.managed_project_id == "mgr-xyz"
    -
    -
    -class TestAccessTokenExpired:
    -    def test_fresh_token_not_expired(self):
    -        from agent.google_oauth import GoogleCredentials
    -
    -        creds = GoogleCredentials(
    -            access_token="at", refresh_token="rt",
    -            expires_ms=int((time.time() + 3600) * 1000),
    -        )
    -        assert creds.access_token_expired() is False
    -
    -    def test_near_expiry_considered_expired(self):
    -        """60s skew — a token with 30s left is considered expired."""
    -        from agent.google_oauth import GoogleCredentials
    -
    -        creds = GoogleCredentials(
    -            access_token="at", refresh_token="rt",
    -            expires_ms=int((time.time() + 30) * 1000),
    -        )
    -        assert creds.access_token_expired() is True
    -
    -    def test_no_token_is_expired(self):
    -        from agent.google_oauth import GoogleCredentials
    -
    -        creds = GoogleCredentials(
    -            access_token="", refresh_token="rt", expires_ms=999999999,
    -        )
    -        assert creds.access_token_expired() is True
    -
    -
    -class TestGetValidAccessToken:
    -    def _save(self, **over):
    -        from agent.google_oauth import GoogleCredentials, save_credentials
    -
    -        defaults = {
    -            "access_token": "at",
    -            "refresh_token": "rt",
    -            "expires_ms": int((time.time() + 3600) * 1000),
    -        }
    -        defaults.update(over)
    -        save_credentials(GoogleCredentials(**defaults))
    -
    -    def test_returns_cached_when_fresh(self):
    -        from agent.google_oauth import get_valid_access_token
    -
    -        self._save(access_token="[redacted:secret-assignment]")
    -        assert get_valid_access_token() == "cached-token"
    -
    -    def test_refreshes_when_near_expiry(self, monkeypatch):
    -        from agent import google_oauth
    -
    -        self._save(expires_ms=int((time.time() + 30) * 1000))
    -        monkeypatch.setattr(
    -            google_oauth, "_post_form",
    -            lambda *a, **kw: {"access_token": "refreshed", "expires_in": 3600},
    -        )
    -        assert google_oauth.get_valid_access_token() == "refreshed"
    -
    -    def test_invalid_grant_clears_credentials(self, monkeypatch):
    -        from agent import google_oauth
    -
    -        self._save(expires_ms=int((time.time() - 10) * 1000))
    -
    -        def boom(*a, **kw):
    -            raise google_oauth.GoogleOAuthError(
    -                "invalid_grant", code="google_oauth_invalid_grant",
    -            )
    -
    -        monkeypatch.setattr(google_oauth, "_post_form", boom)
    -
    -        with pytest.raises(google_oauth.GoogleOAuthError) as exc_info:
    -            google_oauth.get_valid_access_token()
    -        assert exc_info.value.code == "google_oauth_invalid_grant"
    -        # Credentials should be wiped
    -        assert google_oauth.load_credentials() is None
    -
    -    def test_preserves_refresh_when_google_omits(self, monkeypatch):
    -        from agent import google_oauth
    -
    -        self._save(expires_ms=int((time.time() + 30) * 1000), refresh_token="[redacted:secret-assignment]")
    -        monkeypatch.setattr(
    -            google_oauth, "_post_form",
    -            lambda *a, **kw: {"access_token": "new", "expires_in": 3600},
    -        )
    -        google_oauth.get_valid_access_token()
    -        assert google_oauth.load_credentials().refresh_token == "original-rt"
    -
    -
    -class TestProjectIdResolution:
    -    @pytest.mark.parametrize("env_var", [
    -        "HERMES_GEMINI_PROJECT_ID",
    -        "GOOGLE_CLOUD_PROJECT",
    -        "GOOGLE_CLOUD_PROJECT_ID",
    -    ])
    -    def test_env_vars_checked(self, monkeypatch, env_var):
    -        from agent.google_oauth import resolve_project_id_from_env
    -
    -        monkeypatch.setenv(env_var, "test-proj")
    -        assert resolve_project_id_from_env() == "test-proj"
    -
    -    def test_priority_order(self, monkeypatch):
    -        from agent.google_oauth import resolve_project_id_from_env
    -
    -        monkeypatch.setenv("GOOGLE_CLOUD_PROJECT", "lower-priority")
    -        monkeypatch.setenv("HERMES_GEMINI_PROJECT_ID", "higher-priority")
    -        assert resolve_project_id_from_env() == "higher-priority"
    -
    -    def test_no_env_returns_empty(self):
    -        from agent.google_oauth import resolve_project_id_from_env
    -
    -        assert resolve_project_id_from_env() == ""
    -
    -
    -class TestHeadlessDetection:
    -    def test_detects_ssh(self, monkeypatch):
    -        from agent.google_oauth import _is_headless
    -
    -        monkeypatch.setenv("SSH_CONNECTION", "1.2.3.4 22 5.6.7.8 9876")
    -        assert _is_headless() is True
    -
    -    def test_detects_hermes_headless(self, monkeypatch):
    -        from agent.google_oauth import _is_headless
    -
    -        monkeypatch.setenv("HERMES_HEADLESS", "1")
    -        assert _is_headless() is True
    -
    -    def test_default_not_headless(self):
    -        from agent.google_oauth import _is_headless
    -
    -        assert _is_headless() is False
    -
    -
    -# =============================================================================
    -# google_code_assist.py — project discovery, onboarding, quota, VPC-SC
    -# =============================================================================
    -
    -class TestCodeAssistVpcScDetection:
    -    def test_detects_vpc_sc_in_json(self):
    -        from agent.google_code_assist import _is_vpc_sc_violation
    -
    -        body = json.dumps({
    -            "error": {
    -                "details": [{"reason": "SECURITY_POLICY_VIOLATED"}],
    -                "message": "blocked by policy",
    -            }
    -        })
    -        assert _is_vpc_sc_violation(body) is True
    -
    -    def test_detects_vpc_sc_in_message(self):
    -        from agent.google_code_assist import _is_vpc_sc_violation
    -
    -        body = '{"error": {"message": "SECURITY_POLICY_VIOLATED"}}'
    -        assert _is_vpc_sc_violation(body) is True
    -
    -    def test_non_vpc_sc_returns_false(self):
    -        from agent.google_code_assist import _is_vpc_sc_violation
    -
    -        assert _is_vpc_sc_violation('{"error": {"message": "not found"}}') is False
    -        assert _is_vpc_sc_violation("") is False
    -
    -
    -class TestLoadCodeAssist:
    -    def test_parses_response(self, monkeypatch):
    -        from agent import google_code_assist
    -
    -        fake = {
    -            "currentTier": {"id": "free-tier"},
    -            "cloudaicompanionProject": "proj-123",
    -            "allowedTiers": [{"id": "free-tier"}, {"id": "standard-tier"}],
    -        }
    -        monkeypatch.setattr(google_code_assist, "_post_json", lambda *a, **kw: fake)
    -
    -        info = google_code_assist.load_code_assist("access-token")
    -        assert info.current_tier_id == "free-tier"
    -        assert info.cloudaicompanion_project == "proj-123"
    -        assert "free-tier" in info.allowed_tiers
    -        assert "standard-tier" in info.allowed_tiers
    -
    -    def test_vpc_sc_forces_standard_tier(self, monkeypatch):
    -        from agent import google_code_assist
    -
    -        def boom(*a, **kw):
    -            raise google_code_assist.CodeAssistError(
    -                "VPC-SC policy violation", code="code_assist_vpc_sc",
    -            )
    -
    -        monkeypatch.setattr(google_code_assist, "_post_json", boom)
    -
    -        info = google_code_assist.load_code_assist("access-token", project_id="corp-proj")
    -        assert info.current_tier_id == "standard-tier"
    -        assert info.cloudaicompanion_project == "corp-proj"
    -
    -
    -class TestOnboardUser:
    -    def test_paid_tier_requires_project_id(self):
    -        from agent import google_code_assist
    -
    -        with pytest.raises(google_code_assist.ProjectIdRequiredError):
    -            google_code_assist.onboard_user(
    -                "at", tier_id="standard-tier", project_id="",
    -            )
    -
    -    def test_free_tier_no_project_required(self, monkeypatch):
    -        from agent import google_code_assist
    -
    -        monkeypatch.setattr(
    -            google_code_assist, "_post_json",
    -            lambda *a, **kw: {"done": True, "response": {"cloudaicompanionProject": "gen-123"}},
    -        )
    -        resp = google_code_assist.onboard_user("at", tier_id="free-tier")
    -        assert resp["done"] is True
    -
    -    def test_lro_polling(self, monkeypatch):
    -        """Simulate a long-running operation that completes on the second poll."""
    -        from agent import google_code_assist
    -
    -        call_count = {"n": 0}
    -
    -        def fake_post(url, body, token, **kw):
    -            call_count["n"] += 1
    -            if call_count["n"] == 1:
    -                return {"name": "operations/op-abc", "done": False}
    -            return {"name": "operations/op-abc", "done": True, "response": {}}
    -
    -        monkeypatch.setattr(google_code_assist, "_post_json", fake_post)
    -        monkeypatch.setattr(google_code_assist.time, "sleep", lambda *_: None)
    -
    -        resp = google_code_assist.onboard_user(
    -            "at", tier_id="free-tier",
    -        )
    -        assert resp["done"] is True
    -        assert call_count["n"] >= 2
    -
    -
    -class TestRetrieveUserQuota:
    -    def test_parses_buckets(self, monkeypatch):
    -        from agent import google_code_assist
    -
    -        fake = {
    -            "buckets": [
    -                {
    -                    "modelId": "gemini-2.5-pro",
    -                    "tokenType": "input",
    -                    "remainingFraction": 0.75,
    -                    "resetTime": "2026-04-17T00:00:00Z",
    -                },
    -                {
    -                    "modelId": "gemini-2.5-flash",
    -                    "remainingFraction": 0.9,
    -                },
    -            ]
    -        }
    -        monkeypatch.setattr(google_code_assist, "_post_json", lambda *a, **kw: fake)
    -
    -        buckets = google_code_assist.retrieve_user_quota("at", project_id="p1")
    -        assert len(buckets) == 2
    -        assert buckets[0].model_id == "gemini-2.5-pro"
    -        assert buckets[0].remaining_fraction == 0.75
    -        assert buckets[1].remaining_fraction == 0.9
    -
    -
    -class TestResolveProjectContext:
    -    def test_configured_shortcircuits(self, monkeypatch):
    -        from agent.google_code_assist import resolve_project_context
    -
    -        # Should NOT call loadCodeAssist when configured_project_id is set
    -        def should_not_be_called(*a, **kw):
    -            raise AssertionError("should short-circuit")
    -
    -        monkeypatch.setattr(
    -            "agent.google_code_assist._post_json", should_not_be_called,
    -        )
    -        ctx = resolve_project_context("at", configured_project_id="proj-abc")
    -        assert ctx.project_id == "proj-abc"
    -        assert ctx.source == "config"
    -
    -    def test_env_shortcircuits(self, monkeypatch):
    -        from agent.google_code_assist import resolve_project_context
    -
    -        monkeypatch.setattr(
    -            "agent.google_code_assist._post_json",
    -            lambda *a, **kw: (_ for _ in ()).throw(AssertionError("nope")),
    -        )
    -        ctx = resolve_project_context("at", env_project_id="env-proj")
    -        assert ctx.project_id == "env-proj"
    -        assert ctx.source == "env"
    -
    -    def test_discovers_via_load_code_assist(self, monkeypatch):
    -        from agent import google_code_assist
    -
    -        monkeypatch.setattr(
    -            google_code_assist, "_post_json",
    -            lambda *a, **kw: {
    -                "currentTier": {"id": "free-tier"},
    -                "cloudaicompanionProject": "discovered-proj",
    -            },
    -        )
    -        ctx = google_code_assist.resolve_project_context("at")
    -        assert ctx.project_id == "discovered-proj"
    -        assert ctx.tier_id == "free-tier"
    -        assert ctx.source == "discovered"
    -
    -
    -# =============================================================================
    -# gemini_cloudcode_adapter.py — request/response translation
    -# =============================================================================
    -
    -class TestBuildGeminiRequest:
    -    def test_user_assistant_messages(self):
    -        from agent.gemini_cloudcode_adapter import build_gemini_request
    -
    -        req = build_gemini_request(messages=[
    -            {"role": "user", "content": "hi"},
    -            {"role": "assistant", "content": "hello"},
    -        ])
    -        assert req["contents"][0] == {
    -            "role": "user", "parts": [{"text": "hi"}],
    -        }
    -        assert req["contents"][1] == {
    -            "role": "model", "parts": [{"text": "hello"}],
    -        }
    -
    -    def test_system_instruction_separated(self):
    -        from agent.gemini_cloudcode_adapter import build_gemini_request
    -
    -        req = build_gemini_request(messages=[
    -            {"role": "system", "content": "You are helpful"},
    -            {"role": "user", "content": "hi"},
    -        ])
    -        assert req["systemInstruction"]["parts"][0]["text"] == "You are helpful"
    -        # System should NOT appear in contents
    -        assert all(c["role"] != "system" for c in req["contents"])
    -
    -    def test_multiple_system_messages_joined(self):
    -        from agent.gemini_cloudcode_adapter import build_gemini_request
    -
    -        req = build_gemini_request(messages=[
    -            {"role": "system", "content": "A"},
    -            {"role": "system", "content": "B"},
    -            {"role": "user", "content": "hi"},
    -        ])
    -        assert "A\nB" in req["systemInstruction"]["parts"][0]["text"]
    -
    -    def test_tool_call_translation(self):
    -        from agent.gemini_cloudcode_adapter import build_gemini_request
    -
    -        req = build_gemini_request(messages=[
    -            {"role": "user", "content": "what's the weather?"},
    -            {
    -                "role": "assistant",
    -                "content": None,
    -                "tool_calls": [{
    -                    "id": "call_1",
    -                    "type": "function",
    -                    "function": {"name": "get_weather", "arguments": '{"city": "SF"}'},
    -                }],
    -            },
    -        ])
    -        # Assistant turn should have a functionCall part
    -        model_turn = req["contents"][1]
    -        assert model_turn["role"] == "model"
    -        fc_part = next(p for p in model_turn["parts"] if "functionCall" in p)
    -        assert fc_part["functionCall"]["name"] == "get_weather"
    -        assert fc_part["functionCall"]["args"] == {"city": "SF"}
    -        assert fc_part["functionCall"]["id"] == "call_1"
    -
    -    def test_tool_result_translation(self):
    -        from agent.gemini_cloudcode_adapter import build_gemini_request
    -
    -        req = build_gemini_request(messages=[
    -            {"role": "user", "content": "q"},
    -            {"role": "assistant", "tool_calls": [{
    -                "id": "c1", "type": "function",
    -                "function": {"name": "get_weather", "arguments": "{}"},
    -            }]},
    -            {
    -                "role": "tool",
    -                "name": "get_weather",
    -                "tool_call_id": "c1",
    -                "content": '{"temp": 72}',
    -            },
    -        ])
    -        # Last content turn should carry functionResponse
    -        last = req["contents"][-1]
    -        fr_part = next(p for p in last["parts"] if "functionResponse" in p)
    -        assert fr_part["functionResponse"]["name"] == "get_weather"
    -        assert fr_part["functionResponse"]["response"] == {"temp": 72}
    -        assert fr_part["functionResponse"]["id"] == "c1"
    -
    -    def test_tools_translated_to_function_declarations(self):
    -        from agent.gemini_cloudcode_adapter import build_gemini_request
    -
    -        req = build_gemini_request(
    -            messages=[{"role": "user", "content": "hi"}],
    -            tools=[
    -                {"type": "function", "function": {
    -                    "name": "fn1", "description": "foo",
    -                    "parameters": {"type": "object"},
    -                }},
    -            ],
    -        )
    -        decls = req["tools"][0]["functionDeclarations"]
    -        assert decls[0]["name"] == "fn1"
    -        assert decls[0]["description"] == "foo"
    -        assert decls[0]["parameters"] == {"type": "object"}
    -
    -    def test_tools_strip_json_schema_only_fields_from_parameters(self):
    -        from agent.gemini_cloudcode_adapter import build_gemini_request
    -
    -        req = build_gemini_request(
    -            messages=[{"role": "user", "content": "hi"}],
    -            tools=[
    -                {"type": "function", "function": {
    -                    "name": "fn1",
    -                    "description": "foo",
    -                    "parameters": {
    -                        "$schema": "https://json-schema.org/draft/2020-12/schema",
    -                        "type": "object",
    -                        "additionalProperties": False,
    -                        "properties": {
    -                            "city": {
    -                                "type": "string",
    -                                "$schema": "ignored",
    -                                "description": "City name",
    -                                "additionalProperties": False,
    -                            }
    -                        },
    -                        "required": ["city"],
    -                    },
    -                }},
    -            ],
    -        )
    -        params = req["tools"][0]["functionDeclarations"][0]["parameters"]
    -        assert "$schema" not in params
    -        assert "additionalProperties" not in params
    -        assert params["type"] == "object"
    -        assert params["required"] == ["city"]
    -        assert params["properties"]["city"] == {
    -            "type": "string",
    -            "description": "City name",
    -        }
    -
    -    def test_tool_choice_auto(self):
    -        from agent.gemini_cloudcode_adapter import build_gemini_request
    -
    -        req = build_gemini_request(
    -            messages=[{"role": "user", "content": "hi"}],
    -            tool_choice="auto",
    -        )
    -        assert req["toolConfig"]["functionCallingConfig"]["mode"] == "AUTO"
    -
    -    def test_tool_choice_required(self):
    -        from agent.gemini_cloudcode_adapter import build_gemini_request
    -
    -        req = build_gemini_request(
    -            messages=[{"role": "user", "content": "hi"}],
    -            tool_choice="required",
    -        )
    -        assert req["toolConfig"]["functionCallingConfig"]["mode"] == "ANY"
    -
    -    def test_tool_choice_specific_function(self):
    -        from agent.gemini_cloudcode_adapter import build_gemini_request
    -
    -        req = build_gemini_request(
    -            messages=[{"role": "user", "content": "hi"}],
    -            tool_choice={"type": "function", "function": {"name": "my_fn"}},
    -        )
    -        cfg = req["toolConfig"]["functionCallingConfig"]
    -        assert cfg["mode"] == "ANY"
    -        assert cfg["allowedFunctionNames"] == ["my_fn"]
    -
    -    def test_generation_config_params(self):
    -        from agent.gemini_cloudcode_adapter import build_gemini_request
    -
    -        req = build_gemini_request(
    -            messages=[{"role": "user", "content": "hi"}],
    -            temperature=0.7,
    -            max_tokens=512,
    -            top_p=0.9,
    -            stop=["###", "END"],
    -        )
    -        gc = req["generationConfig"]
    -        assert gc["temperature"] == 0.7
    -        assert gc["maxOutputTokens"] == 512
    -        assert gc["topP"] == 0.9
    -        assert gc["stopSequences"] == ["###", "END"]
    -
    -    def test_thinking_config_normalization(self):
    -        from agent.gemini_cloudcode_adapter import build_gemini_request
    -
    -        req = build_gemini_request(
    -            messages=[{"role": "user", "content": "hi"}],
    -            thinking_config={"thinking_budget": 1024, "include_thoughts": True},
    -        )
    -        tc = req["generationConfig"]["thinkingConfig"]
    -        assert tc["thinkingBudget"] == 1024
    -        assert tc["includeThoughts"] is True
    -
    -
    -class TestWrapCodeAssistRequest:
    -    def test_envelope_shape(self):
    -        from agent.gemini_cloudcode_adapter import wrap_code_assist_request
    -
    -        inner = {"contents": [], "generationConfig": {}}
    -        wrapped = wrap_code_assist_request(
    -            project_id="p1", model="gemini-2.5-pro", inner_request=inner,
    -        )
    -        assert wrapped["project"] == "p1"
    -        assert wrapped["model"] == "gemini-2.5-pro"
    -        assert wrapped["request"] is inner
    -        assert "user_prompt_id" in wrapped
    -        assert len(wrapped["user_prompt_id"]) > 10
    -
    -
    -class TestTranslateGeminiResponse:
    -    def test_text_response(self):
    -        from agent.gemini_cloudcode_adapter import _translate_gemini_response
    -
    -        resp = {
    -            "response": {
    -                "candidates": [{
    -                    "content": {"parts": [{"text": "hello world"}]},
    -                    "finishReason": "STOP",
    -                }],
    -                "usageMetadata": {
    -                    "promptTokenCount": 10,
    -                    "candidatesTokenCount": 5,
    -                    "totalTokenCount": 15,
    -                },
    -            }
    -        }
    -        result = _translate_gemini_response(resp, model="gemini-2.5-flash")
    -        assert result.choices[0].message.content == "hello world"
    -        assert result.choices[0].message.tool_calls is None
    -        assert result.choices[0].finish_reason == "stop"
    -        assert result.usage.prompt_tokens == 10
    -        assert result.usage.completion_tokens == 5
    -        assert result.usage.total_tokens == 15
    -
    -    def test_function_call_response(self):
    -        from agent.gemini_cloudcode_adapter import _translate_gemini_response
    -
    -        resp = {
    -            "response": {
    -                "candidates": [{
    -                    "content": {"parts": [{
    -                        "functionCall": {"name": "lookup", "args": {"q": "weather"}, "id": "provider-call-1"},
    -                    }]},
    -                    "finishReason": "STOP",
    -                }],
    -            }
    -        }
    -        result = _translate_gemini_response(resp, model="gemini-2.5-flash")
    -        tc = result.choices[0].message.tool_calls[0]
    -        assert tc.id == "provider-call-1"
    -        assert tc.function.name == "lookup"
    -        assert json.loads(tc.function.arguments) == {"q": "weather"}
    -        assert result.choices[0].finish_reason == "tool_calls"
    -
    -    def test_thought_parts_go_to_reasoning(self):
    -        from agent.gemini_cloudcode_adapter import _translate_gemini_response
    -
    -        resp = {
    -            "response": {
    -                "candidates": [{
    -                    "content": {"parts": [
    -                        {"thought": True, "text": "let me think"},
    -                        {"text": "final answer"},
    -                    ]},
    -                }],
    -            }
    -        }
    -        result = _translate_gemini_response(resp, model="gemini-2.5-flash")
    -        assert result.choices[0].message.content == "final answer"
    -        assert result.choices[0].message.reasoning == "let me think"
    -
    -    def test_unwraps_direct_format(self):
    -        """If response is already at top level (no 'response' wrapper), still parse."""
    -        from agent.gemini_cloudcode_adapter import _translate_gemini_response
    -
    -        resp = {
    -            "candidates": [{
    -                "content": {"parts": [{"text": "hi"}]},
    -                "finishReason": "STOP",
    -            }],
    -        }
    -        result = _translate_gemini_response(resp, model="gemini-2.5-flash")
    -        assert result.choices[0].message.content == "hi"
    -
    -    def test_empty_candidates(self):
    -        from agent.gemini_cloudcode_adapter import _translate_gemini_response
    -
    -        result = _translate_gemini_response({"response": {"candidates": []}}, model="gemini-2.5-flash")
    -        assert result.choices[0].message.content == ""
    -        assert result.choices[0].finish_reason == "stop"
    -
    -    def test_finish_reason_mapping(self):
    -        from agent.gemini_cloudcode_adapter import _map_gemini_finish_reason
    -
    -        assert _map_gemini_finish_reason("STOP") == "stop"
    -        assert _map_gemini_finish_reason("MAX_TOKENS") == "length"
    -        assert _map_gemini_finish_reason("SAFETY") == "content_filter"
    -        assert _map_gemini_finish_reason("RECITATION") == "content_filter"
    -
    -
    -class TestTranslateStreamEvent:
    -    def test_parallel_calls_to_same_tool_get_unique_indices(self):
    -        """Gemini may emit several functionCall parts with the same name in a
    -        single turn (e.g. parallel file reads). Each must get its own OpenAI
    -        ``index`` — otherwise downstream aggregators collapse them into one.
    -        """
    -        from agent.gemini_cloudcode_adapter import _translate_stream_event
    -
    -        event = {
    -            "response": {
    -                "candidates": [{
    -                    "content": {"parts": [
    -                        {"functionCall": {"name": "read_file", "args": {"path": "a"}}},
    -                        {"functionCall": {"name": "read_file", "args": {"path": "b"}}},
    -                        {"functionCall": {"name": "read_file", "args": {"path": "c"}}},
    -                    ]},
    -                }],
    -            }
    -        }
    -        counter = [0]
    -        chunks = _translate_stream_event(event, model="gemini-2.5-flash",
    -                                         tool_call_counter=counter)
    -        indices = [c.choices[0].delta.tool_calls[0].index for c in chunks]
    -        assert indices == [0, 1, 2]
    -        assert counter[0] == 3
    -
    -    def test_counter_persists_across_events(self):
    -        """Index assignment must continue across SSE events in the same stream."""
    -        from agent.gemini_cloudcode_adapter import _translate_stream_event
    -
    -        def _event(name):
    -            return {"response": {"candidates": [{
    -                "content": {"parts": [{"functionCall": {"name": name, "args": {}}}]},
    -            }]}}
    -
    -        counter = [0]
    -        chunks_a = _translate_stream_event(_event("foo"), model="m", tool_call_counter=counter)
    -        chunks_b = _translate_stream_event(_event("bar"), model="m", tool_call_counter=counter)
    -        chunks_c = _translate_stream_event(_event("foo"), model="m", tool_call_counter=counter)
    -
    -        assert chunks_a[0].choices[0].delta.tool_calls[0].index == 0
    -        assert chunks_b[0].choices[0].delta.tool_calls[0].index == 1
    -        assert chunks_c[0].choices[0].delta.tool_calls[0].index == 2
    -
    -    def test_finish_reason_switches_to_tool_calls_when_any_seen(self):
    -        from agent.gemini_cloudcode_adapter import _translate_stream_event
    -
    -        counter = [0]
    -        # First event emits one tool call.
    -        _translate_stream_event(
    -            {"response": {"candidates": [{
    -                "content": {"parts": [{"functionCall": {"name": "x", "args": {}}}]},
    -            }]}},
    -            model="m", tool_call_counter=counter,
    -        )
    -        # Second event carries only the terminal finishReason.
    -        chunks = _translate_stream_event(
    -            {"response": {"candidates": [{"finishReason": "STOP"}]}},
    -            model="m", tool_call_counter=counter,
    -        )
    -        assert chunks[-1].choices[0].finish_reason == "tool_calls"
    -
    -
    -class TestMakeStreamChunk:
    -    def test_reasoning_only_chunk_has_content_none(self):
    -        from agent.gemini_cloudcode_adapter import _make_stream_chunk
    -
    -        chunk = _make_stream_chunk(model="m", reasoning="think")
    -        delta = chunk.choices[0].delta
    -        assert delta.content is None
    -        assert delta.reasoning == "think"
    -
    -    def test_content_only_chunk_has_reasoning_none(self):
    -        from agent.gemini_cloudcode_adapter import _make_stream_chunk
    -
    -        chunk = _make_stream_chunk(model="m", content="hello")
    -        delta = chunk.choices[0].delta
    -        assert delta.content == "hello"
    -        assert delta.reasoning is None
    -        assert delta.tool_calls is None
    -
    -    def test_finish_only_chunk_has_all_fields_none(self):
    -        from agent.gemini_cloudcode_adapter import _make_stream_chunk
    -
    -        chunk = _make_stream_chunk(model="m", finish_reason="stop")
    -        delta = chunk.choices[0].delta
    -        assert delta.content is None
    -        assert delta.reasoning is None
    -        assert delta.tool_calls is None
    -        assert chunk.choices[0].finish_reason == "stop"
    -
    -
    -class TestGeminiCloudCodeClient:
    -    def test_client_exposes_openai_interface(self):
    -        from agent.gemini_cloudcode_adapter import GeminiCloudCodeClient
    -
    -        client = GeminiCloudCodeClient(api_key="dummy")
    -        try:
    -            assert hasattr(client, "chat")
    -            assert hasattr(client.chat, "completions")
    -            assert callable(client.chat.completions.create)
    -        finally:
    -            client.close()
    -
    -
    -class TestGeminiHttpErrorParsing:
    -    """Regression coverage for _gemini_http_error Google-envelope parsing.
    -
    -    These are the paths that users actually hit during Google-side throttling
    -    (April 2026: gemini-2.5-pro MODEL_CAPACITY_EXHAUSTED, gemma-4-26b-it
    -    returning 404).  The error needs to carry status_code + response so the
    -    main loop's error_classifier and Retry-After logic work.
    -    """
    -
    -    @staticmethod
    -    def _fake_response(status: int, body: dict | str = "", headers=None):
    -        """Minimal httpx.Response stand-in (duck-typed for _gemini_http_error)."""
    -        class _FakeResponse:
    -            def __init__(self):
    -                self.status_code = status
    -                if isinstance(body, dict):
    -                    self.text = json.dumps(body)
    -                else:
    -                    self.text = body
    -                self.headers = headers or {}
    -        return _FakeResponse()
    -
    -    def test_model_capacity_exhausted_produces_friendly_message(self):
    -        from agent.gemini_cloudcode_adapter import _gemini_http_error
    -
    -        body = {
    -            "error": {
    -                "code": 429,
    -                "message": "Resource has been exhausted (e.g. check quota).",
    -                "status": "RESOURCE_EXHAUSTED",
    -                "details": [
    -                    {
    -                        "@type": "type.googleapis.com/google.rpc.ErrorInfo",
    -                        "reason": "MODEL_CAPACITY_EXHAUSTED",
    -                        "domain": "googleapis.com",
    -                        "metadata": {"model": "gemini-2.5-pro"},
    -                    },
    -                    {
    -                        "@type": "type.googleapis.com/google.rpc.RetryInfo",
    -                        "retryDelay": "30s",
    -                    },
    -                ],
    -            }
    -        }
    -        err = _gemini_http_error(self._fake_response(429, body))
    -        assert err.status_code == 429
    -        assert err.code == "code_assist_capacity_exhausted"
    -        assert err.retry_after == 30.0
    -        assert err.details["reason"] == "MODEL_CAPACITY_EXHAUSTED"
    -        # Message must be user-friendly, not a raw JSON dump.
    -        message = str(err)
    -        assert "gemini-2.5-pro" in message
    -        assert "capacity exhausted" in message.lower()
    -        assert "30s" in message
    -        # response attr is preserved for run_agent's Retry-After header path.
    -        assert err.response is not None
    -
    -    def test_resource_exhausted_without_reason(self):
    -        from agent.gemini_cloudcode_adapter import _gemini_http_error
    -
    -        body = {
    -            "error": {
    -                "code": 429,
    -                "message": "Quota exceeded for requests per minute.",
    -                "status": "RESOURCE_EXHAUSTED",
    -            }
    -        }
    -        err = _gemini_http_error(self._fake_response(429, body))
    -        assert err.status_code == 429
    -        assert err.code == "code_assist_rate_limited"
    -        message = str(err)
    -        assert "quota" in message.lower()
    -
    -    def test_404_model_not_found_produces_model_retired_message(self):
    -        from agent.gemini_cloudcode_adapter import _gemini_http_error
    -
    -        body = {
    -            "error": {
    -                "code": 404,
    -                "message": "models/gemma-4-26b-it is not found for API version v1internal",
    -                "status": "NOT_FOUND",
    -            }
    -        }
    -        err = _gemini_http_error(self._fake_response(404, body))
    -        assert err.status_code == 404
    -        message = str(err)
    -        assert "not available" in message.lower() or "retired" in message.lower()
    -        # Error message should reference the actual model text from Google.
    -        assert "gemma-4-26b-it" in message
    -
    -    def test_unauthorized_preserves_status_code(self):
    -        from agent.gemini_cloudcode_adapter import _gemini_http_error
    -
    -        err = _gemini_http_error(self._fake_response(
    -            401, {"error": {"code": 401, "message": "Invalid token", "status": "UNAUTHENTICATED"}},
    -        ))
    -        assert err.status_code == 401
    -        assert err.code == "code_assist_unauthorized"
    -
    -    def test_retry_after_header_fallback(self):
    -        """If the body has no RetryInfo detail, fall back to Retry-After header."""
    -        from agent.gemini_cloudcode_adapter import _gemini_http_error
    -
    -        resp = self._fake_response(
    -            429,
    -            {"error": {"code": 429, "message": "Rate limited", "status": "RESOURCE_EXHAUSTED"}},
    -            headers={"Retry-After": "45"},
    -        )
    -        err = _gemini_http_error(resp)
    -        assert err.retry_after == 45.0
    -
    -    def test_malformed_body_still_produces_structured_error(self):
    -        """Non-JSON body must not swallow status_code — we still want the classifier path."""
    -        from agent.gemini_cloudcode_adapter import _gemini_http_error
    -
    -        err = _gemini_http_error(self._fake_response(500, "<html>internal error</html>"))
    -        assert err.status_code == 500
    -        # Raw body snippet must still be there for debugging.
    -        assert "500" in str(err)
    -
    -    def test_status_code_flows_through_error_classifier(self):
    -        """End-to-end: CodeAssistError from a 429 must classify as rate_limit.
    -
    -        This is the whole point of adding status_code to CodeAssistError —
    -        _extract_status_code must see it and FailoverReason.rate_limit must
    -        fire, so the main loop triggers fallback_providers.
    -        """
    -        from agent.gemini_cloudcode_adapter import _gemini_http_error
    -        from agent.error_classifier import classify_api_error, FailoverReason
    -
    -        body = {
    -            "error": {
    -                "code": 429,
    -                "message": "Resource has been exhausted",
    -                "status": "RESOURCE_EXHAUSTED",
    -                "details": [
    -                    {
    -                        "@type": "type.googleapis.com/google.rpc.ErrorInfo",
    -                        "reason": "MODEL_CAPACITY_EXHAUSTED",
    -                        "metadata": {"model": "gemini-2.5-pro"},
    -                    }
    -                ],
    -            }
    -        }
    -        err = _gemini_http_error(self._fake_response(429, body))
    -
    -        classified = classify_api_error(
    -            err, provider="google-gemini-cli", model="gemini-2.5-pro",
    -        )
    -        assert classified.status_code == 429
    -        assert classified.reason == FailoverReason.rate_limit
    -
    -
    -# =============================================================================
    -# Provider registration
    -# =============================================================================
    -
    -class TestProviderRegistration:
    -    def test_registry_entry(self):
    -        from hermes_cli.auth import PROVIDER_REGISTRY
    -
    -        assert "google-gemini-cli" in PROVIDER_REGISTRY
    -        assert PROVIDER_REGISTRY["google-gemini-cli"].auth_type == "oauth_external"
    -
    -    def test_google_gemini_alias_still_goes_to_api_key_gemini(self):
    -        """Regression guard: don't shadow the existing google-gemini → gemini alias."""
    -        from hermes_cli.auth import resolve_provider
    -
    -        assert resolve_provider("google-gemini") == "gemini"
    -
    -    def test_runtime_provider_raises_when_not_logged_in(self):
    -        from hermes_cli.auth import AuthError
    -        from hermes_cli.runtime_provider import resolve_runtime_provider
    -
    -        with pytest.raises(AuthError) as exc_info:
    -            resolve_runtime_provider(requested="google-gemini-cli")
    -        assert exc_info.value.code == "google_oauth_not_logged_in"
    -
    -    def test_runtime_provider_returns_correct_shape_when_logged_in(self):
    -        from agent.google_oauth import GoogleCredentials, save_credentials
    -        from hermes_cli.runtime_provider import resolve_runtime_provider
    -
    -        save_credentials(GoogleCredentials(
    -            access_token="[redacted:secret-assignment]",
    -            refresh_token="rt",
    -            expires_ms=int((time.time() + 3600) * 1000),
    -            project_id="my-proj",
    -            email="[redacted:email]",
    -        ))
    -
    -        result = resolve_runtime_provider(requested="google-gemini-cli")
    -        assert result["provider"] == "google-gemini-cli"
    -        assert result["api_mode"] == "chat_completions"
    -        assert result["api_key"] == "live-tok"
    -        assert result["base_url"] == "cloudcode-pa://google"
    -        assert result["project_id"] == "my-proj"
    -        assert result["email"] == "[redacted:email]"
    -
    -    def test_determine_api_mode(self):
    -        from hermes_cli.providers import determine_api_mode
    -
    -        assert determine_api_mode("google-gemini-cli", "cloudcode-pa://google") == "chat_completions"
    -
    -    def test_oauth_capable_set_preserves_existing(self):
    -        from hermes_cli.auth_commands import _OAUTH_CAPABLE_PROVIDERS
    -
    -        for required in ("anthropic", "nous", "openai-codex", "qwen-oauth", "google-gemini-cli"):
    -            assert required in _OAUTH_CAPABLE_PROVIDERS
    -
    -    def test_config_env_vars_registered(self):
    -        from hermes_cli.config import OPTIONAL_ENV_VARS
    -
    -        for key in (
    -            "HERMES_GEMINI_CLIENT_ID",
    -            "HERMES_GEMINI_CLIENT_SECRET",
    -            "HERMES_GEMINI_PROJECT_ID",
    -        ):
    -            assert key in OPTIONAL_ENV_VARS
    -
    -
    -class TestAuthStatus:
    -    def test_not_logged_in(self):
    -        from hermes_cli.auth import get_auth_status
    -
    -        s = get_auth_status("google-gemini-cli")
    -        assert s["logged_in"] is False
    -
    -    def test_logged_in_reports_email_and_project(self):
    -        from agent.google_oauth import GoogleCredentials, save_credentials
    -        from hermes_cli.auth import get_auth_status
    -
    -        save_credentials(GoogleCredentials(
    -            access_token="tok", refresh_token="rt",
    -            expires_ms=int((time.time() + 3600) * 1000),
    -            email="[redacted:email]",
    -            project_id="tek-proj",
    -        ))
    -
    -        s = get_auth_status("google-gemini-cli")
    -        assert s["logged_in"] is True
    -        assert s["email"] == "[redacted:email]"
    -        assert s["project_id"] == "tek-proj"
    -
    -
    -class TestGquotaCommand:
    -    def test_gquota_registered(self):
    -        from hermes_cli.commands import COMMANDS
    -
    -        assert "/gquota" in COMMANDS
    -
    -
    -class TestRunGeminiOauthLoginPure:
    -    def test_returns_pool_compatible_dict(self, monkeypatch):
    -        from agent import google_oauth
    -
    -        def fake_start(**kw):
    -            return google_oauth.GoogleCredentials(
    -                access_token="at", refresh_token="rt",
    -                expires_ms=int((time.time() + 3600) * 1000),
    -                email="[redacted:email]", project_id="p",
    -            )
    -
    -        monkeypatch.setattr(google_oauth, "start_oauth_flow", fake_start)
    -
    -        result = google_oauth.run_gemini_oauth_login_pure()
    -        assert result["access_token"] == "at"
    -        assert result["refresh_token"] == "rt"
    -        assert result["email"] == "[redacted:email]"
    -        assert result["project_id"] == "p"
    -        assert isinstance(result["expires_at_ms"], int)
    ```
  - `tests/agent/test_gemini_fast_fallback.py`

    ```diff
    @@ -22,7 +22,7 @@ def _pool(entries: int = 2):
     def test_cloudcode_provider_skips_pool_rotation():
         assert _pool_may_recover_from_rate_limit(
             _pool(entries=3),
    -        provider="google-gemini-cli",
    +        provider="auto",
             base_url="cloudcode-pa://google",
         ) is False
     
    ```
  - `tests/agent/transports/test_chat_completions.py`

    ```diff
    @@ -404,34 +404,6 @@ def test_gemini_openai_compat_xhigh_clamps_to_high(self, transport):
             )
             assert kw["extra_body"]["extra_body"]["google"]["thinking_config"]["thinking_level"] == "high"
     
    -    def test_google_gemini_cli_keeps_top_level_thinking_config(self, transport):
    -        msgs = [{"role": "user", "content": "Hi"}]
    -        kw = transport.build_kwargs(
    -            model="gemini-3-flash-preview",
    -            messages=msgs,
    -            provider_name="google-gemini-cli",
    -            reasoning_config={"enabled": True, "effort": "high"},
    -        )
    -        assert kw["extra_body"]["thinking_config"] == {
    -            "includeThoughts": True,
    -            "thinkingLevel": "high",
    -        }
    -        assert "google" not in kw["extra_body"]
    -
    -    def test_google_antigravity_keeps_top_level_thinking_config(self, transport):
    -        msgs = [{"role": "user", "content": "Hi"}]
    -        kw = transport.build_kwargs(
    -            model="gemini-3-flash-agent",
    -            messages=msgs,
    -            provider_name="google-antigravity",
    -            reasoning_config={"enabled": True, "effort": "high"},
    -        )
    -        assert kw["extra_body"]["thinking_config"] == {
    -            "includeThoughts": True,
    -            "thinkingLevel": "high",
    -        }
    -        assert "google" not in kw["extra_body"]
    -
         def test_gemini_flash_minimal_clamps_to_low(self, transport):
             # Gemini 3 Flash documents low/medium/high; "minimal" isn't accepted,
             # so clamp it down to "low" rather than forwarding it verbatim.
    ```
  - `tests/agent/transports/test_codex_app_server_runtime.py`

    ```diff
    @@ -85,7 +85,6 @@ def test_case_insensitive(self) -> None:
                 "openrouter",
                 "xai",
                 "qwen-oauth",
    -            "google-gemini-cli",
                 "opencode-zen",
                 "bedrock",
                 "",
    ```
  - `/dev/null`

    ```diff
    @@ -1,21 +0,0 @@
    -from unittest.mock import MagicMock, patch
    -
    -
    -def test_gquota_uses_chat_console_when_tui_is_live():
    -    from agent.google_oauth import GoogleOAuthError
    -    from cli import HermesCLI
    -
    -    cli = HermesCLI.__new__(HermesCLI)
    -    cli.console = MagicMock()
    -    cli._app = object()
    -
    -    live_console = MagicMock()
    -
    -    with patch("cli.ChatConsole", return_value=live_console), \
    -         patch("agent.google_oauth.get_valid_access_token", side_effect=GoogleOAuthError("No Google OAuth credentials found")), \
    -         patch("agent.google_oauth.load_credentials", return_value=None), \
    -         patch("agent.google_code_assist.retrieve_user_quota"):
    -        cli._handle_gquota_command("/gquota")
    -
    -    assert live_console.print.call_count == 2
    -    cli.console.print.assert_not_called()
    ```
  - `tests/hermes_cli/test_auth_commands.py`

    ```diff
    @@ -129,51 +129,6 @@ class _Args:
         assert entry["expires_at_ms"] == 1711234567000
     
     
    -def test_auth_add_google_gemini_cli_sets_active_provider(tmp_path, monkeypatch):
    -    """hermes auth add google-gemini-cli must set active_provider in auth.json.
    -
    -    Tokens are managed by agent.google_oauth (written to the Google credential
    -    file by start_oauth_flow). The auth.json entry must record active_provider
    -    so get_active_provider() and _model_section_has_credentials() detect the
    -    provider — without storing tokens that would become stale.
    -    """
    -    monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes"))
    -    _write_auth_store(tmp_path, {"version": 1, "providers": {}})
    -    monkeypatch.setattr(
    -        "agent.google_oauth.run_gemini_oauth_login_pure",
    -        lambda: {
    -            "access_token": "ya29.test-token",
    -            "refresh_token": "google-refresh",
    -            "email": "[redacted:email]",
    -            "expires_at_ms": 9999999999000,
    -            "project_id": "my-project",
    -        },
    -    )
    -
    -    from hermes_cli.auth_commands import auth_add_command
    -
    -    class _Args:
    -        provider = "google-gemini-cli"
    -        auth_type = "oauth"
    -        api_key = None
    -        label = None
    -
    -    auth_add_command(_Args())
    -
    -    payload = json.loads((tmp_path / "hermes" / "auth.json").read_text())
    -    assert payload["active_provider"] == "google-gemini-cli"
    -    state = payload["providers"]["google-gemini-cli"]
    -    # Only email stored — no access_token/refresh_token (those live in
    -    # the Google OAuth credential file managed by agent.google_oauth).
    -    assert state.get("email") == "[redacted:email]"
    -    assert "access_token" not in state
    -    assert "refresh_token" not in state
    -    # pool entry from pool.add_entry() still present for hermes auth list
    -    entries = payload["credential_pool"]["google-gemini-cli"]
    -    entry = next(item for item in entries if item["source"] == "manual:google_pkce")
    -    assert entry["access_token"] == "ya29.test-token"
    -
    -
     def test_auth_add_qwen_oauth_sets_active_provider(tmp_path, monkeypatch):
         """hermes auth add qwen-oauth must set active_provider in auth.json.
     
    ```
  - `tests/hermes_cli/test_config.py`

    ```diff
    @@ -1056,7 +1056,6 @@ def test_denylisted_keys_rejected(self, denied_key):
         @pytest.mark.parametrize(
             "allowed_key",
             [
    -            "HERMES_GEMINI_CLIENT_ID",
                 "HERMES_LANGFUSE_PUBLIC_KEY",
                 "HERMES_SPOTIFY_CLIENT_ID",
                 "HERMES_QWEN_BASE_URL",
    ```
  - `tests/hermes_cli/test_doctor.py`

    ```diff
    @@ -473,7 +473,6 @@ def test_run_doctor_flags_missing_credentials_for_active_openrouter_provider(mon
     
             monkeypatch.setattr(_auth_mod, "get_nous_auth_status", lambda: {})
             monkeypatch.setattr(_auth_mod, "get_codex_auth_status", lambda: {})
    -        monkeypatch.setattr(_auth_mod, "get_gemini_oauth_auth_status", lambda: {})
             monkeypatch.setattr(_auth_mod, "get_minimax_oauth_auth_status", lambda: {})
         except Exception:
             pass
    ```

    ```diff
    @@ -915,7 +914,6 @@ def _run_doctor_with_healthy_oauth_fallback(
         env_key: str,
         bad_key: str,
         failing_host: str,
    -    gemini_oauth_status: dict,
         minimax_oauth_status: dict,
         xai_oauth_status: dict | None = None,
     ) -> str:
    ```

    ```diff
    @@ -952,7 +950,6 @@ def _run_doctor_with_healthy_oauth_fallback(
     
         monkeypatch.setattr(_auth_mod, "get_nous_auth_status", lambda: {"logged_in": True})
         monkeypatch.setattr(_auth_mod, "get_codex_auth_status", lambda: {})
    -    monkeypatch.setattr(_auth_mod, "get_gemini_oauth_auth_status", lambda: gemini_oauth_status)
         monkeypatch.setattr(_auth_mod, "get_minimax_oauth_auth_status", lambda: minimax_oauth_status)
         _xai_status = xai_oauth_status if xai_oauth_status is not None else {}
         monkeypatch.setattr(_auth_mod, "get_xai_oauth_auth_status", lambda: _xai_status)
    ```

    ```diff
    @@ -972,22 +969,12 @@ def fake_get(url, headers=None, timeout=None):
     
     
     @pytest.mark.parametrize(
    -    ("env_key", "bad_key", "failing_host", "gemini_oauth_status", "minimax_oauth_status", "xai_oauth_status", "unexpected_issue"),
    +    ("env_key", "bad_key", "failing_host", "minimax_oauth_status", "xai_oauth_status", "unexpected_issue"),
         [
    -        (
    -            "GOOGLE_API_KEY",
    -            "bad-gemini-key",
    -            "googleapis.com",
    -            {"logged_in": True, "email": "[redacted:email]"},
    -            {},
    -            None,
    -            "Check GOOGLE_API_KEY in .env",
    -        ),
             (
                 "MINIMAX_API_KEY",
                 "bad-minimax-key",
                 "minimax.io",
    -            {},
                 {"logged_in": True, "region": "global"},
                 None,
                 "Check MINIMAX_API_KEY in .env",
    ```

    ```diff
    @@ -997,7 +984,6 @@ def fake_get(url, headers=None, timeout=None):
                 "bad-xai-key",
                 "api.x.ai",
                 {},
    -            {},
                 {"logged_in": True, "auth_mode": "oauth_pkce"},
                 "Check XAI_API_KEY in .env",
             ),
    ```

    ```diff
    @@ -1009,7 +995,6 @@ def test_run_doctor_ignores_invalid_direct_keys_when_oauth_fallback_is_healthy(
         env_key,
         bad_key,
         failing_host,
    -    gemini_oauth_status,
         minimax_oauth_status,
         xai_oauth_status,
         unexpected_issue,
    ```

    ```diff
    @@ -1020,7 +1005,6 @@ def test_run_doctor_ignores_invalid_direct_keys_when_oauth_fallback_is_healthy(
             env_key=env_key,
             bad_key=bad_key,
             failing_host=failing_host,
    -        gemini_oauth_status=gemini_oauth_status,
             minimax_oauth_status=minimax_oauth_status,
             xai_oauth_status=xai_oauth_status,
         )
    ```

    ```diff
    @@ -1062,16 +1046,6 @@ def test_returns_false_when_xai_import_unavailable(self, monkeypatch):
             from hermes_cli.doctor import _has_healthy_oauth_fallback_for_apikey_provider
             assert _has_healthy_oauth_fallback_for_apikey_provider("xai") is False
     
    -    def test_xai_import_failure_does_not_affect_gemini(self, monkeypatch):
    -        import sys
    -        from hermes_cli import auth as _auth_mod
    -        # xAI function missing, but Gemini is healthy
    -        monkeypatch.delattr(_auth_mod, "get_xai_oauth_auth_status", raising=False)
    -        monkeypatch.setattr(_auth_mod, "get_gemini_oauth_auth_status", lambda: {"logged_in": True})
    -        monkeypatch.delitem(sys.modules, "hermes_cli.doctor", raising=False)
    -        from hermes_cli.doctor import _has_healthy_oauth_fallback_for_apikey_provider
    -        assert _has_healthy_oauth_fallback_for_apikey_provider("gemini") is True
    -
     
     # ---------------------------------------------------------------------------
     # ◆ Auth Providers — xAI OAuth display in run_doctor()
    ```

    ```diff
    @@ -1107,7 +1081,6 @@ def _run(self, monkeypatch, tmp_path, *, xai_auth_fn) -> str:
             from hermes_cli import auth as _auth_mod
             monkeypatch.setattr(_auth_mod, "get_nous_auth_status", lambda: {"logged_in": False})
             monkeypatch.setattr(_auth_mod, "get_codex_auth_status", lambda: {"logged_in": False})
    -        monkeypatch.setattr(_auth_mod, "get_gemini_oauth_auth_status", lambda: {"logged_in": False})
             monkeypatch.setattr(_auth_mod, "get_minimax_oauth_auth_status", lambda: {"logged_in": False})
             monkeypatch.setattr(_auth_mod, "get_xai_oauth_auth_status", xai_auth_fn)
     
    ```

    ```diff
    @@ -1182,7 +1155,6 @@ def test_import_failure_does_not_crash_doctor(self, monkeypatch, tmp_path):
             from hermes_cli import auth as _auth_mod
             monkeypatch.setattr(_auth_mod, "get_nous_auth_status", lambda: {"logged_in": False})
             monkeypatch.setattr(_auth_mod, "get_codex_auth_status", lambda: {"logged_in": False})
    -        monkeypatch.setattr(_auth_mod, "get_gemini_oauth_auth_status", lambda: {"logged_in": False})
             monkeypatch.setattr(_auth_mod, "get_minimax_oauth_auth_status", lambda: {"logged_in": False})
             monkeypatch.delattr(_auth_mod, "get_xai_oauth_auth_status", raising=False)
     
    ```

    ```diff
    @@ -1214,7 +1186,6 @@ def test_import_failure_does_not_affect_other_providers(self, monkeypatch, tmp_p
             from hermes_cli import auth as _auth_mod
             monkeypatch.setattr(_auth_mod, "get_nous_auth_status", lambda: {"logged_in": True})
             monkeypatch.setattr(_auth_mod, "get_codex_auth_status", lambda: {"logged_in": False})
    -        monkeypatch.setattr(_auth_mod, "get_gemini_oauth_auth_status", lambda: {"logged_in": False})
             monkeypatch.setattr(_auth_mod, "get_minimax_oauth_auth_status", lambda: {"logged_in": False})
             monkeypatch.delattr(_auth_mod, "get_xai_oauth_auth_status", raising=False)
     
    ```

    ```diff
    @@ -1275,7 +1246,6 @@ def _run(self, monkeypatch, tmp_path, *, codex_logged_in: bool, codex_cli_presen
             from hermes_cli import auth as _auth_mod
             monkeypatch.setattr(_auth_mod, "get_nous_auth_status", lambda: {"logged_in": False})
             monkeypatch.setattr(_auth_mod, "get_codex_auth_status", lambda: {"logged_in": codex_logged_in})
    -        monkeypatch.setattr(_auth_mod, "get_gemini_oauth_auth_status", lambda: {"logged_in": False})
             monkeypatch.setattr(_auth_mod, "get_minimax_oauth_auth_status", lambda: {"logged_in": False})
             monkeypatch.setattr(_auth_mod, "get_xai_oauth_auth_status", lambda: {"logged_in": False})
     
    ```

    ```diff
    @@ -1317,12 +1287,16 @@ def test_hint_suppressed_when_codex_logged_in(self, monkeypatch, tmp_path):
     
         def test_hint_never_attaches_to_minimax_row(self, monkeypatch, tmp_path):
             out = self._run(monkeypatch, tmp_path, codex_logged_in=False, codex_cli_present=False)
    -        # The MiniMax OAuth row and the hint must not be adjacent — the hint
    -        # belongs to the Codex auth row directly above it.
    +        # The hint belongs to the Codex auth row that precedes it, never to the
    +        # MiniMax row that follows (#27975). The MiniMax row itself must not be
    +        # the hint line, and the hint must sit strictly above MiniMax.
             lines = [l for l in out.splitlines() if l.strip()]
    +        codex_idx = next(i for i, l in enumerate(lines) if "OpenAI Codex auth" in l)
    +        hint_idx = next(i for i, l in enumerate(lines) if self._hint_line() in l)
             minimax_idx = next(i for i, l in enumerate(lines) if "MiniMax OAuth" in l)
    -        assert self._hint_line() not in lines[minimax_idx - 1]
    -        assert minimax_idx + 1 >= len(lines) or self._hint_line() not in lines[minimax_idx + 1]
    +        # Hint sits under Codex and above MiniMax; the MiniMax row is not the hint.
    +        assert codex_idx < hint_idx < minimax_idx
    +        assert self._hint_line() not in lines[minimax_idx]
     
     
     class TestDoctorStaleMaxIterationsDrift:
    ```
  - `tests/hermes_cli/test_model_provider_persistence.py`

    ```diff
    @@ -316,41 +316,6 @@ def test_opencode_go_same_provider_switch_recomputes_api_mode(self, config_home,
             assert model.get("default") == "minimax-m2.5"
             assert model.get("api_mode") == "anthropic_messages"
     
    -    def test_antigravity_oauth_provider_saved_when_selected(self, config_home):
    -        """_model_flow_google_antigravity should persist provider/base_url/model together."""
    -        from hermes_cli.main import _model_flow_google_antigravity
    -        from hermes_cli.config import load_config
    -
    -        with patch(
    -            "hermes_cli.auth.get_antigravity_oauth_auth_status",
    -            return_value={"logged_in": True, "email": "[redacted:email]"},
    -        ), patch(
    -            "hermes_cli.auth.resolve_antigravity_oauth_runtime_credentials",
    -            return_value={
    -                "provider": "google-antigravity",
    -                "api_key": "tok",
    -                "base_url": "antigravity-pa://google",
    -                "project_id": "proj-123",
    -            },
    -        ), patch(
    -            "hermes_cli.models.provider_model_ids",
    -            return_value=["gemini-3-flash-agent", "claude-sonnet-4-6"],
    -        ), patch(
    -            "hermes_cli.auth._prompt_model_selection",
    -            return_value="claude-sonnet-4-6",
    -        ):
    -            _model_flow_google_antigravity(load_config(), "old-model")
    -
    -        import yaml
    -
    -        config = yaml.safe_load((config_home / "config.yaml").read_text()) or {}
    -        model = config.get("model")
    -        assert isinstance(model, dict), f"model should be dict, got {type(model)}"
    -        assert model.get("provider") == "google-antigravity"
    -        assert model.get("base_url") == "antigravity-pa://google"
    -        assert model.get("default") == "claude-sonnet-4-6"
    -        assert "api_mode" not in model
    -
     
     
     class TestBaseUrlValidation:
    ```
  - `tests/hermes_cli/test_provider_catalog.py`

    ```diff
    @@ -62,8 +62,6 @@ def test_api_key_providers_route_to_keys_oauth_to_accounts():
         # api_key → keys
         assert by["kilocode"].tab == "keys"
         assert by["openai-api"].tab == "keys"
    -    # account / sign-in flows → accounts
    -    assert by["google-gemini-cli"].tab == "accounts"
         assert by["copilot-acp"].tab == "accounts"
     
     
    ```
  - `tests/hermes_cli/test_web_oauth_dispatch.py`

    ```diff
    @@ -489,14 +489,13 @@ def test_accounts_offers_every_oauth_provider_from_catalog():
                 )
     
     
    -def test_gemini_cli_and_copilot_acp_now_in_accounts():
    -    """Regression: google-gemini-cli and copilot-acp were canonical providers the
    -    CLI could configure, but had no Accounts card (the reported GUI/CLI drift).
    +def test_copilot_acp_now_in_accounts():
    +    """Regression: copilot-acp was a canonical provider the CLI could configure,
    +    but had no Accounts card (the reported GUI/CLI drift).
         """
         resp = client.get("/api/providers/oauth", headers=HEADERS)
         assert resp.status_code == 200, resp.text
         providers = {p["id"]: p for p in resp.json()["providers"]}
    -    assert "google-gemini-cli" in providers
         assert "copilot-acp" in providers
         # copilot-acp is managed by an external CLI: read-only card, not auto-removable.
         assert providers["copilot-acp"]["flow"] == "external"
    ```
  - `/dev/null`

    ```diff
    @@ -1,447 +0,0 @@
    -"""Regression tests for Google Workspace OAuth setup.
    -
    -These tests cover the headless/manual auth-code flow where the browser step and
    -code exchange happen in separate process invocations.
    -"""
    -
    -import importlib.util
    -import json
    -import sys
    -import types
    -from pathlib import Path
    -
    -import pytest
    -
    -
    -SCRIPT_PATH = (
    -    Path(__file__).resolve().parents[2]
    -    / "skills/productivity/google-workspace/scripts/setup.py"
    -)
    -
    -
    -class FakeCredentials:
    -    def __init__(self, payload=None):
    -        self._payload = payload or {
    -            "token": "access-token",
    -            "refresh_token": "refresh-token",
    -            "token_uri": "https://oauth2.googleapis.com/token",
    -            "client_id": "client-id",
    -            "client_secret": "client-secret",
    -            "scopes": [
    -                "https://www.googleapis.com/auth/gmail.readonly",
    -                "https://www.googleapis.com/auth/gmail.send",
    -                "https://www.googleapis.com/auth/gmail.modify",
    -                "https://www.googleapis.com/auth/calendar",
    -                "https://www.googleapis.com/auth/drive.readonly",
    -                "https://www.googleapis.com/auth/contacts.readonly",
    -                "https://www.googleapis.com/auth/spreadsheets",
    -                "https://www.googleapis.com/auth/documents.readonly",
    -            ],
    -        }
    -
    -    def to_json(self):
    -        return json.dumps(self._payload)
    -
    -
    -class FakeFlow:
    -    created = []
    -    default_state = "generated-state"
    -    default_verifier = "generated-code-verifier"
    -    credentials_payload = None
    -    fetch_error = None
    -
    -    def __init__(
    -        self,
    -        client_secrets_file,
    -        scopes,
    -        *,
    -        redirect_uri=None,
    -        state=None,
    -        code_verifier=None,
    -        autogenerate_code_verifier=False,
    -    ):
    -        self.client_secrets_file = [redacted:secret-assignment]
    -        self.scopes = scopes
    -        self.redirect_uri = redirect_uri
    -        self.state = state
    -        self.code_verifier = code_verifier
    -        self.autogenerate_code_verifier = autogenerate_code_verifier
    -        self.authorization_kwargs = None
    -        self.fetch_token_calls = []
    -        self.credentials = [redacted:secret-assignment]
    -
    -        if autogenerate_code_verifier and not self.code_verifier:
    -            self.code_verifier = self.default_verifier
    -        if not self.state:
    -            self.state = self.default_state
    -
    -    @classmethod
    -    def reset(cls):
    -        cls.created = []
    -        cls.default_state = "generated-state"
    -        cls.default_verifier = "generated-code-verifier"
    -        cls.credentials_payload = None
    -        cls.fetch_error = None
    -
    -    @classmethod
    -    def from_client_secrets_file(cls, client_secrets_file, scopes, **kwargs):
    -        inst = cls(client_secrets_file, scopes, **kwargs)
    -        cls.created.append(inst)
    -        return inst
    -
    -    def authorization_url(self, **kwargs):
    -        self.authorization_kwargs = kwargs
    -        return f"https://auth.example/authorize?state={self.state}", self.state
    -
    -    def fetch_token(self, **kwargs):
    -        self.fetch_token_calls.append(kwargs)
    -        if self.fetch_error:
    -            raise self.fetch_error
    -
    -
    [redacted:email]
    -def setup_module(monkeypatch, tmp_path):
    -    FakeFlow.reset()
    -
    -    google_auth_module = types.ModuleType("google_auth_oauthlib")
    -    flow_module = types.ModuleType("google_auth_oauthlib.flow")
    -    flow_module.Flow = FakeFlow
    -    google_auth_module.flow = flow_module
    -    monkeypatch.setitem(sys.modules, "google_auth_oauthlib", google_auth_module)
    -    monkeypatch.setitem(sys.modules, "google_auth_oauthlib.flow", flow_module)
    -
    -    spec = importlib.util.spec_from_file_location("google_workspace_setup_test", SCRIPT_PATH)
    -    module = importlib.util.module_from_spec(spec)
    -    assert spec.loader is not None
    -    spec.loader.exec_module(module)
    -
    -    monkeypatch.setattr(module, "_ensure_deps", lambda: None)
    -    monkeypatch.setattr(module, "CLIENT_SECRET_PATH", tmp_path / "google_client_secret.json")
    -    monkeypatch.setattr(module, "TOKEN_PATH", tmp_path / "google_token.json")
    -    monkeypatch.setattr(module, "PENDING_AUTH_PATH", tmp_path / "google_oauth_pending.json", raising=False)
    -
    -    client_secret = {
    -        "installed": {
    -            "client_id": "client-id",
    -            "client_secret": "client-secret",
    -            "auth_uri": "https://accounts.google.com/o/oauth2/auth",
    -            "token_uri": "https://oauth2.googleapis.com/token",
    -        }
    -    }
    -    module.CLIENT_SECRET_PATH.write_text(json.dumps(client_secret))
    -    return module
    -
    -
    -class TestGetAuthUrl:
    -    def test_persists_state_and_code_verifier_for_later_exchange(self, setup_module, capsys):
    -        setup_module.get_auth_url()
    -
    -        out = capsys.readouterr().out.strip()
    -        assert out == "https://auth.example/authorize?state=generated-state"
    -
    -        saved = json.loads(setup_module.PENDING_AUTH_PATH.read_text())
    -        assert saved["state"] == "generated-state"
    -        assert saved["code_verifier"] == "generated-code-verifier"
    -
    -        flow = FakeFlow.created[-1]
    -        assert flow.autogenerate_code_verifier is True
    -        assert flow.authorization_kwargs == {"access_type": "offline", "prompt": "consent"}
    -
    -
    -class TestExchangeAuthCode:
    -    def test_reuses_saved_pkce_material_for_plain_code(self, setup_module):
    -        setup_module.PENDING_AUTH_PATH.write_text(
    -            json.dumps({"state": "saved-state", "code_verifier": "saved-verifier"})
    -        )
    -
    -        setup_module.exchange_auth_code("4/test-auth-code")
    -
    -        flow = FakeFlow.created[-1]
    -        assert flow.state == "saved-state"
    -        assert flow.code_verifier == "saved-verifier"
    -        assert flow.fetch_token_calls == [{"code": "4/test-auth-code"}]
    -        saved = json.loads(setup_module.TOKEN_PATH.read_text())
    -        assert saved["token"] == "access-token"
    -        assert saved["type"] == "authorized_user"
    -        assert not setup_module.PENDING_AUTH_PATH.exists()
    -
    -    def test_extracts_code_from_redirect_url_and_checks_state(self, setup_module):
    -        setup_module.PENDING_AUTH_PATH.write_text(
    -            json.dumps({"state": "saved-state", "code_verifier": "saved-verifier"})
    -        )
    -
    -        setup_module.exchange_auth_code(
    -            "http://localhost:1/?code=4/extracted-code&state=saved-state&scope=gmail"
    -        )
    -
    -        flow = FakeFlow.created[-1]
    -        assert flow.fetch_token_calls == [{"code": "4/extracted-code"}]
    -
    -    def test_passes_scopes_from_redirect_url_to_flow(self, setup_module):
    -        """Callback URL carries space-delimited scope list; Flow must receive it (not full SCOPES)."""
    -        setup_module.PENDING_AUTH_PATH.write_text(
    -            json.dumps({"state": "saved-state", "code_verifier": "saved-verifier"})
    -        )
    -        g1 = "https://www.googleapis.com/auth/gmail.readonly"
    -        g2 = "https://www.googleapis.com/auth/calendar"
    -        from urllib.parse import quote
    -
    -        scope_q = quote(f"{g1} {g2}", safe="")
    -        setup_module.exchange_auth_code(
    -            f"http://localhost:1/?code=4/extracted-code&state=saved-state&scope={scope_q}"
    -        )
    -        flow = FakeFlow.created[-1]
    -        assert flow.scopes == [g1, g2]
    -
    -    def test_rejects_state_mismatch(self, setup_module, capsys):
    -        setup_module.PENDING_AUTH_PATH.write_text(
    -            json.dumps({"state": "saved-state", "code_verifier": "saved-verifier"})
    -        )
    -
    -        with pytest.raises(SystemExit):
    -            setup_module.exchange_auth_code(
    -                "http://localhost:1/?code=4/extracted-code&state=wrong-state"
    -            )
    -
    -        out = capsys.readouterr().out
    -        assert "state mismatch" in out.lower()
    -        assert not setup_module.TOKEN_PATH.exists()
    -
    -    def test_requires_pending_auth_session(self, setup_module, capsys):
    -        with pytest.raises(SystemExit):
    -            setup_module.exchange_auth_code("4/test-auth-code")
    -
    -        out = capsys.readouterr().out
    -        assert "run --auth-url first" in out.lower()
    -        assert not setup_module.TOKEN_PATH.exists()
    -
    -    def test_keeps_pending_auth_session_when_exchange_fails(self, setup_module, capsys):
    -        setup_module.PENDING_AUTH_PATH.write_text(
    -            json.dumps({"state": "saved-state", "code_verifier": "saved-verifier"})
    -        )
    -        FakeFlow.fetch_error = Exception("invalid_grant: Missing code verifier")
    -
    -        with pytest.raises(SystemExit):
    -            setup_module.exchange_auth_code("4/test-auth-code")
    -
    -        out = capsys.readouterr().out
    -        assert "token exchange failed" in out.lower()
    -        assert setup_module.PENDING_AUTH_PATH.exists()
    -        assert not setup_module.TOKEN_PATH.exists()
    -
    -    def test_accepts_narrower_scopes_with_warning(self, setup_module, capsys):
    -        """Partial scopes are accepted with a warning (gws migration: v2.0)."""
    -        setup_module.PENDING_AUTH_PATH.write_text(
    -            json.dumps({"state": "saved-state", "code_verifier": "saved-verifier"})
    -        )
    -        setup_module.TOKEN_PATH.write_text(json.dumps({"token": "***", "scopes": setup_module.SCOPES}))
    -        FakeFlow.credentials_payload = {
    -            "token": "***",
    -            "refresh_token": "***",
    -            "token_uri": "https://oauth2.googleapis.com/token",
    -            "client_id": "client-id",
    -            "client_secret": "client-secret",
    -            "scopes": [
    -                "https://www.googleapis.com/auth/drive.readonly",
    -                "https://www.googleapis.com/auth/spreadsheets",
    -            ],
    -        }
    -
    -        setup_module.exchange_auth_code("4/test-auth-code")
    -
    -        out = capsys.readouterr().out
    -        assert "warning" in out.lower()
    -        assert "missing" in out.lower()
    -        # Token is saved (partial scopes accepted)
    -        assert setup_module.TOKEN_PATH.exists()
    -        # Pending auth is cleaned up
    -        assert not setup_module.PENDING_AUTH_PATH.exists()
    -
    -
    -class TestHermesConstantsFallback:
    -    """Tests for _hermes_home.py fallback when hermes_constants is unavailable."""
    -
    -    HELPER_PATH = (
    -        Path(__file__).resolve().parents[2]
    -        / "skills/productivity/google-workspace/scripts/_hermes_home.py"
    -    )
    -
    -    def _load_helper(self, monkeypatch):
    -        """Load _hermes_home.py with hermes_constants blocked."""
    -        monkeypatch.setitem(sys.modules, "hermes_constants", None)
    -        spec = importlib.util.spec_from_file_location("_hermes_home_test", self.HELPER_PATH)
    -        module = importlib.util.module_from_spec(spec)
    -        assert spec.loader is not None
    -        spec.loader.exec_module(module)
    -        return module
    -
    -    def test_fallback_uses_hermes_home_env_var(self, monkeypatch, tmp_path):
    -        """When hermes_constants is missing, HERMES_HOME comes from env var."""
    -        monkeypatch.setenv("HERMES_HOME", str(tmp_path / "custom-hermes"))
    -        module = self._load_helper(monkeypatch)
    -        assert module.get_hermes_home() == tmp_path / "custom-hermes"
    -
    -    def test_fallback_defaults_to_dot_hermes(self, monkeypatch):
    -        """When hermes_constants is missing and HERMES_HOME unset, default to ~/.hermes."""
    -        monkeypatch.delenv("HERMES_HOME", raising=False)
    -        module = self._load_helper(monkeypatch)
    -        assert module.get_hermes_home() == Path.home() / ".hermes"
    -
    -    def test_fallback_ignores_empty_hermes_home(self, monkeypatch):
    -        """Empty/whitespace HERMES_HOME is treated as unset."""
    -        monkeypatch.setenv("HERMES_HOME", "  ")
    -        module = self._load_helper(monkeypatch)
    -        assert module.get_hermes_home() == Path.home() / ".hermes"
    -
    -    def test_fallback_display_hermes_home_shortens_path(self, monkeypatch):
    -        """Fallback display_hermes_home() uses ~/ shorthand like the real one."""
    -        monkeypatch.delenv("HERMES_HOME", raising=False)
    -        module = self._load_helper(monkeypatch)
    -        assert module.display_hermes_home() == "~/.hermes"
    -
    -    def test_fallback_display_hermes_home_profile_path(self, monkeypatch):
    -        """Fallback display_hermes_home() handles profile paths under ~/."""
    -        monkeypatch.setenv("HERMES_HOME", str(Path.home() / ".hermes/profiles/coder"))
    -        module = self._load_helper(monkeypatch)
    -        assert module.display_hermes_home() == "~/.hermes/profiles/coder"
    -
    -    def test_fallback_display_hermes_home_custom_path(self, monkeypatch):
    -        """Fallback display_hermes_home() returns full path for non-home locations."""
    -        monkeypatch.setenv("HERMES_HOME", "/opt/hermes-custom")
    -        module = self._load_helper(monkeypatch)
    -        assert module.display_hermes_home() == "/opt/hermes-custom"
    -
    -    def test_delegates_to_hermes_constants_when_available(self):
    -        """When hermes_constants IS importable, _hermes_home delegates to it."""
    -        spec = importlib.util.spec_from_file_location(
    -            "_hermes_home_happy", self.HELPER_PATH
    -        )
    -        module = importlib.util.module_from_spec(spec)
    -        assert spec.loader is not None
    -        spec.loader.exec_module(module)
    -        import hermes_constants
    -        assert module.get_hermes_home is hermes_constants.get_hermes_home
    -        assert module.display_hermes_home is hermes_constants.display_hermes_home
    -
    -
    -def _load_setup_module(monkeypatch):
    -    """Load setup.py without stubbing _ensure_deps (for install_deps tests)."""
    -    spec = importlib.util.spec_from_file_location(
    -        "google_workspace_setup_installdeps_test", SCRIPT_PATH
    -    )
    -    module = importlib.util.module_from_spec(spec)
    -    assert spec.loader is not None
    -    spec.loader.exec_module(module)
    -    return module
    -
    -
    -def _force_deps_missing(monkeypatch):
    -    """Make `import googleapiclient` / `import google_auth_oauthlib` fail so
    -    install_deps() proceeds past its early-return short-circuit."""
    -    for name in ("googleapiclient", "google_auth_oauthlib"):
    -        monkeypatch.setitem(sys.modules, name, None)
    -
    -
    -class TestInstallDeps:
    -    """Tests for install_deps() interpreter/installer selection.
    -
    -    Regression coverage for the Hermes Docker image, whose venv is built with
    -    `uv sync` and ships without pip — `sys.executable -m pip install` fails
    -    with `No module named pip`, so install_deps() must fall back to uv.
    -    """
    -
    -    def test_returns_early_when_already_installed(self, monkeypatch):
    -        """If both libs import, no installer subprocess runs at all."""
    -        module = _load_setup_module(monkeypatch)
    -        # Don't force-missing: real test env has the libs importable. Guard
    -        # against any subprocess being spawned.
    -        calls = []
    -        monkeypatch.setattr(
    -            module.subprocess, "check_call", lambda *a, **k: calls.append(a)
    -        )
    -        # google_auth_oauthlib may not be installed in the test env; only run
    -        # this assertion when the early-return path is actually reachable.
    -        try:
    -            import googleapiclient  # noqa: F401
    -            import google_auth_oauthlib  # noqa: F401
    -        except ImportError:
    -            pytest.skip("Google libs not installed in test env")
    -        assert module.install_deps() is True
    -        assert calls == []
    -
    -    def test_uses_pip_when_available(self, monkeypatch):
    -        """When pip works, install_deps succeeds via pip and never calls uv."""
    -        module = _load_setup_module(monkeypatch)
    -        _force_deps_missing(monkeypatch)
    -
    -        recorded = []
    -
    -        def fake_check_call(cmd, **kwargs):
    -            recorded.append(cmd)
    -            # pip path is the first attempt — succeed.
    -            return 0
    -
    -        which_calls = []
    -        monkeypatch.setattr(module.subprocess, "check_call", fake_check_call)
    -        monkeypatch.setattr(
    -            module.shutil, "which", lambda name: which_calls.append(name)
    -        )
    -
    -        assert module.install_deps() is True
    -        assert recorded[0][:3] == [module.sys.executable, "-m", "pip"]
    -        # Control: uv must NOT be consulted when pip succeeds.
    -        assert which_calls == []
    -
    -    def test_falls_back_to_uv_when_pip_missing(self, monkeypatch):
    -        """No pip → uv pip install --python <interpreter> is used."""
    -        module = _load_setup_module(monkeypatch)
    -        _force_deps_missing(monkeypatch)
    -
    -        recorded = []
    -
    -        def fake_check_call(cmd, **kwargs):
    -            recorded.append(cmd)
    -            if cmd[:3] == [module.sys.executable, "-m", "pip"]:
    -                raise module.subprocess.CalledProcessError(1, cmd)
    -            return 0  # uv invocation succeeds
    -
    -        monkeypatch.setattr(module.subprocess, "check_call", fake_check_call)
    -        monkeypatch.setattr(module.shutil, "which", lambda name: "/usr/local/bin/uv")
    -
    -        assert module.install_deps() is True
    -        assert len(recorded) == 2
    -        uv_cmd = recorded[1]
    -        assert uv_cmd[0] == "/usr/local/bin/uv"
    -        assert uv_cmd[1:5] == ["pip", "install", "--python", module.sys.executable]
    -        for pkg in module.REQUIRED_PACKAGES:
    -            assert pkg in uv_cmd
    -
    -    def test_returns_false_when_no_pip_and_no_uv(self, monkeypatch, capsys):
    -        """No pip AND no uv → failure, with the [google] extra hint printed."""
    -        module = _load_setup_module(monkeypatch)
    -        _force_deps_missing(monkeypatch)
    -
    -        def fake_check_call(cmd, **kwargs):
    -            raise module.subprocess.CalledProcessError(1, cmd)
    -
    -        monkeypatch.setattr(module.subprocess, "check_call", fake_check_call)
    -        monkeypatch.setattr(module.shutil, "which", lambda name: None)
    -
    -        assert module.install_deps() is False
    -        out = capsys.readouterr().out
    -        assert "hermes-agent[google]" in out
    -
    -    def test_returns_false_when_uv_fallback_also_fails(self, monkeypatch, capsys):
    -        """uv present but its install fails → failure surfaced (not swallowed)."""
    -        module = _load_setup_module(monkeypatch)
    -        _force_deps_missing(monkeypatch)
    -
    -        def fake_check_call(cmd, **kwargs):
    -            raise module.subprocess.CalledProcessError(1, cmd)
    -
    -        monkeypatch.setattr(module.subprocess, "check_call", fake_check_call)
    -        monkeypatch.setattr(module.shutil, "which", lambda name: "/usr/local/bin/uv")
    -
    -        assert module.install_deps() is False
    -        out = capsys.readouterr().out
    -        assert "via uv" in out
    ```
  - `website/docs/developer-guide/adding-providers.md`

    ```diff
    @@ -127,7 +127,7 @@ See `plugins/model-providers/nvidia/` or `plugins/model-providers/gmi/` as a tem
     
     Use the full checklist below when your provider needs any of the following:
     
    -- OAuth or token refresh (Nous Portal, Codex, Google Gemini, Qwen Portal, Copilot)
    +- OAuth or token refresh (Nous Portal, Codex, Qwen Portal, Copilot)
     - A non-OpenAI API shape that requires a new adapter (Anthropic Messages, Codex Responses)
     - Custom endpoint detection or multi-region probing (z.ai, Kimi)
     - A curated static model catalog or live `/models` fetch
    ```
  - `website/docs/developer-guide/model-provider-plugin.md`

    ```diff
    @@ -195,7 +195,7 @@ Set `profile.api_mode` to match the default your provider ships — it acts as a
     |---|---|---|
     | `api_key` | Single env var carries a static API key | Most providers |
     | `oauth_device_code` | Device-code OAuth flow | — |
    -| `oauth_external` | User signs in elsewhere, tokens land in `auth.json` | Anthropic OAuth, MiniMax OAuth, Gemini Cloud Code, Qwen Portal, Nous Portal |
    +| `oauth_external` | User signs in elsewhere, tokens land in `auth.json` | Anthropic OAuth, MiniMax OAuth, Qwen Portal, Nous Portal |
     | `copilot` | GitHub Copilot token refresh cycle | `copilot` plugin only |
     | `aws_sdk` | AWS SDK credential chain (IAM role, profile, env) | `bedrock` plugin only |
     | `external_process` | Auth handled by a subprocess the agent spawns | `copilot-acp` plugin only |
    ```
  - `website/docs/developer-guide/provider-runtime.md`

    ```diff
    @@ -47,7 +47,7 @@ Current provider families include (see `plugins/model-providers/` for the comple
     - OpenAI Codex
     - Copilot / Copilot ACP
     - Anthropic (native)
    -- Google / Gemini (`gemini`, `google-gemini-cli`, `google-antigravity`)
    +- Google / Gemini (`gemini`)
     - Alibaba / DashScope (`alibaba`, `alibaba-coding-plan`)
     - DeepSeek
     - Z.AI
    ```
  - `website/docs/getting-started/quickstart.md`

    ```diff
    @@ -126,7 +126,6 @@ Good defaults:
     | **AWS Bedrock** | Claude, Nova, Llama, DeepSeek via native Converse API | IAM role or `aws configure` ([guide](../guides/aws-bedrock.md)) |
     | **Azure Foundry** | Azure AI Foundry-hosted models | Set `AZURE_FOUNDRY_API_KEY` + `AZURE_FOUNDRY_BASE_URL` |
     | **Google AI Studio** | Gemini models via direct API | Set `GOOGLE_API_KEY` / `GEMINI_API_KEY` |
    -| **Google Gemini (OAuth)** | Gemini via the `google-gemini-cli` OAuth flow — no key needed | `hermes model` → Google Gemini (OAuth) |
     | **xAI** | Grok models via direct API | Set `XAI_API_KEY` |
     | **xAI Grok OAuth** | SuperGrok / Premium+ subscription, no API key needed | `hermes model` → xAI Grok OAuth |
     | **NovitaAI** | Multi-model API gateway | Set `NOVITA_API_KEY` |
    ```
  - `website/docs/guides/google-gemini.md`

    ```diff
    @@ -1,15 +1,13 @@
     ---
     sidebar_position: 16
     title: "Google Gemini"
    -description: "Use Hermes Agent with Google Gemini — native AI Studio API, API-key setup, OAuth option, tool calling, streaming, and quota guidance"
    +description: "Use Hermes Agent with Google Gemini — native AI Studio API, API-key setup, tool calling, streaming, and quota guidance"
     ---
     
     # Google Gemini
     
     Hermes Agent supports Google Gemini as a native provider using the **Google AI Studio / Gemini API** — not the OpenAI-compatible endpoint. This lets Hermes translate its internal OpenAI-shaped message and tool loop into Gemini's native `generateContent` API while preserving tool calling, streaming, multimodal inputs, and Gemini-specific response metadata.
     
    -Hermes also supports a separate **Google Gemini (OAuth)** provider that uses the same Cloud Code Assist backend as Google's Gemini CLI. Use the API-key provider (`gemini`) for the lowest-risk official API path.
    -
     ## Prerequisites
     
     - **Google AI Studio API key** — create one at [aistudio.google.com/apikey](https://aistudio.google.com/apikey)
    ```

    ```diff
    @@ -100,30 +98,6 @@ If you previously set `GEMINI_BASE_URL` to the `/openai` URL, remove it or chang
     GEMINI_BASE_URL=https://generativelanguage.googleapis.com/v1beta
     ```
     
    -### OAuth Provider
    -
    -Hermes also has a `google-gemini-cli` provider:
    -
    -```bash
    -hermes model
    -# → Choose "Google Gemini (OAuth)"
    -```
    -
    -This uses browser PKCE login and the Cloud Code Assist backend. It can be useful for users who want Gemini CLI-style OAuth, but Hermes shows an explicit warning because Google may treat use of the Gemini CLI OAuth client from third-party software as a policy violation. For production or lowest-risk usage, prefer the API-key provider above.
    -
    -Hermes also supports `google-antigravity` for Antigravity Code Assist:
    -
    -```bash
    -hermes model
    -# → Choose "Google Antigravity (OAuth)"
    -```
    -
    -That provider uses a separate Antigravity OAuth login and stores separate
    -credentials at `~/.hermes/auth/antigravity_oauth.json`. Its model picker uses
    -live Antigravity model discovery, so the list reflects the signed-in account's
    -subscription and can include Antigravity-only Gemini agent models plus other
    -entitled model families.
    -
     ## Available Models
     
     The `hermes model` picker shows Gemini models maintained in Hermes' provider registry. Common choices include:
    ```

    ```diff
    @@ -205,18 +179,8 @@ hermes doctor
     The doctor checks:
     
     - Whether `GOOGLE_API_KEY` or `GEMINI_API_KEY` is available
    -- Whether Gemini OAuth credentials exist for `google-gemini-cli`
    -- Whether Antigravity OAuth credentials exist for `google-antigravity`
     - Whether configured provider credentials can be resolved
     
    -For OAuth quota usage, run this inside a Hermes session:
    -
    -```text
    -/gquota
    -```
    -
    -`/gquota` applies to the `google-gemini-cli` OAuth provider, not the AI Studio API-key provider.
    -
     ## Gateway (Messaging Platforms)
     
     Gemini works with all Hermes gateway platforms (Telegram, Discord, Slack, WhatsApp, LINE, Feishu, etc.). Configure Gemini as your provider, then start the gateway normally:
    ```

    ```diff
    @@ -278,10 +242,6 @@ Change it to the native endpoint or remove the override:
     GEMINI_BASE_URL=https://generativelanguage.googleapis.com/v1beta
     ```
     
    -### OAuth login warning
    -
    -The `google-gemini-cli` provider uses a Gemini CLI / Cloud Code Assist OAuth flow. Hermes warns before starting it because this is distinct from the official AI Studio API-key path. Use `provider: gemini` with `GOOGLE_API_KEY` for the official API-key integration.
    -
     ### Tool calling fails with schema errors
     
     Upgrade Hermes and rerun `hermes model`. The native Gemini adapter sanitizes tool schemas for Gemini's stricter function-declaration format; older builds or custom endpoints may not.
    ```
  - `website/docs/integrations/providers.md`

    ```diff
    @@ -40,7 +40,6 @@ 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 Gemini (OAuth)** | `hermes model` → "Google Gemini (OAuth)" (provider: `google-gemini-cli`, free tier supported, browser PKCE login) |
     | **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
    @@ -49,7 +48,6 @@ You need at least one way to connect to an LLM. Use `hermes model` to switch pro
     | **Qwen OAuth** | `hermes model` → "Qwen OAuth" (provider: `qwen-oauth`; browser PKCE login) |
     | **MiniMax OAuth** | `hermes model` → "MiniMax (OAuth)" (provider: `minimax-oauth`; browser PKCE login) |
     | **StepFun** | `STEPFUN_API_KEY` in `~/.hermes/.env` (provider: `stepfun`) |
    -| **Google Antigravity (OAuth)** | `hermes model` → "Google Antigravity (OAuth)" (provider: `google-antigravity`, aliases: `antigravity`, `antigravity-oauth`, `agy`) |
     | **LM Studio** | `hermes model` → "LM Studio" (provider: `lmstudio`, optional `LM_API_KEY`) |
     | **Custom Endpoint** | `hermes model` → choose "Custom endpoint" (saved in `config.yaml`) |
     
    ```

    ```diff
    @@ -79,64 +77,6 @@ Don't have a subscription yet? Get one at [portal.nousresearch.com/manage-subscr
     **JWT auth (automatic).** Hermes prefers scoped `inference:invoke` JWTs for Portal requests with the legacy opaque session-key path as a fallback. No configuration is required — credentials are managed by the OAuth flow and rotate transparently. Revoked refresh tokens are quarantined to avoid replay loops.
     
     
    -### Google Antigravity via OAuth (`google-antigravity`)
    -
    -The `google-antigravity` provider uses Antigravity's Code Assist backend and
    -Antigravity OAuth scopes. It is a native Hermes integration: Hermes runs its
    -own browser PKCE login, stores credentials under
    -`~/.hermes/auth/antigravity_oauth.json`, and talks directly to the Antigravity
    -Code Assist endpoints. It does not shell out to `agy` for inference, and it
    -does not depend on the Antigravity CLI's local token storage.
    -
    -**Quick start:**
    -
    -```bash
    -hermes model
    -# -> pick "Google Antigravity (OAuth)"
    -# -> browser opens to accounts.google.com, sign in
    -# -> pick one of the models available to your Antigravity account
    -```
    -
    -Hermes discovers Antigravity models from `fetchAvailableModels` after login.
    -The visible list depends on the authenticated account and subscription, and can
    -include Antigravity-only Gemini agent models plus Claude and GPT-OSS entries
    -when the account is entitled. If live discovery fails, Hermes falls back to a
    -small curated list so the provider remains selectable.
    -
    -Supported aliases:
    -
    -```text
    -google-antigravity
    -google-antigravity-oauth
    -antigravity
    -antigravity-oauth
    -antigravity-cli
    -agy
    -agy-cli
    -```
    -
    -Optional overrides:
    -
    -```bash
    -HERMES_ANTIGRAVITY_CLIENT_ID=your-client.apps.googleusercontent.com
    -HERMES_ANTIGRAVITY_CLIENT_SECRET=...
    -HERMES_ANTIGRAVITY_CLI_PATH=/path/to/agy
    -HERMES_ANTIGRAVITY_PROJECT_ID=your-project
    -```
    -
    -If the client ID/secret are not set explicitly, Hermes tries to discover the
    -desktop OAuth client credentials from the installed Antigravity CLI (`agy`) on
    -`PATH`, `HERMES_ANTIGRAVITY_CLI_PATH`, or common Antigravity install/cache
    -locations. Those client credentials are used only to start and refresh Hermes'
    -own OAuth session; Hermes still keeps its access/refresh tokens in `~/.hermes`.
    -
    -:::note Windows credential storage
    -The Antigravity CLI may keep its own login in platform-specific storage such as
    -Windows Credential Manager. Hermes intentionally keeps separate credentials in
    -`~/.hermes` so development profiles and production Hermes profiles do not share
    -tokens accidentally.
    -:::
    -
     :::info Codex Note
     The OpenAI Codex provider authenticates via device code (open a URL, enter a code). Hermes stores the resulting credentials in its own auth store under `~/.hermes/auth.json` and can import existing Codex CLI credentials from `~/.codex/auth.json` when present. No Codex CLI installation is required.
     
    ```

    ```diff
    @@ -592,91 +532,6 @@ You can append routing suffixes to model names: `:fastest` (default), `:cheapest
     
     The base URL can be overridden with `HF_BASE_URL`.
     
    -### Google Gemini via OAuth (`google-gemini-cli`)
    -
    -The `google-gemini-cli` provider uses Google's Cloud Code Assist backend — the
    -same API that Google's own `gemini-cli` tool uses. This supports both the
    -**free tier** (generous daily quota for personal accounts) and **paid tiers**
    -(Standard/Enterprise via a GCP project).
    -
    -**Quick start:**
    -
    -```bash
    -hermes model
    -# → pick "Google Gemini (OAuth)"
    -# → see policy warning, confirm
    -# → browser opens to accounts.google.com, sign in
    -# → done — Hermes auto-provisions your free tier on first request
    -```
    -
    -Hermes ships Google's **public** `gemini-cli` desktop OAuth client by default —
    -the same credentials Google includes in their open-source `gemini-cli`. Desktop
    -OAuth clients are not confidential (PKCE provides the security). You do not
    -need to install `gemini-cli` or register your own GCP OAuth client.
    -
    -**How auth works:**
    -- PKCE Authorization Code flow against `accounts.google.com`
    -- Browser callback at `http://127.0.0.1:8085/oauth2callback` (with ephemeral-port fallback if busy)
    -- Tokens stored at `~/.hermes/auth/google_oauth.json` (chmod 0600, atomic write, cross-process `fcntl` lock)
    -- Automatic refresh 60 s before expiry
    -- Headless environments (SSH, `HERMES_HEADLESS=1`) → paste-mode fallback
    -- Inflight refresh deduplication — two concurrent requests won't double-refresh
    -- `invalid_grant` (revoked refresh) → credential file wiped, user prompted to re-login
    -
    -**How inference works:**
    -- Traffic goes to `https://cloudcode-pa.googleapis.com/v1internal:generateContent`
    -  (or `:streamGenerateContent?alt=sse` for streaming), NOT the paid `v1beta/openai` endpoint
    -- Request body wrapped `{project, model, user_prompt_id, request}`
    -- OpenAI-shaped `messages[]`, `tools[]`, `tool_choice` are translated to Gemini's native
    -  `contents[]`, `tools[].functionDeclarations`, `toolConfig` shape
    -- Responses translated back to OpenAI shape so the rest of Hermes works unchanged
    -
    -**Tiers & project IDs:**
    -
    -| Your situation | What to do |
    -|---|---|
    -| Personal Google account, want free tier | Nothing — sign in, start chatting |
    -| Workspace / Standard / Enterprise account | Set `HERMES_GEMINI_PROJECT_ID` or `GOOGLE_CLOUD_PROJECT` to your GCP project ID |
    -| VPC-SC-protected org | Hermes detects `SECURITY_POLICY_VIOLATED` and forces `standard-tier` automatically |
    -
    -Free tier auto-provisions a Google-managed project on first use. No GCP setup required.
    -
    -**Quota monitoring:**
    -
    -```
    -/gquota
    -```
    -
    -Shows remaining Code Assist quota per model with progress bars:
    -
    -```
    -Gemini Code Assist quota  (project: 123-abc)
    -
    -  gemini-2.5-pro                      ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░   85%
    -  gemini-2.5-flash [input]            ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░   92%
    -```
    -
    -:::warning Policy risk
    -Google considers using the Gemini CLI OAuth client with third-party software a
    -policy violation. Some users have reported account restrictions. For the lowest-risk
    -experience, use your own API key via the `gemini` provider instead. Hermes shows
    -an upfront warning and requires explicit confirmation before OAuth begins.
    -:::
    -
    -**Custom OAuth client (optional):**
    -
    -If you'd rather register your own Google OAuth client — e.g., to keep quota
    -and consent scoped to your own GCP project — set:
    -
    -```bash
    -HERMES_GEMINI_CLIENT_ID=your-client.apps.googleusercontent.com
    -HERMES_GEMINI_CLIENT_SECRET=...   # optional for Desktop clients
    -```
    -
    -Register a **Desktop app** OAuth client at
    -[console.cloud.google.com/apis/credentials](https://console.cloud.google.com/apis/credentials)
    -with the Generative Language API enabled.
    -
     ## Custom & Self-Hosted LLM Providers
     
     Hermes Agent works with **any OpenAI-compatible API endpoint**. If a server implements `/v1/chat/completions`, you can point Hermes at it. This means you can use local models, GPU inference servers, multi-provider routers, or any third-party API.
    ```

    ```diff
    @@ -1591,7 +1446,7 @@ fallback_model:
     
     When activated, the fallback swaps the model and provider mid-session without losing your conversation. The chain is tried entry-by-entry; activation is one-shot per session.
     
    -Supported providers: `openrouter`, `nous`, `novita`, `openai-codex`, `copilot`, `copilot-acp`, `anthropic`, `gemini`, `google-gemini-cli`, `google-antigravity`, `qwen-oauth`, `huggingface`, `zai`, `kimi-coding`, `kimi-coding-cn`, `minimax`, `minimax-cn`, `minimax-oauth`, `deepseek`, `nvidia`, `xai`, `xai-oauth`, `ollama-cloud`, `bedrock`, `azure-foundry`, `opencode-zen`, `opencode-go`, `kilocode`, `xiaomi`, `arcee`, `gmi`, `stepfun`, `lmstudio`, `alibaba`, `alibaba-coding-plan`, `tencent-tokenhub`, `custom`.
    +Supported providers: `openrouter`, `nous`, `novita`, `openai-codex`, `copilot`, `copilot-acp`, `anthropic`, `gemini`, `qwen-oauth`, `huggingface`, `zai`, `kimi-coding`, `kimi-coding-cn`, `minimax`, `minimax-cn`, `minimax-oauth`, `deepseek`, `nvidia`, `xai`, `xai-oauth`, `ollama-cloud`, `bedrock`, `azure-foundry`, `opencode-zen`, `opencode-go`, `kilocode`, `xiaomi`, `arcee`, `gmi`, `stepfun`, `lmstudio`, `alibaba`, `alibaba-coding-plan`, `tencent-tokenhub`, `custom`.
     
     :::tip
     Fallback is configured exclusively through `config.yaml` — or interactively via `hermes fallback`. For full details on when it triggers, how the chain advances, and how it interacts with auxiliary tasks and delegation, see [Fallback Providers](/user-guide/features/fallback-providers).
    ```
  - `website/docs/reference/cli-commands.md`

    ```diff
    @@ -100,7 +100,7 @@ Common options:
     | `-q`, `--query "..."` | One-shot, non-interactive prompt. |
     | `-m`, `--model <model>` | Override the model for this run. |
     | `-t`, `--toolsets <csv>` | Enable a comma-separated set of toolsets. |
    -| `--provider <provider>` | Force a provider: `auto`, `openrouter`, `nous`, `openai-codex`, `copilot-acp`, `copilot`, `anthropic`, `gemini`, `google-gemini-cli`, `google-antigravity` (aliases: `antigravity`, `antigravity-oauth`, `agy`), `huggingface`, `novita` (aliases `novita-ai`, `novitaai`), `openai-api`, `zai`, `kimi-coding`, `kimi-coding-cn`, `minimax`, `minimax-cn`, `minimax-oauth`, `kilocode`, `xiaomi`, `arcee`, `gmi`, `alibaba`, `alibaba-coding-plan` (alias `alibaba_coding`), `deepseek`, `nvidia`, `ollama-cloud`, `xai` (alias `grok`), `xai-oauth` (alias `grok-oauth`), `qwen-oauth`, `bedrock`, `opencode-zen`, `opencode-go`, `azure-foundry`, `lmstudio`, `stepfun`, `tencent-tokenhub` (alias `tencent`, `tokenhub`). |
    +| `--provider <provider>` | Force a provider: `auto`, `openrouter`, `nous`, `openai-codex`, `copilot-acp`, `copilot`, `anthropic`, `gemini`, `huggingface`, `novita` (aliases `novita-ai`, `novitaai`), `openai-api`, `zai`, `kimi-coding`, `kimi-coding-cn`, `minimax`, `minimax-cn`, `minimax-oauth`, `kilocode`, `xiaomi`, `arcee`, `gmi`, `alibaba`, `alibaba-coding-plan` (alias `alibaba_coding`), `deepseek`, `nvidia`, `ollama-cloud`, `xai` (alias `grok`), `xai-oauth` (alias `grok-oauth`), `qwen-oauth`, `bedrock`, `opencode-zen`, `opencode-go`, `azure-foundry`, `lmstudio`, `stepfun`, `tencent-tokenhub` (alias `tencent`, `tokenhub`). |
     | `-s`, `--skills <name>` | Preload one or more skills for the session (can be repeated or comma-separated). |
     | `-v`, `--verbose` | Verbose output. |
     | `-Q`, `--quiet` | Programmatic mode: suppress banner/spinner/tool previews. |
    ```
  - `website/docs/reference/environment-variables.md`

    ```diff
    @@ -67,13 +67,6 @@ Hermes reads environment variables from the process environment and, for user-ma
     | `GOOGLE_API_KEY` | Google AI Studio API key ([aistudio.google.com/app/apikey](https://aistudio.google.com/app/apikey)) |
     | `GEMINI_API_KEY` | Alias for `GOOGLE_API_KEY` |
     | `GEMINI_BASE_URL` | Override Google AI Studio base URL |
    -| `HERMES_GEMINI_CLIENT_ID` | OAuth client ID for `google-gemini-cli` PKCE login (optional; defaults to Google's public gemini-cli client) |
    -| `HERMES_GEMINI_CLIENT_SECRET` | OAuth client secret for `google-gemini-cli` (optional) |
    -| `HERMES_GEMINI_PROJECT_ID` | GCP project ID for paid Gemini tiers (free tier auto-provisions) |
    -| `HERMES_ANTIGRAVITY_CLIENT_ID` | OAuth client ID for `google-antigravity` PKCE login (optional; discovered from installed `agy` when omitted) |
    -| `HERMES_ANTIGRAVITY_CLIENT_SECRET` | OAuth client secret for `google-antigravity` (optional; discovered from installed `agy` when omitted) |
    -| `HERMES_ANTIGRAVITY_CLI_PATH` | Path to the `agy` executable or install file used for Antigravity OAuth client credential discovery |
    -| `HERMES_ANTIGRAVITY_PROJECT_ID` | GCP project ID for Antigravity Code Assist when you want to pin one explicitly |
     | `ANTHROPIC_API_KEY` | Anthropic Console API key ([console.anthropic.com](https://console.anthropic.com/)) |
     | `ANTHROPIC_BASE_URL` | Override the Anthropic API base URL |
     | `ANTHROPIC_TOKEN` | Manual or legacy Anthropic OAuth/setup-token override |
    ```
  - `website/docs/reference/faq.md`

    ```diff
    @@ -20,7 +20,7 @@ Hermes Agent works with any OpenAI-compatible API. Supported providers include:
     - **[Nous Portal](/integrations/nous-portal)** — Nous Research's subscription gateway — 300+ models plus web/image/TTS/browser through one OAuth login (recommended for newcomers)
     - **OpenAI** — GPT-5.4, GPT-5-codex, GPT-4.1, GPT-4o, etc.
     - **Anthropic** — Claude models (direct API, OAuth via `hermes auth add anthropic`, OpenRouter, or any compatible proxy)
    -- **Google** — Gemini models (direct API via `gemini` provider, the `google-gemini-cli` OAuth provider, the `google-antigravity` OAuth provider, OpenRouter, or compatible proxy)
    +- **Google** — Gemini models (direct API via `gemini` provider, OpenRouter, or compatible proxy)
     - **z.ai / ZhipuAI** — GLM models
     - **Kimi / Moonshot AI** — Kimi models
     - **MiniMax** — global and China endpoints
    ```
  - `website/docs/reference/slash-commands.md`

    ```diff
    @@ -115,7 +115,6 @@ Type `/` in the CLI to open the autocomplete menu. Built-in commands are case-in
     | `/image <path>` | Attach a local image file for your next prompt. |
     | `/debug` | Upload debug report (system info + logs) and get shareable links. Also available in messaging. |
     | `/profile` | Show active profile name and home directory |
    -| `/gquota` | Show Google Gemini Code Assist quota usage with progress bars (only available when the `google-gemini-cli` provider is active). |
     
     ### Exit
     
    ```

    ```diff
    @@ -246,7 +245,7 @@ The messaging gateway supports the following built-in commands inside Telegram,
     
     ## Notes
     
    -- `/skin`, `/snapshot`, `/gquota`, `/reload`, `/tools`, `/toolsets`, `/browser`, `/config`, `/cron`, `/platforms`, `/paste`, `/image`, `/statusbar`, `/plugins`, `/busy`, `/indicator`, `/redraw`, `/clear`, `/history`, `/save`, `/copy`, `/handoff`, `/billing`, and `/quit` are **CLI-only** commands.
    +- `/skin`, `/snapshot`, `/reload`, `/tools`, `/toolsets`, `/browser`, `/config`, `/cron`, `/platforms`, `/paste`, `/image`, `/statusbar`, `/plugins`, `/busy`, `/indicator`, `/redraw`, `/clear`, `/history`, `/save`, `/copy`, `/handoff`, `/billing`, and `/quit` are **CLI-only** commands.
     - `/skills` is **CLI-only for search/browse/install**; its write-approval review subcommands (`pending`, `approve`, `reject`, `diff`, `approval`) also work on messaging platforms when `skills.write_approval` is on. `/memory` works on **both** surfaces.
     - `/verbose` is **CLI-only by default**, but can be enabled for messaging platforms by setting `display.tool_progress_command: true` in `config.yaml`. When enabled, it cycles the `display.tool_progress` mode and saves to config.
     - `/sethome`, `/update`, `/restart`, `/approve`, `/deny`, `/topic`, `/platform`, and `/commands` are **messaging-only** commands.
    ```
  - `website/docs/user-guide/configuration.md`

    ```diff
    @@ -959,7 +959,7 @@ Every model slot in Hermes — auxiliary tasks, compression, fallback — uses t
     
     When `base_url` is set, Hermes ignores the provider and calls that endpoint directly (using `api_key` or `OPENAI_API_KEY` for auth). When only `provider` is set, Hermes uses that provider's built-in auth and base URL.
     
    -Available providers for auxiliary tasks: `auto`, `main`, plus any provider in the [provider registry](/reference/environment-variables) — `openrouter`, `nous`, `openai-codex`, `copilot`, `copilot-acp`, `anthropic`, `gemini`, `google-gemini-cli`, `google-antigravity`, `qwen-oauth`, `zai`, `kimi-coding`, `kimi-coding-cn`, `minimax`, `minimax-cn`, `minimax-oauth`, `deepseek`, `nvidia`, `xai`, `xai-oauth`, `ollama-cloud`, `alibaba`, `bedrock`, `huggingface`, `arcee`, `xiaomi`, `kilocode`, `opencode-zen`, `opencode-go`, `azure-foundry` — or any named custom provider from your `custom_providers` list (e.g. `provider: "beans"`).
    +Available providers for auxiliary tasks: `auto`, `main`, plus any provider in the [provider registry](/reference/environment-variables) — `openrouter`, `nous`, `openai-codex`, `copilot`, `copilot-acp`, `anthropic`, `gemini`, `qwen-oauth`, `zai`, `kimi-coding`, `kimi-coding-cn`, `minimax`, `minimax-cn`, `minimax-oauth`, `deepseek`, `nvidia`, `xai`, `xai-oauth`, `ollama-cloud`, `alibaba`, `bedrock`, `huggingface`, `arcee`, `xiaomi`, `kilocode`, `opencode-zen`, `opencode-go`, `azure-foundry` — or any named custom provider from your `custom_providers` list (e.g. `provider: "beans"`).
     
     :::tip MiniMax OAuth
     `minimax-oauth` logs in via browser OAuth (no API key needed). Run `hermes model` and select **MiniMax (OAuth)** to authenticate. Auxiliary tasks use `MiniMax-M2.7-highspeed` automatically. See the [MiniMax OAuth guide](../guides/minimax-oauth.md).
    ```
  - `website/docs/user-guide/features/fallback-providers.md`

    ```diff
    @@ -62,8 +62,6 @@ Each entry requires both `provider` and `model`. Entries missing either field ar
     | GMI Cloud | `gmi` | `GMI_API_KEY` (optional: `GMI_BASE_URL`) |
     | StepFun | `stepfun` | `STEPFUN_API_KEY` (optional: `STEPFUN_BASE_URL`) |
     | Ollama Cloud | `ollama-cloud` | `OLLAMA_API_KEY` |
    -| Google Gemini (OAuth) | `google-gemini-cli` | `hermes model` (Google OAuth; optional: `HERMES_GEMINI_PROJECT_ID`) |
    -| Google Antigravity (OAuth) | `google-antigravity` | `hermes model` (Antigravity OAuth; optional: `HERMES_ANTIGRAVITY_PROJECT_ID`) |
     | Google AI Studio | `gemini` | `GOOGLE_API_KEY` (alias: `GEMINI_API_KEY`) |
     | xAI (Grok) | `xai` (alias `grok`) | `XAI_API_KEY` (optional: `XAI_BASE_URL`) |
     | xAI Grok OAuth (SuperGrok) | `xai-oauth` (alias `grok-oauth`) | `hermes model` → xAI Grok OAuth (browser login; SuperGrok subscription) |
    ```
  - `website/docs/user-guide/skills/bundled/autonomous-ai-agents/autonomous-ai-agents-hermes-agent.md`

    ```diff
    @@ -343,7 +343,6 @@ The registry of record is `hermes_cli/commands.py` — every consumer
     /commands [page]     Browse all commands (gateway)
     /usage               Token usage
     /insights [days]     Usage analytics
    -/gquota              Show Google Gemini Code Assist quota usage (CLI)
     /status              Session info (gateway)
     /profile             Active profile info
     /debug               Upload debug report (system info + logs) and get shareable links
    ```
  - `website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/developer-guide/adding-providers.md`

    ```diff
    @@ -127,7 +127,7 @@ Hermes 已经可以通过自定义 provider 路径与任何 OpenAI 兼容的端
     
     当你的 provider 需要以下任何内容时，使用下面的完整清单：
     
    -- OAuth 或 token 刷新（Nous Portal、Codex、Google Gemini、Qwen Portal、Copilot）
    +- OAuth 或 token 刷新（Nous Portal、Codex、Qwen Portal、Copilot）
     - 需要新适配器的非 OpenAI API 格式（Anthropic Messages、Codex Responses）
     - 自定义端点检测或多区域探测（z.ai、Kimi）
     - 精选的静态模型目录或实时 `/models` 获取
    ```
  - `website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/developer-guide/model-provider-plugin.md`

    ```diff
    @@ -194,7 +194,7 @@ register_provider(ProviderProfile(
     |---|---|---|
     | `api_key` | 单个环境变量携带静态 API key | 大多数提供商 |
     | `oauth_device_code` | 设备码 OAuth 流程 | — |
    -| `oauth_external` | 用户在其他地方登录，token 存入 `auth.json` | Anthropic OAuth、MiniMax OAuth、Gemini Cloud Code、Qwen Portal、Nous Portal |
    +| `oauth_external` | 用户在其他地方登录，token 存入 `auth.json` | Anthropic OAuth、MiniMax OAuth、Qwen Portal、Nous Portal |
     | `copilot` | GitHub Copilot token 刷新周期 | 仅 `copilot` 插件 |
     | `aws_sdk` | AWS SDK 凭据链（IAM role、profile、env） | 仅 `bedrock` 插件 |
     | `external_process` | 认证由 agent 启动的子进程处理 | 仅 `copilot-acp` 插件 |
    ```
  - `website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/developer-guide/provider-runtime.md`

    ```diff
    @@ -47,7 +47,7 @@ Hermes 拥有一个共享的 provider 运行时解析器，用于以下场景：
     - OpenAI Codex
     - Copilot / Copilot ACP
     - Anthropic（原生）
    -- Google / Gemini（`gemini`、`google-gemini-cli`）
    +- Google / Gemini（`gemini`）
     - Alibaba / DashScope（`alibaba`、`alibaba-coding-plan`）
     - DeepSeek
     - Z.AI
    ```
  - `website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/guides/google-gemini.md`

    ```diff
    @@ -1,15 +1,13 @@
     ---
     sidebar_position: 16
     title: "Google Gemini"
    -description: "将 Hermes Agent 与 Google Gemini 配合使用——原生 AI Studio API、API 密钥配置、OAuth 选项、工具调用、流式传输及配额说明"
    +description: "将 Hermes Agent 与 Google Gemini 配合使用——原生 AI Studio API、API 密钥配置、工具调用、流式传输及配额说明"
     ---
     
     # Google Gemini
     
     Hermes Agent 通过 **Google AI Studio / Gemini API** 原生支持 Google Gemini——而非 OpenAI 兼容端点。这使 Hermes 能够将其内部 OpenAI 格式的消息和工具循环转换为 Gemini 原生的 `generateContent` API，同时保留工具调用、流式传输、多模态输入以及 Gemini 特有的响应元数据。
     
    -Hermes 还支持独立的 **Google Gemini（OAuth）** provider，使用与 Google Gemini CLI 相同的 Cloud Code Assist 后端。如需最低风险的官方 API 路径，请使用 API 密钥 provider（`gemini`）。
    -
     ## 前提条件
     
     - **Google AI Studio API 密钥** — 在 [aistudio.google.com/apikey](https://aistudio.google.com/apikey) 创建
    ```

    ```diff
    @@ -100,17 +98,6 @@ https://generativelanguage.googleapis.com/v1beta/openai/
     GEMINI_BASE_URL=https://generativelanguage.googleapis.com/v1beta
     ```
     
    -### OAuth Provider
    -
    -Hermes 还提供 `google-gemini-cli` provider：
    -
    -```bash
    -hermes model
    -# → 选择 "Google Gemini (OAuth)"
    -```
    -
    -该方式使用浏览器 PKCE 登录和 Cloud Code Assist 后端。对于希望使用 Gemini CLI 风格 OAuth 的用户可能有用，但 Hermes 会显示明确警告，因为 Google 可能将第三方软件使用 Gemini CLI OAuth 客户端的行为视为违反政策。对于生产环境或最低风险使用场景，请优先使用上述 API 密钥 provider。
    -
     ## 可用模型
     
     `hermes model` 选择器显示 Hermes provider 注册表中维护的 Gemini 模型。常见选项包括：
    ```

    ```diff
    @@ -192,17 +179,8 @@ hermes doctor
     doctor 命令检查：
     
     - `GOOGLE_API_KEY` 或 `GEMINI_API_KEY` 是否可用
    -- `google-gemini-cli` 的 Gemini OAuth 凭据是否存在
     - 已配置的 provider 凭据是否可以解析
     
    -如需查看 OAuth 配额使用情况，请在 Hermes 会话中运行：
    -
    -```text
    -/gquota
    -```
    -
    -`/gquota` 适用于 `google-gemini-cli` OAuth provider，不适用于 AI Studio API 密钥 provider。
    -
     ## Gateway（消息平台）
     
     Gemini 可与所有 Hermes gateway 平台配合使用（Telegram、Discord、Slack、WhatsApp、LINE、飞书等）。将 Gemini 配置为你的 provider，然后正常启动 gateway：
    ```

    ```diff
    @@ -264,10 +242,6 @@ GEMINI_BASE_URL=https://generativelanguage.googleapis.com/v1beta/openai/
     GEMINI_BASE_URL=https://generativelanguage.googleapis.com/v1beta
     ```
     
    -### OAuth 登录警告
    -
    -`google-gemini-cli` provider 使用 Gemini CLI / Cloud Code Assist OAuth 流程。Hermes 在启动前会发出警告，因为这与官方 AI Studio API 密钥路径不同。如需官方 API 密钥集成，请使用 `provider: gemini` 配合 `GOOGLE_API_KEY`。
    -
     ### 工具调用因 schema 错误而失败
     
     升级 Hermes 并重新运行 `hermes model`。原生 Gemini 适配器会针对 Gemini 更严格的函数声明格式对工具 schema 进行清理；旧版本或自定义端点可能不支持此功能。
    ```
  - `website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/integrations/providers.md`

    ```diff
    @@ -40,7 +40,6 @@ sidebar_position: 1
     | **DeepSeek** | `~/.hermes/.env` 中的 `DEEPSEEK_API_KEY`（provider: `deepseek`） |
     | **Hugging Face** | `~/.hermes/.env` 中的 `HF_TOKEN`（provider: `huggingface`，别名：`hf`） |
     | **Google / Gemini** | `~/.hermes/.env` 中的 `GOOGLE_API_KEY`（或 `GEMINI_API_KEY`）（provider: `gemini`） |
    -| **Google Gemini（OAuth）** | `hermes model` → "Google Gemini (OAuth)"（provider: `google-gemini-cli`，支持免费层，浏览器 PKCE 登录） |
     | **LM Studio** | `hermes model` → "LM Studio"（provider: `lmstudio`，可选 `LM_API_KEY`） |
     | **自定义端点** | `hermes model` → 选择"Custom endpoint"（保存在 `config.yaml`） |
     
    ```

    ```diff
    @@ -512,79 +511,6 @@ model:
     
     基础 URL 可通过 `HF_BASE_URL` 覆盖。
     
    -### 通过 OAuth 使用 Google Gemini（`google-gemini-cli`）
    -
    -`google-gemini-cli` 提供商使用 Google 的 Cloud Code Assist 后端——与 Google 自己的 `gemini-cli` 工具使用的 API 相同。支持**免费层**（个人账户每日配额充足）和**付费层**（通过 GCP 项目的 Standard/Enterprise）。
    -
    -**快速开始：**
    -
    -```bash
    -hermes model
    -# → 选择"Google Gemini (OAuth)"
    -# → 查看政策警告，确认
    -# → 浏览器打开 accounts.google.com，登录
    -# → 完成——Hermes 在首次请求时自动开通免费层
    -```
    -
    -Hermes 默认使用 Google 的**公开** `gemini-cli` 桌面 OAuth 客户端——与 Google 在其开源 `gemini-cli` 中包含的凭据相同。桌面 OAuth 客户端不是机密客户端（PKCE 提供安全保障）。你无需安装 `gemini-cli` 或注册自己的 GCP OAuth 客户端。
    -
    -**认证工作原理：**
    -- 针对 `accounts.google.com` 的 PKCE 授权码流程
    -- 浏览器回调地址 `http://127.0.0.1:8085/oauth2callback`（端口占用时自动回退到临时端口）
    -- Token 存储在 `~/.hermes/auth/google_oauth.json`（chmod 0600，原子写入，跨进程 `fcntl` 锁）
    -- 到期前 60 秒自动刷新
    -- 无头环境（SSH、`HERMES_HEADLESS=1`）→ 粘贴模式回退
    -- 并发刷新去重——两个并发请求不会触发双重刷新
    -- `invalid_grant`（刷新 token 被撤销）→ 凭据文件被清除，提示用户重新登录
    -
    -**推理工作原理：**
    -- 流量发送到 `https://cloudcode-pa.googleapis.com/v1internal:generateContent`
    -  （流式传输为 `:streamGenerateContent?alt=sse`），而非付费的 `v1beta/openai` 端点
    -- 请求体封装为 `{project, model, user_prompt_id, request}`
    -- OpenAI 格式的 `messages[]`、`tools[]`、`tool_choice` 被转换为 Gemini 原生的
    -  `contents[]`、`tools[].functionDeclarations`、`toolConfig` 格式
    -- 响应转换回 OpenAI 格式，Hermes 其余部分无感知
    -
    -**层级与项目 ID：**
    -
    -| 你的情况 | 操作 |
    -|---|---|
    -| 个人 Google 账户，使用免费层 | 无需操作——登录即可开始聊天 |
    -| Workspace / Standard / Enterprise 账户 | 将 `HERMES_GEMINI_PROJECT_ID` 或 `GOOGLE_CLOUD_PROJECT` 设置为你的 GCP 项目 ID |
    -| VPC-SC 保护的组织 | Hermes 检测到 `SECURITY_POLICY_VIOLATED` 后自动强制使用 `standard-tier` |
    -
    -免费层在首次使用时自动开通 Google 托管项目。无需 GCP 配置。
    -
    -**配额监控：**
    -
    -```
    -/gquota
    -```
    -
    -以进度条显示每个模型的剩余 Code Assist 配额：
    -
    -```
    -Gemini Code Assist quota  (project: 123-abc)
    -
    -  gemini-2.5-pro                      ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░   85%
    -  gemini-2.5-flash [input]            ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░   92%
    -```
    -
    -:::warning 政策风险
    -Google 认为将 Gemini CLI OAuth 客户端用于第三方软件违反政策。部分用户反映账户受到限制。为降低风险，建议改用 `gemini` 提供商并通过 API key 访问。Hermes 会在 OAuth 开始前显示警告并要求明确确认。
    -:::
    -
    -**自定义 OAuth 客户端（可选）：**
    -
    -如果你希望注册自己的 Google OAuth 客户端——例如将配额和授权范围限定在自己的 GCP 项目内——请设置：
    -
    -```bash
    -HERMES_GEMINI_CLIENT_ID=your-client.apps.googleusercontent.com
    -HERMES_GEMINI_CLIENT_SECRET=...   # 桌面客户端可选
    -```
    -
    -在 [console.cloud.google.com/apis/credentials](https://console.cloud.google.com/apis/credentials) 注册一个**桌面应用** OAuth 客户端，并启用 Generative Language API。
    -
     ## 自定义与自托管 LLM 提供商
     
     Hermes Agent 可与**任何 OpenAI 兼容 API 端点**配合使用。只要服务器实现了 `/v1/chat/completions`，就可以将 Hermes 指向它。这意味着你可以使用本地模型、GPU 推理服务器、多提供商路由器或任何第三方 API。
    ```

    ```diff
    @@ -1477,7 +1403,7 @@ fallback_model:
     
     激活时，故障转移在不丢失对话的情况下中途切换模型和提供商。链按条目逐一尝试；每个会话激活一次。
     
    -支持的提供商：`openrouter`、`nous`、`openai-codex`、`copilot`、`copilot-acp`、`anthropic`、`gemini`、`google-gemini-cli`、`qwen-oauth`、`huggingface`、`zai`、`kimi-coding`、`kimi-coding-cn`、`minimax`、`minimax-cn`、`minimax-oauth`、`deepseek`、`nvidia`、`xai`、`xai-oauth`、`ollama-cloud`、`bedrock`、`azure-foundry`、`opencode-zen`、`opencode-go`、`kilocode`、`xiaomi`、`arcee`、`gmi`、`stepfun`、`lmstudio`、`alibaba`、`alibaba-coding-plan`、`tencent-tokenhub`、`custom`。
    +支持的提供商：`openrouter`、`nous`、`openai-codex`、`copilot`、`copilot-acp`、`anthropic`、`gemini`、`qwen-oauth`、`huggingface`、`zai`、`kimi-coding`、`kimi-coding-cn`、`minimax`、`minimax-cn`、`minimax-oauth`、`deepseek`、`nvidia`、`xai`、`xai-oauth`、`ollama-cloud`、`bedrock`、`azure-foundry`、`opencode-zen`、`opencode-go`、`kilocode`、`xiaomi`、`arcee`、`gmi`、`stepfun`、`lmstudio`、`alibaba`、`alibaba-coding-plan`、`tencent-tokenhub`、`custom`。
     
     :::tip
     故障转移仅通过 `config.yaml` 配置——或通过 `hermes fallback` 交互式配置。有关触发时机、链推进方式以及与辅助任务和委托的交互，参见[故障转移提供商](/user-guide/features/fallback-providers)。
    ```
  - `website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/reference/cli-commands.md`

    ```diff
    @@ -95,7 +95,7 @@ hermes chat [options]
     | `-q`, `--query "..."` | 单次非交互式 prompt。 |
     | `-m`, `--model <model>` | 覆盖本次运行的模型。 |
     | `-t`, `--toolsets <csv>` | 启用逗号分隔的 toolset 集合。 |
    -| `--provider <provider>` | 强制指定 provider：`auto`、`openrouter`、`nous`、`openai-codex`、`copilot-acp`、`copilot`、`anthropic`、`gemini`、`google-gemini-cli`、`huggingface`、`novita`（别名 `novita-ai`、`novitaai`）、`openai-api`、`zai`、`kimi-coding`、`kimi-coding-cn`、`minimax`、`minimax-cn`、`minimax-oauth`、`kilocode`、`xiaomi`、`arcee`、`gmi`、`alibaba`、`alibaba-coding-plan`（别名 `alibaba_coding`）、`deepseek`、`nvidia`、`ollama-cloud`、`xai`（别名 `grok`）、`xai-oauth`（别名 `grok-oauth`）、`qwen-oauth`、`bedrock`、`opencode-zen`、`opencode-go`、`azure-foundry`、`lmstudio`、`stepfun`、`tencent-tokenhub`（别名 `tencent`、`tokenhub`）。 |
    +| `--provider <provider>` | 强制指定 provider：`auto`、`openrouter`、`nous`、`openai-codex`、`copilot-acp`、`copilot`、`anthropic`、`gemini`、`huggingface`、`novita`（别名 `novita-ai`、`novitaai`）、`openai-api`、`zai`、`kimi-coding`、`kimi-coding-cn`、`minimax`、`minimax-cn`、`minimax-oauth`、`kilocode`、`xiaomi`、`arcee`、`gmi`、`alibaba`、`alibaba-coding-plan`（别名 `alibaba_coding`）、`deepseek`、`nvidia`、`ollama-cloud`、`xai`（别名 `grok`）、`xai-oauth`（别名 `grok-oauth`）、`qwen-oauth`、`bedrock`、`opencode-zen`、`opencode-go`、`azure-foundry`、`lmstudio`、`stepfun`、`tencent-tokenhub`（别名 `tencent`、`tokenhub`）。 |
     | `-s`, `--skills <name>` | 为会话预加载一个或多个 skill（可重复或逗号分隔）。 |
     | `-v`, `--verbose` | 详细输出。 |
     | `-Q`, `--quiet` | 程序化模式：抑制横幅/spinner/工具预览。 |
    ```
  - `website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/reference/environment-variables.md`

    ```diff
    @@ -63,9 +63,6 @@ description: "Hermes Agent 使用的所有环境变量完整参考"
     | `GOOGLE_API_KEY` | Google AI Studio API 密钥（[aistudio.google.com/app/apikey](https://aistudio.google.com/app/apikey)） |
     | `GEMINI_API_KEY` | `GOOGLE_API_KEY` 的别名 |
     | `GEMINI_BASE_URL` | 覆盖 Google AI Studio base URL |
    -| `HERMES_GEMINI_CLIENT_ID` | `google-gemini-cli` PKCE 登录的 OAuth 客户端 ID（可选；默认使用 Google 公共 gemini-cli 客户端） |
    -| `HERMES_GEMINI_CLIENT_SECRET` | `google-gemini-cli` 的 OAuth 客户端密钥（可选） |
    -| `HERMES_GEMINI_PROJECT_ID` | 付费 Gemini 层级的 GCP 项目 ID（免费层级自动配置） |
     | `ANTHROPIC_API_KEY` | Anthropic Console API 密钥（[console.anthropic.com](https://console.anthropic.com/)） |
     | `ANTHROPIC_TOKEN` | 手动或旧版 Anthropic OAuth/setup-token 覆盖 |
     | `DASHSCOPE_API_KEY` | Qwen Cloud（阿里巴巴 DashScope）Qwen 模型 API 密钥（[modelstudio.console.alibabacloud.com](https://modelstudio.console.alibabacloud.com/)） |
    ```
  - `website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/reference/faq.md`

    ```diff
    @@ -20,7 +20,7 @@ Hermes Agent 可与任何兼容 OpenAI 的 API 配合使用。支持的提供商
     - **Nous Portal** — Nous Research 自有推理端点
     - **OpenAI** — GPT-5.4、GPT-5-codex、GPT-4.1、GPT-4o 等
     - **Anthropic** — Claude 模型（直接 API、通过 `hermes auth add anthropic` 进行 OAuth、OpenRouter 或任何兼容代理）
    -- **Google** — Gemini 模型（通过 `gemini` 提供商直接调用 API、`google-gemini-cli` OAuth 提供商、OpenRouter 或兼容代理）
    +- **Google** — Gemini 模型（通过 `gemini` 提供商直接调用 API、OpenRouter 或兼容代理）
     - **z.ai / ZhipuAI** — GLM 模型
     - **Kimi / Moonshot AI** — Kimi 模型
     - **MiniMax** — 全球及中国区端点
    ```
  - `website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/reference/slash-commands.md`

    ```diff
    @@ -115,7 +115,6 @@ Hermes 有两个斜杠命令入口，均由 `hermes_cli/commands.py` 中的中
     | `/image <path>` | 为下一条 prompt 附加本地图片文件。 |
     | `/debug` | 上传调试报告（系统信息 + 日志）并获取可分享链接。消息平台中也可用。 |
     | `/profile` | 显示活动 profile 名称和主目录 |
    -| `/gquota` | 以进度条形式显示 Google Gemini Code Assist 配额用量（仅在 `google-gemini-cli` 提供商激活时可用）。 |
     
     ### 退出
     
    ```

    ```diff
    @@ -246,7 +245,7 @@ hermes config set model.aliases.grok x-ai/grok-4
     
     ## 注意事项
     
    -- `/skin`、`/snapshot`、`/gquota`、`/reload`、`/tools`、`/toolsets`、`/browser`、`/config`、`/cron`、`/platforms`、`/paste`、`/image`、`/statusbar`、`/plugins`、`/busy`、`/indicator`、`/redraw`、`/clear`、`/history`、`/save`、`/copy`、`/handoff`、`/billing` 和 `/quit` 是**仅限 CLI** 的命令。
    +- `/skin`、`/snapshot`、`/reload`、`/tools`、`/toolsets`、`/browser`、`/config`、`/cron`、`/platforms`、`/paste`、`/image`、`/statusbar`、`/plugins`、`/busy`、`/indicator`、`/redraw`、`/clear`、`/history`、`/save`、`/copy`、`/handoff`、`/billing` 和 `/quit` 是**仅限 CLI** 的命令。
     - `/skills` **仅在搜索/浏览/安装时属于 CLI-only**；其写入审批子命令（`pending`、`approve`、`reject`、`diff`、`approval`）在 `skills.write_approval` 开启时也可在消息平台使用。`/memory` 可在**两个表面**使用。
     - `/verbose` **默认仅限 CLI**，但可通过在 `config.yaml` 中设置 `display.tool_progress_command: true` 为消息平台启用。启用后，它会循环切换 `display.tool_progress` 模式并保存到配置。
     - `/sethome`、`/update`、`/restart`、`/approve`、`/deny`、`/topic`、`/platform` 和 `/commands` 是**仅限消息平台**的命令。
    ```
  - `website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/configuration.md`

    ```diff
    @@ -774,7 +774,7 @@ Hermes 中的每个模型槽位 —— 辅助任务、压缩、回退 —— 使
     
     当设置 `base_url` 时，Hermes 忽略 provider 并直接调用该端点（使用 `api_key` 或 `OPENAI_API_KEY` 进行认证）。当仅设置 `provider` 时，Hermes 使用该 provider 的内置认证和基础 URL。
     
    -辅助任务的可用 providers：`auto`、`main`，以及[provider 注册表](/reference/environment-variables)中的任何 provider —— `openrouter`、`nous`、`openai-codex`、`copilot`、`copilot-acp`、`anthropic`、`gemini`、`google-gemini-cli`、`qwen-oauth`、`zai`、`kimi-coding`、`kimi-coding-cn`、`minimax`、`minimax-cn`、`minimax-oauth`、`deepseek`、`nvidia`、`xai`、`xai-oauth`、`ollama-cloud`、`alibaba`、`bedrock`、`huggingface`、`arcee`、`xiaomi`、`kilocode`、`opencode-zen`、`opencode-go`、`azure-foundry` —— 或您 `custom_providers` 列表中任何命名的自定义 provider（例如 `provider: "beans"`）。
    +辅助任务的可用 providers：`auto`、`main`，以及[provider 注册表](/reference/environment-variables)中的任何 provider —— `openrouter`、`nous`、`openai-codex`、`copilot`、`copilot-acp`、`anthropic`、`gemini`、`qwen-oauth`、`zai`、`kimi-coding`、`kimi-coding-cn`、`minimax`、`minimax-cn`、`minimax-oauth`、`deepseek`、`nvidia`、`xai`、`xai-oauth`、`ollama-cloud`、`alibaba`、`bedrock`、`huggingface`、`arcee`、`xiaomi`、`kilocode`、`opencode-zen`、`opencode-go`、`azure-foundry` —— 或您 `custom_providers` 列表中任何命名的自定义 provider（例如 `provider: "beans"`）。
     
     :::tip MiniMax OAuth
     `minimax-oauth` 通过浏览器 OAuth 登录（无需 API 密钥）。运行 `hermes model` 并选择 **MiniMax (OAuth)** 进行认证。辅助任务自动使用 `MiniMax-M2.7-highspeed`。参阅 [MiniMax OAuth 指南](../guides/minimax-oauth.md)。
    ```
  - `website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/features/fallback-providers.md`

    ```diff
    @@ -62,7 +62,6 @@ fallback_model:
     | GMI Cloud | `gmi` | `GMI_API_KEY`（可选：`GMI_BASE_URL`） |
     | StepFun | `stepfun` | `STEPFUN_API_KEY`（可选：`STEPFUN_BASE_URL`） |
     | Ollama Cloud | `ollama-cloud` | `OLLAMA_API_KEY` |
    -| Google Gemini（OAuth） | `google-gemini-cli` | `hermes model`（Google OAuth；可选：`HERMES_GEMINI_PROJECT_ID`） |
     | Google AI Studio | `gemini` | `GOOGLE_API_KEY`（别名：`GEMINI_API_KEY`） |
     | xAI（Grok） | `xai`（别名 `grok`） | `XAI_API_KEY`（可选：`XAI_BASE_URL`） |
     | xAI Grok OAuth（SuperGrok） | `xai-oauth`（别名 `grok-oauth`） | `hermes model` → xAI Grok OAuth（浏览器登录；需 SuperGrok 订阅） |
    ```
  - `website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/bundled/autonomous-ai-agents/autonomous-ai-agents-hermes-agent.md`

    ```diff
    @@ -332,7 +332,6 @@ hermes uninstall            Uninstall Hermes
     /commands [page]     Browse all commands (gateway)
     /usage               Token usage
     /insights [days]     Usage analytics
    -/gquota              Show Google Gemini Code Assist quota usage (CLI)
     /status              Session info (gateway)
     /profile             Active profile info
     /debug               Upload debug report (system info + logs) and get shareable links
    ```
- **#30416: fix(provider/gemini): clarify RESOURCE_EXHAUSTED 429 error — distinguish free-tier routing from per-minute limits**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`
- **#39077: fix(agent): distinguish transient Gemini rate limits from daily quota exhaustion**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `7130d6086`, `#50492`, `#38804`
- **#40755: fix(agent): add per-minute TPM/RPM cooldown tracker for Gemini 429 handling**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `7130d60861a9243301514bff611a9381830d59d8`, `#15895`

## Fix status

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