Skip to content
dsh.fish
Bundle

dsh-gemini-oauth-bridge

Bridge Google AI subscription (Antigravity/Gemini OAuth, Code Assist API) into DeepSeek Harness as an OpenAI-compatible model endpoint

Source
hyqhyq3
License
MIT
Updated
Updated 21 hours ago

Readme

# dsh-gemini-oauth-bridge

English | [简体中文](README.zh-CN.md)

Bridges a **Google AI subscription** (AI Pro / AI Ultra, via the Antigravity / Code Assist OAuth surface) into an **OpenAI-compatible endpoint** that DeepSeek Harness (DSH) can use directly as a model provider.

> **Important boundary**: Google AI subscription developer quotas only cover the AI Studio web UI and OAuth login inside official CLI tools (Gemini CLI / Antigravity); **API-key usage is billed separately**. This plugin speaks the same OAuth surface as the official client, so it consumes subscription quota rather than API credit — but that also means it is not a Google-sanctioned third-party integration. Account risk / breakage is possible; evaluate for yourself.

## Features

- **Login and go**: one-click Google OAuth from Settings → Gemini OAuth (authorization code + loopback callback), with automatic `loadCodeAssist` registration / `onboardUser` onboarding and upstream model discovery.
- **OpenAI-compatible endpoint**: `/gemini-oauth-bridge/v1/chat/completions` (streaming SSE + non-streaming) and `/v1/models` (so DSH can discover models).
- **Full protocol translation**: system messages, multimodal (data-URL images), function calling (tool_calls ↔ functionCall/functionResponse), `reasoning_content` (thinking stream), usage stats, finish-reason mapping.
- **Gemini 3 thought-signature replay**: the server remembers upstream `thoughtSignature` values by tool-call id and re-attaches them to functionCall parts on the next turn — OpenAI format carries no signatures, and Gemini 3 multi-turn tool calls fail without replay.
- **Subscription-friendly (anti-drift)**: dynamically tracks the latest shipping Antigravity client version for the User-Agent (stale versions are rejected for newer models); requests carry `requestType: "agent"`, a stable derived `sessionId`, and `agent-<uuid>` request IDs; keep-alive connections without `Connection: close`.
- **429 cooldowns**: parses upstream `Retry-After` / `retryDelay`, enters a per-model cooldown window, and refuses requests locally during it instead of hammering upstream.
- **Single-flight token refresh**: access tokens auto-renew 120 s before expiry; concurrent requests share one refresh.

## Install

```sh
dsh plugin --profile web add github:hyqhyq3/dsh-gemini-oauth-bridge
# or a local checkout
dsh plugin --profile web add link:/path/to/dsh-gemini-oauth-bridge
```

Restart `dsh --profile web`, then open Settings → **Gemini OAuth**.

## Usage

1. Click **登录 Google 账号** (Log in with Google) and finish the flow in your browser. The plugin temporarily listens on `127.0.0.1:51121` for the loopback callback — this exact redirect URI is registered for the Antigravity client and the port cannot change. Port conflicts are reported explicitly.
2. After login the page shows the account, project ID, tier, and model list.
3. Register the bridge as a DSH provider in `~/.dsh/settings.yaml` (port matches your DSH web URL, default 3080):

```yaml
llm-pi-ai:
  providers:
    gemini-oauth:
      api: openai-completions
      baseURL: http://127.0.0.1:3080/gemini-oauth-bridge/v1
      apiKeyEnv: GEMINI_OAUTH_API_KEY
      models:
        - id: gemini-3.1-pro-high
        - id: gemini-3.7-flash-high
```

4. Restart DSH and pick the models in the model selector. **Step 3 is usually unnecessary**: logging in and refreshing the model list write this block automatically (the `baseURL` port follows the address you use to open the DSH UI). DSH refuses a provider profile that declares no credential, so the plugin also provisions the matching `GEMINI_OAUTH_API_KEY` entry in `~/.dsh/.credentials.yaml` (mode 0600) with the fixed placeholder `dsh-gemini-oauth-bridge` — an existing entry is never overwritten, and the bridge accepts any bearer token. To use your own key instead, set it through the Models page; to add local auth to the bridge itself, set `"apiKey": "some-string"` in `~/.dsh/gemini-oauth-bridge.json` — requests must then carry `Authorization: Bearer <value>`.

### Local state

`~/.dsh/gemini-oauth-bridge.json` (mode 0600, contains OAuth tokens — treat as a secret):

| Field | Meaning |
|---|---|
| `tokens` | access/refresh tokens and expiry |
| `email` / `project` / `tier` | login account and registration result |
| `models` | upstream model cache (refreshable from the UI) |
| `signatures` | thoughtSignature replay cache (by tool-call id, cap 800) |
| `apiKey` | optional local auth for the bridge endpoint |
| `apiConfig` | optional upstream overrides (`generateBase` / `loadBase` / `onboardBase`); defaults match CLIProxyAPI: generation via daily, registration via prod |

