gemini-stealth-bridge (gsb)
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 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
- What
gsbprovides - Requirements
- Quick start
- Core concepts
- Web Control Plane
- Account onboarding
- Client integration
- API reference
- Native image and video generation
- Web search grounding
- Configuration reference
- Data layout
- Security
- Docker and deployment
- Operations and troubleshooting
- How it works
- Development
- 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, andmax_output_tokensare 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_idis 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-argontogemini-carbonon 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 fromGET /v1/models; a name upstream has dropped returns404 model_not_found. - Documented limits actually apply: the limit table was keyed from
displayName, but the live registry carries the versioned name indescriptionand a UI label indisplayName, so every model silently reportedlimitsSource: 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=0suppresses it. - Built-in MCP server:
POST /mcpexposesweb_searchwith a per-calltimeout_secondsdeadline. - Per-account egress proxies:
BRIDGE_EGRESS_PER_ACCOUNTor the Web UI's Outbound Proxy field. - Group keys:
BRIDGE_GROUP_TOKENS+BRIDGE_ACCOUNT_GROUPfor multi-user deployments. - Faster tool streaming: the tool-envelope detector is a hand-rolled scan instead of a regexp over the whole accumulated answer.
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/.
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/. 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.
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=publicmeans the pool key ispublicand requests without a key are accepted. The Web UI also derives a stablegsb-pool-token fromBRIDGE_HOMEand 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_TOKENSadds explicittoken:account_idmappings. It does not remove the automatic aliases. When it is set, unknown tokens no longer trigger a hot reload of account files.BRIDGE_GROUP_TOKENSaddstoken:group_namemappings. A group key cannot reach accounts outside its group.BRIDGE_ADMIN_KEYgrants the admin scope: all accounts, plus/v1/admin/*. A non-publicBRIDGE_API_KEYalso satisfies admin authentication whenBRIDGE_ADMIN_KEYis 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 oldestLastUsed.fill-first— drain a fixed order set byBRIDGE_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.
- 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.
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:
batchexecuteRPCqpEbW(CHECK_GEMINI_QUOTA), and- Gemini Web's GraphQL
GetUserQuotaServicepoolsGXU_FIVE_HOURLYandGXU_WEEKLY.
/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: truerequests
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=86400andAccess-Control-Allow-Origin: * - Unauthenticated, because upstream media URLs require Google cookies that browsers and
curldo 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/Forwardedheader) is locked with HTTP403whileBRIDGE_API_KEYispublicandBRIDGE_ADMIN_KEYis empty. - When a key is configured, the page prompts for it. The key is kept in the tab's
sessionStorageand sent as bothAuthorization: BearerandX-GSB-Admin-Key. - Five failed admin authentications from one IP within 60 seconds trigger HTTP
429. - Admin requests with
Sec-Fetch-Site: cross-siteare rejected.
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 inBRIDGE_ACCOUNT_PRIORITY; the order is persisted toconfig.env. ⋯action menu:
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.
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.
- 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:20134when the server reports no browser/GUI. Rungsb --login-helperon your laptop and forward both ports (ssh -L 20133:127.0.0.1:20133 -L 20134:127.0.0.1:20134 user@vps). - Incognito session cookies (works locally and over SSH):
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.
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/ | Bootstrap, refresh models/quota, clear failures on success |
| POST | /v1/admin/accounts/ | Rotate rotating cookies |
| POST | /v1/admin/accounts/ | Clear quarantine, failures, and synthetic cooldowns |
| POST | /v1/admin/accounts/ | Pause/resume the account in the pool |
| POST | /v1/admin/accounts/ | Reorder priority; body: {"direction":"up"\|"down"} |
6. Account onboarding
Each imported account is stored in ~/.gemini-stealth-bridge/accounts/, 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.
- Run
./gsb --loginand complete Google sign-in, including any 2FA or "verify it's you" prompt. - If operating on a headless server without a local browser, paste a
Cookieheader or Cookie-Editor JSON export from an Incognito window that you immediately close after copying. - Run
./gsb --probeand confirm the account reports models and 5h/weekly quota without an auth error.
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/ | ~/Library/Application Support/Firefox/Profiles/ | %APPDATA%\Mozilla\Firefox\Profiles\ |
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-tooland an unlocked desktop keyring. - macOS:
gsbqueries the login Keychain for the matchingitem (Safe Storage Chrome Safe Storage,Arc Safe Storage,Brave Safe Storage, …). macOS asks you to allow access the first time;CHROME_PASSoverrides the lookup when Keychain access is unavailable. - Windows: DPAPI decryption requires the same Windows user context; App-Bound (
v20) encryption requires a manual export.
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-helperrunning 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/accountsand/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,aliasescontext_length,context_window,max_output_tokens,max_completion_tokensreasoning,supported_parametersarchitecture,top_provider,pricingthinking(mode,default,defaultLevel,efforts,requiresEffort)capabilities(vision,search,tools,reasoning,thinkingLevels,defaultThinkingLevel,limitsSource, …)
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_contentwhen extracted and enabled - tool calls in
choices[0].delta.tool_calls - a final chunk with
finish_reason(stoportool_calls) andusage data: [DONE]: pingcomments 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=eagerorBRIDGE_REJECT_DOWNGRADE=0relaxes 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;trueemits incremental deltas and cannot repair a late upstream revision.BRIDGE_LIVE_THOUGHTS=falsesuppressesreasoning_contentwithout 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)