dsh-gemini-oauth-bridge
English | 简体中文
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
loadCodeAssistregistration /onboardUseronboarding 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
thoughtSignaturevalues 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 derivedsessionId, andagent-request IDs; keep-alive connections withoutConnection: close. - 429 cooldowns: parses upstream
Retry-After/retryDelay, enters a per-model cooldown window, and refuses requests locally during it instead of hammering upstream. - Catalog honesty: an entry the upstream catalog flags
disabled: true— e.g.gemini-4-argon-disabled("Unavailable" / "available to Ultra and paid Pro plan subscribers") — is never published as a DSH model, so the picker cannot offer something that can only fail. A request for an id the account cannot use returns the upstream's own explanation plus a refresh hint instead of an opaque404 Requested entity was not found. Per-model context/output limits are read from the upstreammaxTokens/maxOutputTokensfields. - Browser-trust fence: a plugin prefix route is dispatched before DSH's own Host/Origin fence, so the bridge enforces its own — the
Hostmust be an authority it serves (loopback by default), aSec-Fetch-Site: cross-siterequest, an opaqueOrigin: null, or anOriginthat disagrees with theHostis refused with 403, and a request carrying noHostat all is only accepted from a loopback peer. - Single-flight token refresh: access tokens auto-renew 120 s before expiry; concurrent requests share one refresh.
Install
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
- Click 登录 Google 账号 (Log in with Google) and finish the flow in your browser. The plugin temporarily listens on
127.0.0.1:51121for the loopback callback — this exact redirect URI is registered for the Antigravity client and the port cannot change. Port conflicts are reported explicitly. - After login the page shows the account, project ID, tier, and model list.
- Register the bridge as a DSH provider in
~/.dsh/settings.yaml(port matches your DSH web URL, default 3080):
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
- Pick the models in the model selector. Step 3 is usually unnecessary: logging in and refreshing the model list write this block automatically (the
baseURLport follows the address you use to open the DSH UI). On DSH ≥ 0.2 the plugin edits thellm-pi-aientry through the settings service — the same path the DSH Models page uses — so the refreshed model list is hot-applied to the running process and persisted into the active profile'scordis.patch.yml; no restart is needed. Older DSH versions get the legacy~/.dsh/settings.yamlwrite, which takes effect at the next restart. DSH refuses a provider profile that declares no credential, so the plugin also provisions the matchingGEMINI_OAUTH_API_KEYentry in~/.dsh/.credentials.yaml(mode 0600) with the fixed placeholderdsh-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 carryAuthorization: Bearer.
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) |
modelCaps |
per-model contextLength / maxCompletionTokens read from the upstream catalog |
modelsUnavailable |
ids the upstream catalog marks disabled, with the upstream explanation shown when such a model is requested |
trustedHosts |
extra host:port authorities the browser-trust fence accepts (for non-loopback deployments); loopback is always accepted |
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 (Apache-2.0):
- Live version tracking: the latest Antigravity version is fetched from the official updater hub (UA
electron-builder, like the real updater), semver-validated, and cached for 6 hours — refreshed on the first request that finds the cache stale, not on a timer; failures fall back to the cache, then to the2.9.1floor (Cloud Code rejects clients below 2.9.0 for newer models). - Per-surface User-Agents: generate/stream/loadCodeAssist use
antigravity/hub/ darwin/arm64; onboardUser appendsgoogle-api-nodejs-client/10.3.0; OAuth token refresh usesGo-http-client/2.0— each surface matches the real client. - Envelope semantics:
requestType: "agent",requestId: "agent-",userAgent: "antigravity",request.sessionIdderived deterministically from the conversation's first message (one session per conversation);safetySettingsstripped;generationConfig.maxOutputTokensremoved for gemini-3 models. - Connection fingerprint: no
Connection: close; keep-alive reuse. - Backoff: on 429, reads the
Retry-Afterheader or in-bodyretryDelay, 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); afree-tiercurrentTieris 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-oauthroute (with the latest model ids and theGEMINI_OAUTH_API_KEYcredential reference) is written through the DSH settings service when available — the same path the Models page uses — so the change hot-applies to the running process and persists into the active profile'scordis.patch.yml; only the gemini-oauth route is touched and the edit is validated by the adapter's own schema. When the settings service is unavailable or the update fails, a fallback writes the legacy~/.dsh/settings.yamldocument instead (surgical merge, one-time backup atsettings.yaml.bak-gemini-oauth-bridge), which older deployments load at boot; once the hot path has succeeded that document is deliberately left alone, because on DSH ≥ 0.2 the next boot imports it and renames it oversettings.yaml.imported, so rewriting it would clobber that archive. 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). Internalchat_*/tab_*ids are filtered out. Set"providerSync": falsein 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.
Security
- OAuth tokens live in
~/.dsh/gemini-oauth-bridge.json(mode 0600) and are never returned by any endpoint;/api/statusexposes only account metadata (email, project, tier) and the model list. - DSH applies a Host/Origin fence to its own
/api/*routes, but a pluginwebServer.register({kind: 'prefix'})route is dispatched before that fence, so the bridge enforces the same rules itself: a request is refused with 403 when itsHostis not an authority the bridge serves, whenSec-Fetch-Site: cross-site, whenOriginis the opaquenulla sandboxed frame sends, whenOrigindisagrees withHost, or when a request arrives with noHostat all from anything other than a loopback peer. This blocks DNS-rebinding reads of/api/statusand/v1/*, quota-burning calls through/v1/chat/completions, and blind-CSRF of the mutating POSTs (/api/login,/api/logout,/api/refresh-models) — a cross-siteContent-Type: text/plainPOST needs no CORS preflight, so without the fence the browser would deliver it. /v1/*is unauthenticated by default because DSH calls it locally; set"apiKey"in the state file to requireAuthorization: Bearer, and"trustedHosts": ["host:port"]to serve DSH on a non-loopback address.
Limitations
- Unofficial usage: relies on the Antigravity client's public OAuth credentials and a non-public
v1internalsurface; 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-pachannel (matching CLIProxyAPI's default); override viaapiConfigif needed. - Some catalog models cannot generate. Upstream advertises entries it will not serve for your account — the Gemini 4 Argon family appears only as
gemini-4-argon-disabledwithdisabled: true,tagTitle: "Unavailable"andtagDescription: "Gemini 4 Argon is currently available to Ultra and paid Pro plan subscribers.", and every id variant returns404 NOT_FOUNDeven on a paidg1-pro-tier(Google AI Pro) account. Catalog membership is therefore not proof that a model can generate; the plugin hidesdisabledentries, but no client-side change can enable them.
Development
Zero dependencies, plain ESM JavaScript (Node ≥ 22); lib/*.js ships as written, no build step.
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.