## Design notes (anti-drift)

Protocol details follow the public implementation in [CLIProxyAPI](https://github.com/router-for-me/CLIProxyAPI) (Apache-2.0):

- **Live version tracking**: every 6 hours the latest Antigravity version is fetched from the official updater hub (UA `electron-builder`, like the real updater), semver-validated, and cached; failures fall back to the cache, then to the `2.9.1` floor (Cloud Code rejects clients below 2.9.0 for newer models).
- **Per-surface User-Agents**: generate/stream/loadCodeAssist use `antigravity/hub/<version> darwin/arm64`; onboardUser appends ` google-api-nodejs-client/10.3.0`; OAuth token refresh uses `Go-http-client/2.0` — each surface matches the real client.
- **Envelope semantics**: `requestType: "agent"`, `requestId: "agent-<uuid>"`, `userAgent: "antigravity"`, `request.sessionId` derived deterministically from the conversation's first message (one session per conversation); `safetySettings` stripped; `generationConfig.maxOutputTokens` removed for gemini-3 models.
- **Connection fingerprint**: no `Connection: close`; keep-alive reuse.
- **Backoff**: on 429, reads the `Retry-After` header or in-body `retryDelay`, enters a per-model cooldown (capped at 30 min) with no upstream traffic during it.
- **Subscription tier resolution**: request entitlements follow `paidTier.id` (Google AI Pro → `g1-pro-tier`); a `free-tier` `currentTier` is just the registration state and does not limit quota or model access.
- **Automatic provider configuration**: on login and on 刷新模型列表 (refresh models), the `llm-pi-ai.providers.gemini-oauth` block (with the latest model ids and the `GEMINI_OAUTH_API_KEY` credential reference) is surgically merged into `~/.dsh/settings.yaml` — every other byte of your config is preserved, and a one-time backup is kept at `settings.yaml.bak-gemini-oauth-bridge`. The matching placeholder key is provisioned in `~/.dsh/.credentials.yaml` (append-only; a version-1 document whose shape cannot be proven is left untouched and reported instead). Internal `chat_*`/`tab_*` ids are filtered out. Takes effect after a DSH restart; set `"providerSync": false` in the state file to opt out.
- **Public client credentials**: the OAuth client credentials are public values embedded in the Antigravity app (published verbatim in CLIProxyAPI's repo); they are stored base64-encoded in this repository only to keep GitHub push protection quiet, and decoded at runtime.

## Limitations

- **Unofficial usage**: relies on the Antigravity client's public OAuth credentials and a non-public `v1internal` surface; Google may change or restrict it at any time. Intended for individual subscribers' own use.
- Single account (no rotation); `http(s)://` image URLs unsupported (data URLs only); the signature cache is persisted but LRU-evicts past 800 entries.
- The login callback occupies `127.0.0.1:51121`; logging in while the Antigravity desktop app is also logging in may conflict.
- Generation goes to the `daily-cloudcode-pa` channel (matching CLIProxyAPI's default); override via `apiConfig` if needed.

## Development

Zero dependencies, plain ESM JavaScript (Node ≥ 22); `lib/*.js` ships as written, no build step.

```sh
node --check lib/index.js && node --check lib/protocol.js && node --check lib/client.js
node --test test/protocol.test.js test/host.smoke.test.js test/bridge.e2e.test.js
```

Test coverage: protocol translation units (OAuth URLs / envelope / stream fragments / signature replay), host-route smoke (mocked cordis ctx + real HTTP + loopback login error paths), and bridge end-to-end (mocked upstream verifying the envelope contract, SSE translation, and 429 cooldown).

## License

MIT

## Language (0.3.0)

The Settings section is bilingual (English / Simplified Chinese), using DSH's
`geminiOauth` locale namespace and framework-injected `t` seat. Change Language in
Settings → General; every plugin API request sends the active `?lang=zh|en`.
The root summary page and OpenAI-compatible bridge also accept this parameter.
Bridge errors with locale descriptors use the resolved language; raw errors,
status codes, and response envelopes are preserved. A valid explicit language
wins over `locale.preference` in `$DSH_HOME/settings.yaml` (default home:
`~/.dsh`); absent or unreadable preferences fall back to Chinese. The OAuth
callback remembers the language of the login request, since Google's navigation
does not carry `lang`. Google/upstream text is preserved verbatim. This requires
DSH's `@deepseek-ai/dsh-client-locale` service, included in the web profile.

Run `npm test` for protocol, HTTP integration, and locale coverage. No build step
or runtime dependencies are needed.

Install

dsh plugin --profile web add github:hyqhyq3/dsh-gemini-oauth-bridge

Profile: web

  • This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.
Source