feat(gemini): add Google Gemini (OAuth) inference provider

Adds 'google-gemini-cli' as a first-class inference provider using
Authorization Code + PKCE (S256) OAuth against Google's accounts.google.com,
hitting the OpenAI-compatible Gemini endpoint (v1beta/openai) with a Bearer
access token. Users sign in with their Google account — no API-key copy-paste.

Synthesized from three competing PRs per multi-PR design analysis:
- Clean PKCE module structure shaped after #10176 (thanks @sliverp)
- Cross-process file lock (fcntl POSIX / msvcrt Windows) with thread-local
  re-entrancy counter from #10779 (thanks @newarthur)
- Rejects #6745's subprocess approach entirely (different paradigm)

Improvements over the competing PRs:
- Port fallback: if 8085 is taken, bind ephemeral port instead of failing
- Preserves refresh_token when Google omits one (correct per Google spec)
- Accepts both full redirect URL and bare code in paste fallback
- doctor.py health check (neither PR had this)
- No regression in _OAUTH_CAPABLE_PROVIDERS (#10779 dropped anthropic/nous)
- No bundled unrelated features (#10779 mixed in persona/personality routing)

Storage:
- ~/.hermes/auth/google_oauth.json (0o600, atomic write via fsync+replace)
- Cross-process fcntl/msvcrt lock with 30s timeout
- Refresh 5 min before expiry on every request via get_valid_access_token

Provider registration (9-point checklist):
- auth.py: PROVIDER_REGISTRY entry, aliases (gemini-cli, gemini-oauth),
  resolve_gemini_oauth_runtime_credentials, get_gemini_oauth_auth_status,
  get_auth_status() dispatch
- models.py: _PROVIDER_MODELS catalog, CANONICAL_PROVIDERS entry, aliases
- providers.py: HermesOverlay, ALIASES entries
- runtime_provider.py: resolve_runtime_provider() dispatch branch
- config.py: OPTIONAL_ENV_VARS for HERMES_GEMINI_CLIENT_ID/_SECRET/_BASE_URL
- main.py: _model_flow_google_gemini_cli, select_provider_and_model dispatch
- auth_commands.py: add-to-pool handler, _OAUTH_CAPABLE_PROVIDERS
- doctor.py: 'Google Gemini OAuth' status line

Client ID: Not shipped. Users register a Desktop OAuth client in Google Cloud
Console (Generative Language API) and set HERMES_GEMINI_CLIENT_ID in
~/.hermes/.env. Documented in website/docs/integrations/providers.md.

Tests: 44 new unit tests covering PKCE S256 roundtrip, credential I/O
(permissions + atomic write), cross-process lock, port fallback, paste
fallback (URL + bare code), token exchange/refresh, rotation handling,
get_valid_access_token refresh semantics, runtime provider dispatch,
alias resolution, and regression guards for _OAUTH_CAPABLE_PROVIDERS.

Docs: new 'Google Gemini via OAuth' section in providers.md with full
walkthrough including GCP Desktop OAuth client registration, and env var
table updated in environment-variables.md.

Closes partial work in #6745, #10176, #10779 (to be closed with credit
once this merges).
This commit is contained in:
Teknium 2026-04-16 15:08:49 -07:00
parent 387aa9afc9
commit 1e5ee33f68
No known key found for this signature in database
12 changed files with 1693 additions and 4 deletions

View file

@ -35,12 +35,68 @@ 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`, browser PKCE login, requires `HERMES_GEMINI_CLIENT_ID`) |
| **Custom Endpoint** | `hermes model` → choose "Custom endpoint" (saved in `config.yaml`) |
:::tip Model key alias
In the `model:` config section, you can use either `default:` or `model:` as the key name for your model ID. Both `model: { default: my-model }` and `model: { model: my-model }` work identically.
:::
### Google Gemini via OAuth (`google-gemini-cli`)
The `google-gemini-cli` provider lets you authenticate with your Google account
via a browser-based Authorization Code + PKCE flow — no API key copy-paste, and
credentials are refreshed automatically before every request.
**Quick start:**
```bash
# 1. Set your OAuth client ID (see "Registering a Desktop OAuth client" below)
export HERMES_GEMINI_CLIENT_ID="your-client-id.apps.googleusercontent.com"
# 2. Run the login flow
hermes model
# → pick "Google Gemini (OAuth)"
# → a browser opens to accounts.google.com, sign in, Hermes captures the callback
# 3. Chat as normal
hermes chat
```
**Storage:** tokens are persisted to `~/.hermes/auth/google_oauth.json` with
0600 permissions, atomic writes, and a cross-process fcntl lock so multiple
Hermes instances can safely share a session.
**Endpoint:** requests are routed to Google's OpenAI-compatible Gemini endpoint
(`https://generativelanguage.googleapis.com/v1beta/openai`) with a Bearer
access token. Supports the full Gemini 2.5 / 3.x lineup and Gemma open models.
#### Registering a Desktop OAuth client
Hermes does not ship with a default OAuth client ID — you register one yourself
in Google Cloud Console so quota and consent screens are scoped to your
organization:
1. Go to <https://console.cloud.google.com/apis/credentials>.
2. Create (or pick) a project and click **"Create Credentials" → "OAuth client ID"**.
3. Choose **Application type: Desktop app**, name it "Hermes Agent".
4. Enable the **Generative Language API** for the project under APIs & Services.
5. Download the JSON and set `HERMES_GEMINI_CLIENT_ID` in `~/.hermes/.env`
(client secret is optional for Desktop clients but can be set via
`HERMES_GEMINI_CLIENT_SECRET` if required by your org policy).
#### Troubleshooting
- **Port 8085 already in use** — Hermes will automatically fall back to an
ephemeral port. Add that exact URL to your OAuth client's authorized redirect
URIs if Google refuses it.
- **"State mismatch — aborting for safety"** — someone hit the callback URL
with a stale/forged request. Re-run the login.
- **Refresh failures persist** — re-run login (`hermes auth add --provider
google-gemini-cli`); stale refresh tokens can happen after password changes
or scope revocation.
:::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.
:::