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 | claudeOr follow the manual steps below.
1. Log in
shunt login antigravityThis runs a Google authorization-code flow using Antigravity’s own OAuth client and writes
~/.shunt/antigravity-auth.json, which shunt reads and auto-refreshes. 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/experimentsandconfigsThe 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).
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:
efforton the[[routes]]entry, or on the provider — an explicit pin.xhighandmaxfold ontohighhere 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.- The request’s
output_config.effort(Claude Code sendslow/medium/high/xhigh/max;xhighandmaxfold ontohigh). thinking.type = "enabled"—budget_tokensat or below 2048 islow, at or below 8192 ismedium, above that ishigh. An enabled block that names no budget uses the same1024default the translated request carries, so it lands inlow.- 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
imageblock becomes GeminiinlineData. A URL image source is rejected with a400(URL image sources are not supported by the Gemini adapter). - Tool results cannot carry rich media. A
tool_resultwhose content includesimageordocumentblocks is rejected.
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.