Profile
Back to NewsBack
GitHub Trending 34 min
Reader Mode
azrinaldi/gemini-stealth-bridge: Browserless Chrome uTLS + HTTP/2 OpenAI-compatible API daemon for Google Gemini Web (/v1/chat/completions & /v1/responses)

azrinaldi/gemini-stealth-bridge: Browserless Chrome uTLS + HTTP/2 OpenAI-compatible API daemon for Google Gemini Web (/v1/chat/completions & /v1/responses)

gemini-stealth-bridge (gsb)

release</a> release workflow</a> go</a> license</a> ghcr</a>

OpenAI-compatible proxy for Gemini Web. Point coding agents, routers, and OpenAI SDK clients at gsb and use your Gemini account through Chat Completions or Responses — with account pooling, quota-aware failover, native image/video generation, Google Search grounding, and a built-in MCP server.

!gsb Web Control Plane

gsb translates OpenAI-style messages, images, and tool definitions into Gemini Web requests, then converts the results back into JSON or Server-Sent Events (SSE). It authenticates with your Google session cookies, not a Gemini Developer API key. Model access and quotas depend on the connected account.

The daemon runs without headless Chrome or CDP. Browserless transport is an implementation detail; the API bridge is the interface your clients use.

Unofficial. This is a bridge to the Gemini web app, not Google's supported Gemini Developer API or an OpenAI service. Changes to Google's web protocol, sign-in requirements, account restrictions, and quotas can interrupt operation. The transport does not guarantee protection from detection or account restrictions. Use only accounts you are authorized to access and review Google's applicable terms.

Contents

  1. What gsb provides
  2. Requirements
  3. Quick start
  4. Core concepts
  5. Web Control Plane
  6. Account onboarding
  7. Client integration
  8. API reference
  9. Native image and video generation
  10. Web search grounding
  11. Configuration reference
  12. Data layout
  13. Security
  14. Docker and deployment
  15. Operations and troubleshooting
  16. How it works
  17. Development
  18. License

1. What gsb provides

Capabilities

| Capability | Behavior | |---|---| | Chat Completions | POST /v1/chat/completions, JSON or SSE output | | Responses | POST /v1/responses, JSON or SSE output, including function_call, custom_tool_call, local_shell, and namespaced tools | | Function calling | Translates tool definitions into a prompt contract, validates returned arguments against the tool's JSON Schema, repairs common model mistakes, and emits OpenAI-style calls. The client executes the tools | | Image input | Base64 data URLs, remote HTTPS images, and Anthropic-style image parts | | Thinking effort | Maps low, medium, and high to Gemini Web thinking levels; accepts many client-specific field names | | Conversation history | Reuses Gemini continuation tokens when matching history is available; offloads older turns into text attachments when the inline prompt budget is exceeded | | Multiple accounts and quota | Pool key routes across accounts (rotate, lru, fill-first); per-account keys pin one account; group keys serve one user's accounts with failover inside the group. Exposes rolling 5-hour and 7-day weekly quota windows | | Per-account proxies | Each account can egress through its own HTTP/SOCKS5 proxy, configured per account file, the Web UI, or BRIDGE_EGRESS_PER_ACCOUNT | | Web search grounding | Extracts the Google Search citations Gemini Web returned and appends them to the answer as a numbered Sources: list | | MCP server | Built-in Streamable HTTP MCP endpoint at POST /mcp (alias /v1/mcp) exposing grounding as a web_search tool for Claude Code, Cursor, OMP, and other MCP clients | | Native image and video | Imagen / Nano Banana images and Veo MP4 video with audio through Chat, Coding Agent tool-calling (generate_media), POST /v1/images/generations, POST /v1/videos/generations, and the local GET /v1/media/ proxy | | Web Control Plane | Embedded single-binary dashboard at GET /ui for account onboarding, quota bars, priority ordering, pause/quarantine actions, and runtime configuration | | Session maintenance | Background cookie rotation, model/quota refresh, idle-session reaping, and persistence of updated credentials | | Diagnostics | X-GSB-* response headers, request timing logs, redacted request dumps, and fingerprint comparison scripts |

Compatibility boundaries

Compatibility covers the endpoints and fields documented here, not the entire OpenAI API:

  • Sampling and output controls such as temperature, top_p, max_tokens, and max_output_tokens are not forwarded as generation controls. They may appear in model metadata for client compatibility, but they do not enforce output limits or change sampling.
  • Responses clients must resend conversation history in input; previous_response_id is not a continuation mechanism. Response retrieval, deletion, and background jobs are not implemented.
  • Function calling is prompt-based, not native Gemini Developer API function calling. Malformed or schema-invalid calls can fail with tool_call_error.
  • Token usage is estimated locally, not reported by Google's billing/tokenizer API. The model registry advertises a 1,048,576-token context window and 65,536-token output limit as metadata, not a guarantee for every account or request.
  • Audio input/output, embeddings, and the general OpenAI Files API are not exposed. Internal text-attachment offloading is not a general file-upload API.
  • The daemon serves plain HTTP. It does not terminate TLS.

What's new in v0.4.1

  • Frontier model tier tracked through the rename: upstream renamed the top tier from gemini-argon to gemini-carbon on 2026-11-10. Thinking level and model selection now come from the live registry instead of name tables, so a renamed or newly introduced tier is described correctly without a code change. The name tables remain only as a fallback for a registry that publishes no thinking policy. Pin clients from GET /v1/models; a name upstream has dropped returns 404 model_not_found.
  • Documented limits actually apply: the limit table was keyed from displayName, but the live registry carries the versioned name in description and a UI label in displayName, so every model silently reported limitsSource: config. Each candidate is now tried in turn.
  • Frontier quota attribution: the quota bucket behind the top tier is still named "Gemini Pro", so both the current and previous tier names map to it when reporting an exhausted bucket.

