Skip to content

Antigravity

Reach the Google Antigravity backend over HTTP — login, project discovery, model slugs, thinking, and what the adapter carries.

Updated View as Markdown

The built-in antigravity provider reaches Google’s Antigravity backend over HTTP at daily-cloudcode-pa.googleapis.com — the daily- control plane the Antigravity client itself addresses for both discovery and inference. It speaks the same Code Assist protocol as the gemini provider — shunt translates Anthropic Messages to generateContent / streamGenerateContent — but it authenticates with an Antigravity subscription token and identifies itself as ideType: ANTIGRAVITY during project discovery.

Antigravity has a second, older transport. kind = "antigravity_cli" runs the local agy binary as a subprocess and is deprecated — see the deprecated antigravity-cli transport. This page covers the HTTP provider, which needs none of that machinery.

Quick start

Let a coding agent wire it up for you — shunt add prints an embedded setup blueprint (offline and read-only; the agent edits the config, the command never does):

shunt add upstream antigravity --print | claude

Or follow the manual steps below.

1. Log in

shunt login antigravity

This runs a Google authorization-code flow using Antigravity’s own OAuth client and writes ~/.shunt/antigravity-auth.json, which shunt reads and auto-refreshes. For more than one Google account, log each in under a name instead:

shunt login antigravity --name <account-name>

A named login runs the same flow but stores the credential at ~/.shunt/accounts/antigravity/<account-name>.json (0600, in a 0700 directory; override the store with SHUNT_ANTIGRAVITY_ACCOUNTS_DIR) instead of the singleton file, so the two never overwrite each other. --long-lived and --mode are rejected here, as with the other OAuth-only logins. The flow asks for five scopes:

https://www.googleapis.com/auth/cloud-platform
https://www.googleapis.com/auth/userinfo.email
https://www.googleapis.com/auth/userinfo.profile
https://www.googleapis.com/auth/cclog
https://www.googleapis.com/auth/experimentsandconfigs

The last two are the reason a Gemini CLI login cannot be reused here, and vice versa: a ~/.gemini/oauth_creds.json token never carries cclog or experimentsandconfigs. google_oauth and antigravity_oauth are not interchangeable.

Login also resolves your Code Assist project — loadCodeAssist first, then onboardUser to provision one for a first-time account — and stores the project id in the credential file. No discovery sits in front of your first request. If discovery fails, login still saves the token and retries on the first request.

2. Configure the upstream

The provider is built in, so a [[routes]] entry naming provider = "antigravity" works with no provider table of your own. Declaring it as an ordered upstream is different: the Antigravity kinds have no [[upstreams]] preset, so provider = "antigravity" is not available there and you must set kind, base_url, and auth yourself:

[[upstreams]]
name = "anthropic"
provider = "anthropic"   # keep Anthropic as the default for unrouted models (e.g. claude-*)

[[upstreams]]
name = "antigravity"
kind = "antigravity"
base_url = "https://daily-cloudcode-pa.googleapis.com"
auth = "antigravity_oauth"

Ordered [[upstreams]] replace shunt’s built-in providers, so an explicit config must also declare the anthropic default it still falls back to (server.default_provider defaults to anthropic).

antigravity_oauth is pool-capable, like claude_oauth, chatgpt_oauth, and kimi_oauth: select accounts with auth = { mode = "antigravity_oauth", accounts = ["personal", "work"] } (or a single account = "..."; the two are mutually exclusive), or omit both to scan the whole shunt-managed Antigravity account store. With no configured accounts and an empty store, the provider falls back to the singleton credential from step 1.

The legacy [providers.antigravity] table form remains supported — but do not mix [[upstreams]] and [providers.*] in one file.

3. Route a model

shunt keeps no model allowlist for this provider: the resolved upstream_model is sent to the backend as written, and the backend decides. Antigravity serves the Gemini-family slugs the Code Assist gemini provider does not accept — but it publishes them with the effort in the id, in one of two shapes: a -low / -medium / -high suffix (Pro has only -low and -high), or a single -tiered id that takes the effort as a request field instead. Which shape you get is per account and changes over time — one account is served gemini-3.8-flash-tiered, another gemini-3.8-flash-medium. Run agy models to list the catalog as it stands today; at the time of writing it holds gemini-3.8-flash-*, gemini-3.7-flash-*, gemini-3.6-flash-*, gemini-3.1-pro-low / -high, claude-sonnet-4-6, claude-opus-4-6-thinking, and gpt-oss-120b-medium.

