Skip to content

feat(gemini): add Google Gemini (OAuth) inference provider - #11250

Closed
teknium1 wants to merge 1 commit into
mainfrom
hermes/gemini-oauth-30b2099d
Closed

feat(gemini): add Google Gemini (OAuth) inference provider#11250
teknium1 wants to merge 1 commit into
mainfrom
hermes/gemini-oauth-30b2099d

Conversation

@teknium1

Copy link
Copy Markdown
Contributor

Summary

Adds google-gemini-cli as a first-class inference provider using Authorization Code + PKCE (S256) OAuth against Google, hitting the OpenAI-compatible Gemini endpoint (v1beta/openai) with a Bearer access token. Users sign in with their Google account — no API key to copy-paste, and the access token is auto-refreshed 5 minutes before expiry on every request.

Synthesized from three competing open PRs per the multi-PR design doc. Direction A from the synthesis (OAuth against paid v1beta/openai endpoint). Free-tier Code Assist endpoint (cloudcode-pa.googleapis.com/v1internal:generateContent) would be a larger follow-up PR introducing a new api_mode.

Credit to the competing PRs

From Taken Used how
#10176 (@sliverp) Clean module layout, PKCE flow shape, dual-path idea Foundation of agent/google_oauth.py
#10779 (@newarthur) Cross-process fcntl/msvcrt lock, thread-local re-entrancy counter, atomic write pattern _credentials_lock() and save_credentials()
#6745 (@Protect) Subprocess approach — explicitly rejected; different paradigm

Improvements over all three

  • Port fallback. If preferred port 8085 is taken, bind an ephemeral port instead of failing.
  • Refresh token rotation. Preserves the old refresh_token when Google omits one in the refresh response (correct per Google spec) and rotates when a new one is returned.
  • Paste fallback accepts both formats. Full redirect URL OR bare code.
  • doctor.py health check. Neither competing PR wired this.
  • No regression in _OAUTH_CAPABLE_PROVIDERS. feat(auth): add Google Gemini CLI OAuth PKCE provider integration  #10779 dropped anthropic and nous; we keep all existing entries and append google-gemini-cli.
  • No bundled unrelated features. feat(auth): add Google Gemini CLI OAuth PKCE provider integration  #10779 mixed in persona/personality routing, Chinese NL commands, and merge-conflict artifacts.

Architecture

  1. start_oauth_flow() generates PKCE (verifier, S256 challenge), spins up a 127.0.0.1 callback server (with ephemeral-port fallback), opens the browser.
  2. exchange_code() swaps the auth code + verifier for access + refresh tokens.
  3. save_credentials() writes ~/.hermes/auth/google_oauth.json atomically (tmp + fsync + replace) with 0o600 perms, holding a cross-process fcntl/msvcrt lock.
  4. On every inference request, get_valid_access_token() loads the file, refreshes if within 5 min of expiry, and returns a fresh Bearer.

api_mode: chat_completions (reuses existing path — no new api_mode, no run_agent.py changes).

Provider registration — 9/9 touchpoints covered

auth.py, models.py, providers.py, runtime_provider.py, config.py, main.py, auth_commands.py, doctor.py, plus the new agent/google_oauth.py module.

Client ID

Not shipped. Users register a Desktop OAuth client in Google Cloud Console themselves and set HERMES_GEMINI_CLIENT_ID in ~/.hermes/.env. Documented in website/docs/integrations/providers.md with a full walkthrough. This avoids shipping someone else's OAuth client ID and keeps quota scoped to the user's org.

If Nous maintainers want to register an official 'Hermes Agent' desktop client later, drop the ID into _DEFAULT_CLIENT_ID in agent/google_oauth.py — that's the only code change needed.

Test plan