What's new in v0.4.0

  • Web search grounding: answers include a Sources: list; BRIDGE_INCLUDE_GROUNDING=0 suppresses it.
  • Built-in MCP server: POST /mcp exposes web_search with a per-call timeout_seconds deadline.
  • Per-account egress proxies: BRIDGE_EGRESS_PER_ACCOUNT or the Web UI's Outbound Proxy field.
  • Group keys: BRIDGE_GROUP_TOKENS + BRIDGE_ACCOUNT_GROUP for multi-user deployments.
  • Faster tool streaming: the tool-envelope detector is a hand-rolled scan instead of a regexp over the whole accumulated answer.
Older release notes are on the GitHub releases page.

2. Requirements

| Component | Requirement | |---|---| | Runtime | Linux, macOS, or Windows on amd64 or arm64. Static binaries need no runtime dependencies | | Build | Go 1.26.0 or newer, Git | | Account | A Google account that can use Gemini Web | | Browser (optional) | Chrome, Chromium, Brave, Edge, Vivaldi, or Arc for --login / --import-chrome; Firefox cookie databases are also readable | | Docker (optional) | Any recent Docker Engine with Compose v2 | | Scripts (optional) | python3 for scripts/check-router-metadata.sh and scripts/diff-fingerprint.sh; POSIX sh |

Shell examples use a POSIX shell. On Windows, run the binary as gsb.exe and adapt environment-variable assignments to your shell.


3. Quick start

3.1 Install

Pick whichever path fits: a prebuilt binary, a Go install, or the container image.

Prebuilt binary (recommended) — every release publishes static binaries for Linux, macOS, and Windows on both amd64 and arm64, plus SHA256SUMS:

# Linux x86_64; swap in darwin_arm64, linux_arm64, windows_amd64.exe, ...
curl -fL -o gsb https://github.com/azrinaldi/gemini-stealth-bridge/releases/latest/download/gsb_linux_amd64
chmod +x gsb
./gsb --version

Verify the download against the release checksums:

curl -fLO https://github.com/azrinaldi/gemini-stealth-bridge/releases/latest/download/SHA256SUMS
sha256sum -c SHA256SUMS --ignore-missing

From source:

git clone https://github.com/azrinaldi/gemini-stealth-bridge.git
cd gemini-stealth-bridge
go build -o gsb ./cmd/gsb

Or install into your Go binary directory:

go install github.com/azrinaldi/gemini-stealth-bridge/cmd/gsb@latest

Container — Docker is optional. If you only want the binary, you can use the image purely as an installer and never leave a container running:

cid=$(docker create ghcr.io/azrinaldi/gemini-stealth-bridge:latest)
docker cp "$cid:/usr/local/bin/gsb" ./gsb
docker rm "$cid"

See Docker and deployment for the full container setup.

3.2 Connect a Gemini account

Sign in through gsb so the session is captured in an isolated disposable browser profile that never conflicts with background cookie rotation:

./gsb --login acc1
./gsb --probe

--login opens a temporary Chrome/Chromium/Brave/Edge window with a disposable --user-data-dir, waits for Google sign-in, captures the cookies through CDP, verifies the session against Gemini Web, and deletes the temporary profile. No background browser process is left competing with gsb for __Secure-1PSIDTS rotation.

--probe prints the account's models and 5-hour/weekly quota. Treat a probe without an auth error as the setup success signal, not the mere existence of accounts/.json.

3.3 Start the daemon

./start.sh

or directly:

./gsb --web

The default address is http://127.0.0.1:20133 and the Web Control Plane is at http://127.0.0.1:20133/ui. Keep this default local-only setup on loopback; see Security before sharing access.

start.sh builds a missing or outdated binary when a Go toolchain is available, falls back to the existing binary when it is not, exports BRIDGE_HOME (default ~/.gemini-stealth-bridge), enables the Web UI, and then execs gsb.

3.4 Send a request

There are three kinds of key. Pick one:

| Key | Value | Behavior | |---|---|---| | Pool key | BRIDGE_API_KEY (default public) | Routes each request across all loaded accounts using BRIDGE_ACCOUNT_STRATEGY. Use this normally | | Group key | BRIDGE_GROUP_TOKENS entry (token:group) | Serves only the accounts of its group and fails over inside that group. Use this in multi-user deployments | | Account key | gsb- or | Pins the request to that account and never fails over. Use this to test or isolate one account |

List the models available to the pool:

curl -sS http://127.0.0.1:20133/v1/models \
  -H 'Authorization: Bearer public'

Set MODEL to an id returned by that endpoint. gemini-flash below is an example, not a promise of availability:

export MODEL=gemini-flash
curl -sS http://127.0.0.1:20133/v1/chat/completions \
  -H 'Authorization: Bearer public' \
  -H 'Content-Type: application/json' \
  -d "{\"model\":\"$MODEL\",\"messages\":[{\"role\":\"user\",\"content\":\"Reply with OK.\"}]}"

Read the answer from choices[0].message.content. /health checks that the daemon is running; only a successful model request verifies upstream generation.

X-GSB-Account in the response headers names the account that served the request, which is the quickest way to see the pool working. Replace public with gsb-acc1 to pin the same request to one account.


4. Core concepts

4.1 Accounts

An account is one saved Google session under $BRIDGE_HOME/accounts/.json. Account names accept 1–64 characters from A-Z, a-z, 0-9, ., _, and -.

Each account has its own:

  • cookie jar and rotating session cookies,
  • live model list and quota state,
  • transport session and optional egress proxy,
  • failure/quarantine/disabled state,
  • account group and priority position.
Accounts are deduplicated by email address. Multiple files for the same Google account do not create additional upstream quota; the freshest credential wins.

4.2 API keys

| | Pool key | Group key | Account key | |---|---|---|---| | Value | BRIDGE_API_KEY (default public) | A BRIDGE_GROUP_TOKENS entry (token:group) | gsb- and | | Scope | All loaded accounts | The accounts assigned to one group via BRIDGE_ACCOUNT_GROUP | Exactly one account | | Account selection | Automatic, per BRIDGE_ACCOUNT_STRATEGY | Automatic, inside the group only | None; the named account is always used | | Failover on quota, ban, or failure | Yes | Yes, but only among the group's accounts | No; the error is returned to the caller | | /v1/accounts and /v1/quota | All accounts | Only the group's accounts | Only the named account | | Best for | Normal use, agents, routers | Multi-user deployments where each user has their own accounts | Testing one account, per-project quota isolation |

Notes:

  • BRIDGE_API_KEY=public means the pool key is public and requests without a key are accepted. The Web UI also derives a stable gsb-pool- token from BRIDGE_HOME and accepts it as the pool key.
  • When exactly one account is loaded, the pool key resolves to that account's scope rather than the admin/all-accounts scope.
  • Account keys are registered automatically for every loaded account file, including the raw account name. They are predictable from the filename, so treat them as routing aliases, not secrets. Setting a pool key does not remove them.
  • BRIDGE_ACCOUNT_TOKENS adds explicit token:account_id mappings. It does not remove the automatic aliases. When it is set, unknown tokens no longer trigger a hot reload of account files.
  • BRIDGE_GROUP_TOKENS adds token:group_name mappings. A group key cannot reach accounts outside its group.
  • BRIDGE_ADMIN_KEY grants the admin scope: all accounts, plus /v1/admin/*. A non-public BRIDGE_API_KEY also satisfies admin authentication when BRIDGE_ADMIN_KEY is empty.
  • Accepted credential headers: Authorization: Bearer (recommended), x-api-key, x-goog-api-key, and ?key=. Query keys can leak into URL logs.

4.3 Pool routing strategies

BRIDGE_ACCOUNT_STRATEGY controls pool-key selection:

  • rotate (default) — keep using one account until it is exhausted, banned, rate-limited, or failing, then advance to the next and wrap around. Best session continuity, and it leaves other accounts at full quota as standby. It does not jump back to an earlier account the moment that account recovers.
  • lru — spread load to the least recently used healthy account. Ties prefer fewer recorded failures, then the oldest LastUsed.
  • fill-first — drain a fixed order set by BRIDGE_ACCOUNT_PRIORITY, then load order. A recent failure demotes an account only for the 60-second cooldown, so a preferred account reclaims its rank as soon as it is healthy.
All strategies share the same eligibility rules. An account is skipped when it is:
  • disabled (Paused) or quarantined,
  • missing the requested model,
  • known to have the requested model's quota exhausted (per BRIDGE_QUOTA_PREFLIGHT),
  • in a synthetic model-substitution cooldown,
  • at its per-account RPM or concurrency limit,
  • in the 60-second failure cooldown.
An account that already holds the conversation's continuation cache is preferred, so a multi-turn conversation stays on one Gemini session even when the pool rotates.

Failover respects account groups. Once a request is being served by an account in a group, retries stay inside that group. A pool-key request that starts on an ungrouped account also stays on ungrouped accounts, because a grouped account is not a valid fallback for it.

4.4 Quota model

gsb queries two upstream sources:

  • batchexecute RPC qpEbW (CHECK_GEMINI_QUOTA), and
  • Gemini Web's GraphQL GetUserQuotaService pools GXU_FIVE_HOURLY and GXU_WEEKLY.
The resulting buckets are exposed in /v1/accounts, /v1/quota, gsb --accounts, and the Web UI:

| Window | Default credits | Meaning | |---|---|---| | 5h | 2,400 | Rolling five-hour window | | weekly | 48,384 | Seven-day weekly allowance |

The 5-hour window is nested under the weekly allowance, so gsb caps effective 5-hour remaining credits to the weekly remaining credits when the weekly pool runs lower.

When an account reaches 0 credits, non-lite models (gemini-flash, gemini-pro, gemini-carbon) are blocked or downgraded upstream until resetAt, while gemini-flash-lite remains usable. gsb also records synthetic per-model cooldowns when upstream silently substitutes a model, escalating 1m → 5m → 15m → 1h → 6h → 24h across repeated observations.

quotaWindow in /v1/accounts describes what the numbers cover, including weeklyExposed, weeklyRemaining, weeklyTotal, nextResetAt, and nextResetInSeconds.

4.5 Sessions and continuation cache

Gemini Web conversations are continued with internal metadata (cid, rid, rcid, context token). gsb hashes the conversation history, tools, and tool choice, then stores continuation metadata per (account, model, history prefix):

  • TTL: 30 minutes
  • Maximum entries: 256
  • Persisted to $BRIDGE_HOME/session-cache.json
  • Disabled with BRIDGE_SESSION_CACHE=0
  • Bypassed for temporary: true requests
Uploaded images and offloaded text attachments are cached separately for 25 minutes (maximum 128 entries) so repeated turns do not re-upload the same bytes.

BRIDGE_HISTORY_OFFLOAD=1 (default) uploads older conversation turns as text attachments when the inline prompt budget is exceeded. Setting it to 0 forces inline compaction instead.

4.6 Media cache

Generated images and videos are registered in memory and served through GET /v1/media/:

  • Maximum 96 entries, oldest evicted first
  • Not persisted across restarts
  • Served with Cache-Control: public, max-age=86400 and Access-Control-Allow-Origin: *
  • Unauthenticated, because upstream media URLs require Google cookies that browsers and curl do not have

5. Web Control Plane

Start gsb with ./start.sh, ./gsb --web, or BRIDGE_WEB_UI=1 and open http://127.0.0.1:20133/ui.

The UI is a single embedded HTML/CSS/JS page compiled into the binary. It can be excluded at build time with -tags nowebui.

5.1 Access control

  • Loopback access with no admin key is allowed.
  • Remote access (any non-loopback peer, or a loopback peer with a non-loopback X-Forwarded-For / X-Real-IP / CF-Connecting-IP / Forwarded header) is locked with HTTP 403 while BRIDGE_API_KEY is public and BRIDGE_ADMIN_KEY is empty.
  • When a key is configured, the page prompts for it. The key is kept in the tab's sessionStorage and sent as both Authorization: Bearer and X-GSB-Admin-Key.
  • Five failed admin authentications from one IP within 60 seconds trigger HTTP 429.
  • Admin requests with Sec-Fetch-Site: cross-site are rejected.
This matters for Docker: bridge NAT makes every request look remote, so the control plane stays locked until BRIDGE_ADMIN_KEY or a non-public BRIDGE_API_KEY is set.

5.2 Accounts tab

  • Quick-connect cards: Pool Base URL + pool key, per-account key chips (gsb-), and the MCP endpoint URL/key. Every value is click-to-copy.
  • Pool telemetry: current strategy, rotation cursor, active HTTP/2 sessions, and continuation cache hits/misses.
  • Filters: All, Ready, Exhausted, Attention.
  • Per-account row: status badge, email, 5-hour and weekly quota bars with reset countdowns, model list, RPM/stream telemetry, and a Key: chip.
  • Priority controls: ↑ / ↓ move an account in BRIDGE_ACCOUNT_PRIORITY; the order is persisted to config.env.
  • ⋯ action menu:
- Refresh Quota & Session (probe) - Pause Account in Pool / Resume Account in Pool (toggle) - Unquarantine Account or Reset Failure Counter (unquarantine) - Copy Key - Remove Account (two-step confirmation)
  • Refresh All queries upstream with /v1/accounts?live=1.
Status badges map the API statuses to UI labels: ok → Ready, disabled → Paused, exhausted → Exhausted, cooling → Cooling, degraded → Degraded, quarantined → Quarantined, expired → Expired, unknown → Unprobed.

5.3 Add Account

The + Add Account dialog offers two isolated methods. Only the cookie box is visible by default; account name, proxy, and group live under Optional settings.

  1. Isolated browser window (recommended): the daemon opens a disposable browser profile and auto-captures cookies on sign-in. On a headless VPS, the page falls back to the local helper at http://127.0.0.1:20134 when the server reports no browser/GUI. Run gsb --login-helper on your laptop and forward both ports (ssh -L 20133:127.0.0.1:20133 -L 20134:127.0.0.1:20134 user@vps).
  2. Incognito session cookies (works locally and over SSH):
- Open a new Incognito window (Ctrl+Shift+N), sign in to gemini.google.com/app, export cookies with Cookie-Editor (or copy the Cookie header), then immediately close the Incognito window without signing out. - Paste the JSON array or raw header into the dialog.

Optional fields:

  • Account name — leave blank to derive an ID from the Google email.
  • Outbound proxy — per-account HTTP/SOCKS5 proxy; blank uses BRIDGE_PROXY_SERVER.
  • Account group — only needed when you issue group keys; ignore it for a single pool key.
For an already-signed-in local browser profile, use the CLI: gsb --import-chrome .

5.4 Runtime configuration tab

The UI can change these settings live and optionally persist them to $BRIDGE_HOME/config.env (mode 0600):

| UI field | Environment variable | Allowed values | |---|---|---| | Account Strategy | BRIDGE_ACCOUNT_STRATEGY | rotate, lru, fill-first | | Quota Preflight | BRIDGE_QUOTA_PREFLIGHT | strict, advisory, off | | Account Priority | BRIDGE_ACCOUNT_PRIORITY | Comma-separated account IDs | | Default Thinking | BRIDGE_DEFAULT_THINKING | low, medium, high | | Max RPM per Account | BRIDGE_MAX_RPM_PER_ACCOUNT | 0–600 (0 disables) | | Max Concurrent Streams | BRIDGE_MAX_CONCURRENT_PER_ACCOUNT | 0–64 (0 disables) | | Reject Downgrade | BRIDGE_REJECT_DOWNGRADE | Boolean | | Stream reasoning_content | BRIDGE_LIVE_THOUGHTS | Boolean | | Stream tool_calls deltas | BRIDGE_LIVE_TOOL_CALLS | Boolean | | Keep pre-tool preamble | BRIDGE_TOOL_PREAMBLE | Boolean | | Offload long history | BRIDGE_HISTORY_OFFLOAD | Boolean |

Apply In-Memory Only changes the running daemon without writing a file. Save to config.env writes the values atomically. Values set in the process environment take precedence over config.env at the next start; the UI shows each field's source (env, file, runtime, or default).

Timeouts, proxy settings, keys, and transport fingerprint options are environment-only; they are not exposed in the UI.

5.5 Admin REST API

All admin routes require the admin key when one is configured, and are only mounted when the Web UI is enabled.

| Method | Path | Purpose | |---|---|---| | GET | /v1/admin/config | Effective runtime config, field sources, pool key, rotation cursor, uptime, metrics | | PATCH | /v1/admin/config | Update the 11 runtime fields above; persist: true writes config.env | | POST | /v1/admin/accounts/import | Import an account from a cookie payload; body: id, cookie, accountGroup, proxyUrl | | POST | /v1/admin/accounts/browser-login | Run an isolated browser login (4-minute timeout) and import the result | | DELETE | /v1/admin/accounts/ | Remove the account file and unload it | | POST | /v1/admin/accounts//probe | Bootstrap, refresh models/quota, clear failures on success | | POST | /v1/admin/accounts//rotate | Rotate rotating cookies | | POST | /v1/admin/accounts//unquarantine | Clear quarantine, failures, and synthetic cooldowns | | POST | /v1/admin/accounts//toggle | Pause/resume the account in the pool | | POST | /v1/admin/accounts//move | Reorder priority; body: {"direction":"up"\|"down"} |


6. Account onboarding

Each imported account is stored in ~/.gemini-stealth-bridge/accounts/.json, or under $BRIDGE_HOME/accounts/ when configured. Cookie imports bootstrap the account against Gemini Web before reporting success. Treat both source cookie exports and saved account files as credentials.

6.1 Sign in with --login (recommended)

./gsb --login acc1

--login launches a temporary Chrome/Chromium/Brave/Edge window using a disposable --user-data-dir, opens https://gemini.google.com/app, and monitors cookies via CDP. As soon as Google sign-in completes and the SAPISID cookie is present, gsb captures the session, verifies it against Gemini Web, and deletes the temporary browser directory.

  1. Run ./gsb --login and complete Google sign-in, including any 2FA or "verify it's you" prompt.
  2. If operating on a headless server without a local browser, paste a Cookie header or Cookie-Editor JSON export from an Incognito window that you immediately close after copying.
  3. Run ./gsb --probe and confirm the account reports models and 5h/weekly quota without an auth error.
If an account later fails with an expired-session error, repeat gsb --login instead of re-exporting stale cookies. ./gsb --rotate only renews rotating cookies such as __Secure-1PSIDTS; it cannot restore a revoked or never-established sign-in.

6.2 Reuse a signed-in browser profile

Despite its name, --import-chrome also recognizes other Chromium browsers and Firefox cookie databases. No browser is launched during import. Use this only when that browser profile is already signed in to Gemini.

# Auto-detect a local cookie database.
./gsb --import-chrome acc1

Choose a profile by name.

./gsb --import-chrome acc1 --chrome-profile "Profile 4"

Choose a profile directory or cookie database explicitly.

./gsb --import-chrome acc1 --cookie-db "$HOME/.config/google-chrome/Default"

--chrome-profile also accepts a directory or cookie database path. --cookie-db takes precedence when both are supplied.

Supported browsers and typical paths:

| Browser | Linux | macOS | Windows | |---|---|---|---| | Chrome | ~/.config/google-chrome/Default | ~/Library/Application Support/Google/Chrome/Default | %LOCALAPPDATA%\Google\Chrome\User Data\Default | | Chromium | ~/.config/chromium/Default | ~/Library/Application Support/Chromium/Default | %LOCALAPPDATA%\Chromium\User Data\Default | | Brave | ~/.config/BraveSoftware/Brave-Browser/Default | ~/Library/Application Support/BraveSoftware/Brave-Browser/Default | %LOCALAPPDATA%\BraveSoftware\Brave-Browser\User Data\Default | | Edge | ~/.config/microsoft-edge/Default | ~/Library/Application Support/Microsoft Edge/Default | %LOCALAPPDATA%\Microsoft\Edge\User Data\Default | | Vivaldi | ~/.config/vivaldi/Default | ~/Library/Application Support/Vivaldi/Default | %LOCALAPPDATA%\Vivaldi\User Data\Default | | Arc | — | ~/Library/Application Support/Arc/User Data | MSIX package directory %LOCALAPPDATA%\Packages\TheBrowserCompany.Arc_*\LocalCache\Local\Arc\User Data | | Firefox | ~/.mozilla/firefox//cookies.sqlite | ~/Library/Application Support/Firefox/Profiles//cookies.sqlite | %APPDATA%\Mozilla\Firefox\Profiles\\cookies.sqlite |

Arc is auto-detected on Windows through the MSIX glob. When no profile is given, auto-detection scans every known browser and prefers the first database that actually contains a SAPISID cookie, so an unsigned-in Chrome install does not shadow a signed-in Arc, Brave, or Edge profile.

# macOS Arc
./gsb --import-chrome acc1 --cookie-db "$HOME/Library/Application Support/Arc/User Data/Default"

Or just name the browser and let gsb find the profile.

./gsb --import-chrome acc1 --chrome-profile arc

Decryption depends on the browser, OS, and access to the browser's encryption keys:

  • Linux: keyring-backed cookies may require secret-tool and an unlocked desktop keyring.
  • macOS: gsb queries the login Keychain for the matching Safe Storage item (Chrome Safe Storage, Arc Safe Storage, Brave Safe Storage, …). macOS asks you to allow access the first time; CHROME_PASS overrides the lookup when Keychain access is unavailable.
  • Windows: DPAPI decryption requires the same Windows user context; App-Bound (v20) encryption requires a manual export.
A detected database is not a guarantee that its cookies can be decrypted. gsb warns when the profile looks active or when long-lived session cookies are already expired or about to expire.

6.3 Cookie header or JSON export

Import either a raw Cookie header value or a Cookie-Editor JSON array:

./gsb --import-cookie acc1 --cookie-file /path/to/cookies.json

The same payload can be supplied on stdin:

./gsb --import-cookie acc1 < /path/to/cookies.json

--cookie-value accepts an inline payload, but puts credentials in shell history and potentially process listings. Prefer a protected file or stdin.

6.4 Remote VPS and headless servers

Recommended flow:

# On your laptop:
ssh -L 20133:127.0.0.1:20133 -L 20134:127.0.0.1:20134 user@vps

On the VPS:

gsb --web

Then open http://127.0.0.1:20133/ui locally and use + Add Account:

  • Incognito session cookies (works everywhere), or
  • Isolated browser window → Launch with gsb --login-helper running on the laptop.
gsb --login-helper listens on 127.0.0.1:20134 and exposes POST /browser-login. It allows CORS only from http://localhost: and http://127.0.0.1: origins.

6.5 Additional accounts

Repeat the login with another name:

./gsb --login acc2

Or, if that account is already signed in to a different browser profile:

./gsb --import-chrome acc2 --chrome-profile "Profile 2"

Each account has its own cookie state, model list, quota state, and transport session. Files belonging to the same Google account are deduplicated by email.

6.6 Account groups

Assign accounts to a group and map a key to that group:

BRIDGE_ACCOUNT_GROUP="acct-a:team-a,acct-b:team-a,acct-c:team-b" \
BRIDGE_GROUP_TOKENS="alice-key:team-a,bob-key:team-b" ./gsb

The group can also be set per account in the Web UI's Account Group field, or with accountGroup in the account file.

A group key:

  • serves only that group's accounts,
  • fails over only inside the group,
  • sees only the group's accounts in /v1/accounts and /v1/quota,
  • never consumes another group's quota.

6.7 Per-account egress proxies

BRIDGE_EGRESS_PER_ACCOUNT="acct-a:socks5://127.0.0.1:1081,acct-b:http://user:[email protected]:3128" ./gsb

The same is available in the Web UI's Outbound Proxy field and as proxyUrl in the account file. The value applies to that account's transport session only; changing it rebuilds the session so no connection survives from the previous egress. Accounts without their own proxy use BRIDGE_PROXY_SERVER.

BRIDGE_PROXY_SERVER applies to Gemini transport, not to the separate remote-image downloader. It does not provide a process-wide proxy or privacy boundary.


7. Client integration

7.1 Direct connection

Configure any client that supports a custom OpenAI-compatible endpoint:

| Setting | Value | |---|---| | Base URL | http://127.0.0.1:20133/v1 | | API key | Pool key (BRIDGE_API_KEY, default public) for automatic rotation/failover, a group key, or gsb-acc1 to pin acc1 | | API mode | Chat Completions or Responses, matching the client's adapter | | Model | An ID from GET /v1/models using the same key |

Choose Chat Completions for clients that consume choices / tool_calls, or Responses for clients that consume output items and response.* events. Client support for reasoning fields varies. OpenAI compatibility does not imply that every coding agent or every client-specific feature has been tested.

7.2 Through 9router

Add gsb as an OpenAI-compatible provider in 9router:

| Provider setting | Value | |---|---| | Name | Gemini Stealth Bridge | | Base URL | http://127.0.0.1:20133/v1 | | API type | Chat Completions (chat) or Responses (responses) | | Prefix | gsb | | Credential | Pool key (BRIDGE_API_KEY) to let gsb rotate accounts, or an account key (gsb-acc1) to pin one account |

Point the coding agent at the router's URL with the router's own API key; use the model name/prefix exposed by the router, for example gsb/gemini-flash. The bridge also accepts the gsb/ model prefix directly, plus google/, models/, vx/, and vertex/ prefixes and a @think= suffix.

Prefer one connection with the pool key. Account rotation belongs to gsb because it is the only component that knows per-account quota, quarantine state, and conversation continuation affinity. A router that spreads requests across per-account connections cannot see any of that: it only learns about exhaustion from a 429, and it will move the next turn to a different account, discarding the cached Gemini session. Use per-account connections only when you deliberately want to pin traffic to one account.

The base URL must be reachable from the router process. Inside a container, 127.0.0.1 refers to that container, not the host running gsb. Any non-loopback deployment needs the security measures in Security.

If your router caches or overrides model metadata, verify what survives:

# Check the bridge alone (all fields required)
./scripts/check-router-metadata.sh http://127.0.0.1:20133 public

Compare the bridge against the router; reports exactly which fields the router dropped

./scripts/check-router-metadata.sh --compare \ http://127.0.0.1:20133 http://127.0.0.1:20130 - sk-router-key

The compare form distinguishes a bridge problem from a router problem. A router that drops capabilities.thinkingLevels is why a harness cannot render a low/medium/high selector, and the script names that field rather than blaming the bridge.

7.3 MCP clients

gsb speaks MCP Streamable HTTP at POST /mcp (alias /v1/mcp) and exposes one tool, web_search.

{
  "mcpServers": {
    "gemini-search": {
      "type": "http",
      "url": "http://127.0.0.1:20133/mcp",
      "headers": { "Authorization": "Bearer public" }
    }
  }
}

The endpoint accepts the pool key, a group key, or an account key. See MCP server for protocol details.

7.4 Coding agents

For agent workflows, point the agent at the Chat Completions or Responses base URL with the pool key. When the prompt asks for an image, diagram, or video, gsb injects the server-side generate_media tool so the agent can save generated media directly into the workspace. See Native image and video generation.


8. API reference

8.1 Authentication

All /v1/ endpoints except /v1/media/ and /v1/admin/ use the same credential resolver. /v1/admin/ uses the admin key resolver. /health, /readyz, /, /ui, and /media/* have their own rules noted below; /mcp uses the API-key resolver.

Accepted forms:

Authorization: Bearer <key>
x-api-key: <key>
x-goog-api-key: <key>
GET /v1/models?key=<key>

Admin routes additionally accept X-GSB-Admin-Key.

Failure shape for a bad API key:

{
  "error": {
    "message": "invalid api key",
    "type": "authentication_error"
  }
}

Chat and Responses errors also include a code field. Simpler handlers such as /v1/models, /v1/accounts, /v1/quota, and the media proxy return only message and type.

8.2 Common response headers

Chat, Responses, and MCP responses can include:

| Header | Meaning | |---|---| | X-GSB-Account | Account ID that served the request | | X-GSB-Model | Model ID used for the request | | X-GSB-Attempt | 1-based attempt number | | X-GSB-Upstream-Status | Last upstream HTTP status observed | | X-GSB-Retry-Reason | Why the last retry/failover happened (model_substitution, session_busy, rate_limit_error, …) | | X-GSB-Duration-Ms | MCP only: search duration | | X-GSB-Sources | MCP only: number of citations | | Retry-After | Seconds to wait when quota/rate limited | | X-Resets-At | RFC 3339 quota reset timestamp, when known |

Chat and Responses stream headers flush as soon as the served model identity is verified. When downgrade rejection is disabled or BRIDGE_STREAM_BUFFER=eager is set, BRIDGE_EARLY_SSE and BRIDGE_EARLY_SSE_GRACE_MS can flush them earlier so long thinking turns do not hit proxy header timeouts.

8.3 Error format and codes

Non-streaming errors use HTTP status codes with the envelope above. Streaming requests can emit an error after HTTP 200; clients must inspect SSE events, not only the status line.

| Code | Typical HTTP | Meaning | |---|---|---| | invalid_request_error | 400 | Malformed body, tool schema, image, or model field | | authentication_error | 401 | Invalid or missing API key | | auth_unavailable | 503 | No accounts configured or loaded | | account_banned | 403 | Upstream restricted the account (TOS_VIOLATION / PERMISSION_DENIED); account is quarantined | | upstream_auth_error | 401 | Saved Google session expired or rejected | | auth_redirect | 401 | Upstream redirected to Google sign-in | | model_not_found | 404 | Account cannot serve the requested model | | model_substitution | 429 | Upstream served a different model and downgrade rejection is enabled | | model_identity_unknown | 502 | Upstream did not identify the served model | | model_identity_conflict | 502 | Upstream reported conflicting served models | | rate_limit_error | 429 | Quota, RPM, concurrency, or session-busy limit | | session_busy | 429 | Transport session queue or wait timeout | | tool_call_error | 502 | Tool call missing, malformed, or schema-invalid | | empty_upstream_response | 502 | Upstream completed without an answer | | incomplete_upstream_response | 502 | Upstream ended before the candidate completed | | upstream_error_reply | 502 | Gemini returned a canned refusal/error text | | upstream_revision | 502 | Upstream changed an answer or tool call after part of it was already streamed | | upstream_http_error | 502+ | Upstream returned a non-2xx status | | upstream_error | 502 | Generic upstream failure type, also used by the media proxy | | upstream_parse_error | 502 | Malformed upstream frame | | upstream_callback_error | 502 | A frame callback failed while processing the stream | | upstream_transport_error | 502 | TLS/HTTP transport failure | | model_discovery_error | 502 | Upstream model registry was malformed or unavailable | | upstream_connect_timeout | 504 | First-byte timeout | | upstream_timeout | 504 | Overall upstream timeout | | upstream_stalled | 504 | No generation frame within BRIDGE_STALL_TIMEOUT_MS | | upstream_upload_error | 502 | Attachment upload failed | | search_timeout | 504 | MCP search deadline exceeded | | media_generation_failed | 502 | No media returned by upstream | | not_found_error | 404 | Unknown media ID or route | | client_cancelled | 499 | The caller disconnected or cancelled the request | | server_error | 500 | Local filesystem/state failure | | remote_web_ui_locked | 403 | Remote control-plane access while no admin key is configured | | admin_auth_required | 401 | Admin key required | | admin_auth_rate_limited | 429 | Too many failed admin auth attempts | | cross_site_forbidden | 403 | Cross-site admin request |

For a pool-key request, a 429 may say that no pool account can serve the requested model and summarize unavailable accounts by reason (exhausted, substitution_cooldown, cooling, rate_limited, busy, model_unavailable, quarantined). The message reports counts rather than emails or credentials; use /v1/accounts?live=1 for per-account detail. Account-key requests keep the precise account-specific error.

8.4 Health and readiness

| Method | Path | Auth | Description | |---|---|---|---| | GET | /health | None | {status, service, transport, profile, accounts, models, upstream}; proves the daemon is running, not that upstream generation works | | GET | / | None | /health when the Web UI is disabled; the UI when enabled | | GET | /readyz | None | Per-account models_fresh, quota_fresh, auth_ok, quarantined; ?strict=1 returns 503 when no healthy account is available |

/readyz reports degraded when there are no accounts, no healthy account, any quarantined account, or fewer healthy accounts than loaded accounts. A model list is considered fresh for twice BRIDGE_MODEL_REFRESH_MS; quota is fresh for 30 minutes.

8.5 Models

GET /v1/models
GET /v1/models?fresh=1

Returns an OpenAI-style list plus OpenRouter-style metadata:

  • id, display_name, description, aliases
  • context_length, context_window, max_output_tokens, max_completion_tokens
  • reasoning, supported_parameters
  • architecture, top_provider, pricing
  • thinking (mode, default, defaultLevel, efforts, requiresEffort)
  • capabilities (vision, search, tools, reasoning, thinkingLevels, defaultThinkingLevel, limitsSource, …)
Responses carry an ETag; send If-None-Match to get 304. The HTTP response is cacheable for 60 seconds (Cache-Control: max-age=60), while the per-account upstream model registry is cached for 10 minutes and refreshed in the background every BRIDGE_MODEL_REFRESH_MS. ?fresh=1 forces live discovery.

Model IDs are whatever Gemini Web currently advertises, for example gemini-flash, gemini-flash-lite, gemini-pro, or gemini-carbon. Aliases and display names are matched too. Prefixes gsb/, google/, models/, vx/, and vertex/ are stripped before matching; a @think= suffix sets thinking effort for that request.

Upstream renames a model in place without notice: on 2026-11-10 the frontier tier moved from gemini-argon to gemini-carbon. A name that upstream has stopped advertising no longer resolves and returns 404 model_not_found. Pin clients to a name after checking GET /v1/models rather than assuming one lasts across an upstream release.

gsb --models-json prints the same payload without starting the daemon. With no loaded accounts it prints a static fallback list.

8.6 Accounts, quota, sessions, refresh

| Method | Path | Description | |---|---|---| | GET | /v1/accounts | Account IDs, emails, computed status/statusReason, failures, timestamps, provenance, quarantine/disabled state, models, quota buckets, quotaWindow, load telemetry. ?live=1 or ?fresh=1 refreshes upstream first | | GET | /v1/quota | Cached quota buckets; ?fresh=1 queries upstream. A cold cache triggers a refresh automatically | | GET | /v1/sessions | Transport session status (ready, busy, queued, active, stale, idleSec) plus continuation cache hits/misses | | POST | /v1/refresh | Reloads credential files and refreshes the model registry. Rejects requests carrying an Origin header |

Account objects include:

{
  "id": "acc1",
  "email": "[email protected]",
  "status": "ok",
  "statusReason": "",
  "capacity": 2,
  "fails": 0,
  "lastError": "",
  "lastFailAt": "",
  "models": ["gemini-flash", "gemini-pro"],
  "quota": [{"id": "5h", "label": "5h Window", "total": 2400, "remaining": 2310, "resetAt": "…"}],
  "quotaWindow": {"scope": "rolling", "label": "5h", "nestedUnderWeekly": true, "weeklyExposed": true},
  "capturedAt": "…",
  "rotatedAt": "…",
  "modelsAt": "…",
  "quotaAt": "…",
  "lastUsedAt": "…",
  "quarantined": false,
  "quarantineReason": "",
  "disabled": false,
  "accountGroup": "team-a",
  "importedFrom": "web-ui",
  "profileId": "cookie-export",
  "proxyConfigured": true,
  "activeStreams": 0,
  "recentRpm": 0,
  "peakRpm": 1,
  "totalRequests": 12,
  "offloadRequests": 0,
  "surfaces": {"geminiWeb": "ok"}
}

8.7 Chat Completions

POST /v1/chat/completions
Content-Type: application/json

Minimal request:

{
  "model": "gemini-flash",
  "messages": [{ "role": "user", "content": "Reply with OK." }]
}

Accepted fields:

| Field | Notes | |---|---| | model | Optional; empty uses the first model on the selected account. Supports router prefixes and @think= | | messages | Required, non-empty array. Roles: system, developer, user, assistant, tool, function | | stream | Boolean; SSE when true | | tools | Array of {"type":"function","function":{name,description,parameters}}. Tool names match ^[A-Za-z0-9_.:-]{1,128}$; duplicates are rejected | | tool_choice | auto, none, required, or {"type":"function","function":{"name":"…"}} | | temporary | Boolean; requests a temporary Gemini conversation and skips continuation-cache reuse | | reasoning_effort | low, medium, high (also numeric aliases) | | thinking_level, thinkingLevel | Aliases for the effort | | reasoning | Object: enabled, effort, level | | thinking | Object: enabled, type, effort, level, thinkingLevel, budget_tokens | | thinking_budget | Token budget mapped to low ≤4096, medium ≤16384, high above | | thinkingConfig, generationConfig.thinkingConfig | thinkingLevel or thinkingBudget | | output_config | effort |

Sampling parameters such as temperature, top_p, max_tokens, and stop are accepted but ignored.

Message content can be a string or an array of parts. Supported image shapes:

{ "type": "image_url", "image_url": { "url": "data:image/png;base64,..." } }
{ "type": "input_image", "image_url": "https://example.com/image.png" }
{ "type": "image", "data": "<base64>", "mimeType": "image/png" }

Remote images are limited to 20 MiB, follow at most 5 redirects, and reject private/loopback destinations unless BRIDGE_ALLOW_PRIVATE_IMAGE_URLS=1.

The total request body is limited to 32 MiB.

Streaming Chat

Use curl -N to avoid buffering:

curl -N -sS http://127.0.0.1:20133/v1/chat/completions \
  -H 'Authorization: Bearer gsb-acc1' \
  -H 'Content-Type: application/json' \
  -d "{\"model\":\"$MODEL\",\"stream\":true,\"reasoning_effort\":\"high\",\"messages\":[{\"role\":\"user\",\"content\":\"Explain how this bridge works.\"}]}"

Wire format:

  • data: {"object":"chat.completion.chunk", ...} events
  • a first role-only delta ({"role":"assistant","content":""})
  • text in choices[0].delta.content
  • reasoning in choices[0].delta.reasoning_content when extracted and enabled
  • tool calls in choices[0].delta.tool_calls
  • a final chunk with finish_reason (stop or tool_calls) and usage
  • data: [DONE]
  • : ping comments every 15 seconds
usage is included on the final chunk regardless of stream_options.

Streaming behavior:

  • The first client-visible delta is held until the served model identity is verified when downgrade rejection is enabled; BRIDGE_STREAM_BUFFER=eager or BRIDGE_REJECT_DOWNGRADE=0 relaxes that.
  • With tools enabled, prose answers longer than 240 bytes stream live once identity is verified. Mid-answer tool envelopes and fenced look-alikes stay out of the streamed prefix until parsing commits the turn; quoted envelope examples pass through as text.
  • BRIDGE_LIVE_TOOL_CALLS=false (default) buffers and validates the complete tool call before emitting it; true emits incremental deltas and cannot repair a late upstream revision.
  • BRIDGE_LIVE_THOUGHTS=false suppresses reasoning_content without changing the upstream thinking level.
  • Errors can arrive after HTTP 200 as data: {"error":{...}}; inspect the stream, not just the status code.

Non-streaming Chat

```json { "id": "chatcmpl-…", "object": "chat.completion", "created": 1760000000, "model": "gemini-flash", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "OK", "reasoning_content": "…", "tool_calls": [] }, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 12, "completion_tokens": 3, "total_tokens": 15

... (README truncated for length)

Chat with me