A bare slug is not served: daily-cloudcode-pa.googleapis.com answers it with a 404. Against the production host, shunt’s earlier request — a bare id and the plain Code Assist envelope — came back as a misleading 429 RESOURCE_EXHAUSTED (“check quota”), which reads as a rate limit; the probes changed both inputs together, so which one produced that 429 is not established.

So that neither shape has to be guessed, shunt reads your account’s own catalog (fetchAvailableModels) on the request path — cached for ten minutes per account, bounded, and fail-open — and picks the id that account actually publishes.

Map a local alias to the exact Antigravity slug:

[[models]]
id = "claude-gemini-3.6-flash-via-antigravity"
display_name = "[AGY ] Gemini-3.6-Flash"

[[routes]]
model = "claude-gemini-3.6-flash-via-antigravity"
provider = "antigravity"
upstream_model = "gemini-3.6-flash-medium"

You do not have to write the suffix by hand. For a bare gemini-* id, shunt resolves the tier itself and appends it, taking the first signal that applies:

  1. effort on the [[routes]] entry, or on the provider — an explicit pin. xhigh and max fold onto high here too, and matching ignores case and surrounding whitespace. A level shunt does not recognise is passed through as written (trimmed and lower-cased), so you can name a tier the catalog adds later.
  2. The request’s output_config.effort (Claude Code sends low/medium/high/xhigh/max; xhigh and max fold onto high).
  3. thinking.type = "enabled" — budget_tokens at or below 2048 is low, at or below 8192 is medium, above that is high. An enabled block that names no budget uses the same 1024 default the translated request carries, so it lands in low.
  4. Otherwise medium.

The catalog then decides what that tier becomes. If your account publishes {id}-{tier}, that is what goes out. If it publishes only {id}-tiered, shunt sends the tiered id and carries the tier in generationConfig.thinkingConfig.thinkingLevel instead (alongside any thinkingBudget, which the backend accepts together). If it publishes neither but does publish other tiers of the same model, the nearest published tier wins, ties resolving upward — which is how a medium on a -pro id becomes -high without any Pro-specific rule.

A suffix you pinned by hand gets the same treatment. If your account no longer publishes gemini-3.6-flash-medium, the suffix still says which tier you wanted, so shunt re-resolves it through the rules above rather than forwarding an id the catalog just proved is a 404; a catalog that says nothing about the family leaves your pin alone. A -tiered id you pinned yourself keeps its id and still carries your effort as the thinkingLevel, so pinning the exact published id costs you no effort control — and if your account later moves back to suffixed ids, that pin is re-resolved the same way. Only a catalog fetched just now can prove an id is gone: while shunt is serving the last known set after a failed refresh, every pin goes out as written.

When the catalog is unavailable (a discovery outage, a backend that does not answer fetchAvailableModels), shunt falls back to appending -{tier} with that Pro clamp — the 0.40.0 behaviour — so a failed lookup costs you the guess, never the request. A failed lookup is remembered for a minute before it is retried, so an unreachable control plane costs one bounded request per minute rather than one per message, and only Gemini ids are looked up at all. An unrecognised configured effort is appended as written, per rule 1, and is never clamped without a catalog; with one, it folds onto a tier the account publishes, because the catalog has just said that tier is not served. On the -tiered path it folds onto medium, because the backend rejects a thinkingLevel it does not know. An id your catalog publishes verbatim and any non-Gemini id (claude-sonnet-4-6, gpt-oss-120b-medium) is sent exactly as written. An unrecognised output_config.effort falls back to medium rather than reaching the model id. A request that explicitly disables thinking (thinking.type = "disabled") never gains a thinkingLevel: the translated thinkingBudget: 0 stands on its own.

Antigravity also offers Claude models, but shunt does not yet implement the request rewrites they need (#368). Nothing rejects such a slug locally — it reaches the backend as written — so route only the Gemini-family slugs for now.

What the adapter carries

The adapter streams assistant text and thinking as Anthropic SSE, and bridges your client’s tools natively: tool_use becomes a Gemini functionCall, your tool_result becomes a functionResponse, and tool declarations and tool choice are translated with it. System prompts become systemInstruction. Token usage is Google’s reported count — promptTokenCount and candidatesTokenCount map to input_tokens and output_tokens.

Thinking follows the request: thinking.type = "enabled" sets thinkingConfig.thinkingBudget from budget_tokens (default 1024), and "disabled" sets it to 0. An enabled thinking block is also one of the signals that picks the model’s effort tier, as described under Route a model above.

Every request additionally carries the agent identity the Antigravity client sends: userAgent: "antigravity", requestType: "agent", a per-request requestId, and a sessionId derived from the earliest user text in the conversation so follow-up turns land on the same session. The gemini provider’s Code Assist requests carry none of these.

Tool schemas are adapted to Gemini’s Schema dialect — on this provider and on gemini, which shares the adapter. $schema, $id, $comment, propertyNames, patternProperties, exclusiveMinimum, exclusiveMaximum, and const are dropped; every other keyword is forwarded as written. A type list keeps its first non-null entry, a null in the list sets nullable: true, and a null-only list becomes a nullable string. Any schema that is not an array — a union that settled on another member, or a plain scalar — drops prefixItems and any items that is not a schema object, keeping an object-valued items as written; a typeless schema counts as an array when it carries prefixItems or an array- or object-valued items, and otherwise loses a lone boolean items. Every array is sent with a single items schema: a tuple — prefixItems, or draft-07’s array-valued items — is folded into one, where identical positions keep their schema, positions that agree only on a non-array type keep that type, and otherwise the first position’s schema stands. A typed items schema already sitting beside prefixItems is kept as written and the positions are dropped. An element whose type lives in anyOf/oneOf/allOf arms is folded the same way. An array that says nothing about its elements, or whose element schema declares no type, is sent with items: {"type": "string"} unless the element implies its type through enum or properties. These fallbacks narrow the element type rather than rejecting the request, so a tool whose tuple arrives typed as string is this adaptation at work, not a routing error.

Two limits are worth knowing before you route production traffic:

  • Images must be inline. A base64 image block becomes Gemini inlineData. A URL image source is rejected with a 400 (URL image sources are not supported by the Gemini adapter).
  • Tool results cannot carry rich media. A tool_result whose content includes image or document blocks is rejected.

Account pools and failover

Each pool attempt resolves its own credential — access token plus cached project id — and rebuilds the request (catalog lookup included) for that account, so a rotation never leaks one account’s project into another’s turn. A 429 RESOURCE_EXHAUSTED cools that account down and tries the next one; a client error (4xx other than 401/429) is relayed without rotating. The response carries x-shunt-account naming the account that served it. If every account fails, the last upstream error is relayed, so the cause stays visible.

Usage reporting

Setting usage_refresh_seconds also polls every imported (refreshable) antigravity_oauth account against Code Assist’s retrieveUserQuotaSummary RPC. Production cloudcode-pa.googleapis.com configurations are normalized to Antigravity’s daily-cloudcode-pa.googleapis.com control plane, matching inference and model-catalog traffic.

The response is grouped into two shared model-family quota pools: Gemini Models and Claude + GPT Models. Each group carries its provider-native 5-hour and weekly windows when the account tier exposes them, including each window’s remaining fraction and reset time. The dashboard renders those grouped windows rather than one bar per catalog model. These buckets are display-only: pool selection and rotation do not consult them.

Verify

shunt check    # -> config ok
shunt run
curl -sS http://127.0.0.1:3001/v1/messages \
  -H 'anthropic-version: 2023-06-01' \
  -H 'content-type: application/json' \
  -d '{"model":"claude-gemini-3.6-flash-via-antigravity","max_tokens":16,"messages":[{"role":"user","content":"Reply with OK."}]}'

Confirm the response’s x-gateway-upstream header names antigravity, then point Claude Code at shunt.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close