44 new tests in tests/agent/test_google_oauth.py covering:

  • PKCE S256 verifier/challenge roundtrip + uniqueness
  • Credential I/O: save/load/clear, 0o600 permissions, atomic write (no tmp leftovers), corrupt JSON + missing file handling
  • Cross-process lock: acquire/release + thread-reentrant
  • Client ID resolution: env override, missing → google_oauth_client_id_missing
  • Token exchange: correct body, refresh success/failure, empty-refresh-token guard
  • get_valid_access_token: cached when fresh, refresh when near expiry, force_refresh, not-logged-in error, preserves refresh_token when Google omits, rotates when Google returns new
  • Callback server: preferred port when free, ephemeral fallback when busy
  • Paste fallback: URL + bare code + empty input
  • Runtime provider: resolves when logged in, AuthError when not, dispatch through resolve_runtime_provider, base URL env override
  • Provider registration: registry entry, 4 alias forms, models catalog, determine_api_modechat_completions, _OAUTH_CAPABLE_PROVIDERS regression guard, config OPTIONAL_ENV_VARS
  • Auth status: logged-in + not-logged-in dispatch
  • run_gemini_oauth_login_pure returns pool-compatible dict

All 44 pass. Plus 243 existing tests in related files still pass — no regressions.

Usage

# 1. Register a Desktop OAuth client at console.cloud.google.com/apis/credentials
#    Enable the Generative Language API
echo 'HERMES_GEMINI_CLIENT_ID=your-client.apps.googleusercontent.com' >> ~/.hermes/.env

# 2. Log in
hermes model            # pick 'Google Gemini (OAuth)'
# OR
hermes auth add --provider google-gemini-cli

# 3. Chat
hermes chat

Closure plan

Once merged, close with credit: #6745, #10176, #10779.

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).
@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Supply Chain Risk Detected

This PR contains patterns commonly associated with supply chain attacks. This does not mean the PR is malicious — but these patterns require careful human review before merging.

⚠️ WARNING: Outbound network calls (POST/PUT)

Outbound POST/PUT requests in new code could be data exfiltration. Verify the destination URLs are legitimate.

Matches (first 10):

399:+        with urllib.request.urlopen(request, timeout=timeout) as response:
475:+        with urllib.request.urlopen(request, timeout=timeout) as response:

Automated scan triggered by supply-chain-audit. If this is a false positive, a maintainer can approve after manual review.

@teknium1

Copy link
Copy Markdown
Contributor Author

Closing unmerged — pivoting to a full Code Assist integration instead.

Follow-up research showed this PR (OAuth against the paid v1beta/openai endpoint) misses what users actually want from 'Gemini CLI as an inference provider':

  1. Free-tier access — requires hitting cloudcode-pa.googleapis.com/v1internal:generateContent (the Code Assist endpoint), not v1beta/openai. The paid endpoint provides ~zero advantage over API keys.
  2. Zero-setup auth — can use Google's public desktop OAuth client baked into their open-source gemini-cli. No need to ask users to register their own GCP OAuth client.
  3. All-tiers support — single provider slug that transparently handles free tier (no project ID needed, Google auto-provisions) and paid tier (project ID required).

Reference implementation: jenslys/opencode-gemini-auth (MIT).

Elements from this PR that will carry forward into the new one:

  • agent/google_oauth.py PKCE module structure
  • Cross-process fcntl / msvcrt credential lock
  • 9-point provider registration wiring pattern
  • 44-test suite (will be expanded)

New elements coming:

  • Endpoint pivot to cloudcode-pa with Code Assist API
  • Public OAuth client creds baked in (same as Google's official gemini-cli)
  • loadCodeAssist / onboardUser project discovery + provisioning
  • Native Gemini ↔ OpenAI request/response translator
  • Project ID resolution (env → config → auto-discover → free-tier fallback)
  • VPC-SC / corporate-account graceful degradation
  • 429 retry with Retry-After + MODEL_CAPACITY_EXHAUSTED cooldown
  • /gquota command showing Code Assist quota buckets
  • Thinking config pass-through (gemini-3 thinkingLevel, gemini-2.5 thinkingBudget)
  • Upfront warning re: Google's ToS stance on third-party OAuth

New PR incoming. Thanks again to @sliverp (#10176), @newarthur (#10779), @Protect (#6745) for the groundwork this builds on.

@teknium1 teknium1 closed this Apr 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant