Gemini, Vertex AI and Model Garden
Four Google doors to the same models, each with its own credential and price namespace. How to pick the one that prices yours.
On this page
Google sells the same models through several front doors, and they do not share a credential, a URL shape or a price list. Picking the wrong descriptor does not produce a wrong number; it produces no number, which is easier to spot but still a surprise if you were expecting one.
There are 4 doors here, and 13 descriptors behind them. This page tells you which one you are standing at.
The four doors
| Door | Descriptor | Credential | Prices under |
|---|---|---|---|
| Gemini API (AI Studio) | gemini |
an API key in x-goog-api-key |
gemini/, 63 keys |
| Gemini on Vertex | vertex_ai_language, or vertex_ai |
a service-account key file | vertex_ai-language-models/ with 37 keys, vertex_ai/ with 13 |
| Claude on Vertex | vertex_ai_anthropic |
a service-account key file | vertex_ai-anthropic_models/, 35 keys |
| Model Garden open models | 9 vertex_ai_* descriptors |
a service-account key file | one prefix per family, 49 keys between them |
Every figure in that last column counts rows in the vendored litellm@2026-07-14#179affb price
file. They describe a price table rather than what any door can reach.
The first door is the one you get with a key from a web console and no cloud project. The other
three all need a Google Cloud project, a location and a service-account key file. TokenTriage puts
13 of its 95 built-in provider descriptors on the gcp_oauth scheme, which makes it the second
largest scheme in use, including the vertex_ai_embedding descriptor, whose endpoint is embeddings rather than chat.
Each of the 13 descriptors on this page declares one endpoint, chat, and the generated matrix gates
all 13 of those cells at its strongest evidence level, a
row-stamp. The matrix names the gate per cell, so you can run
one: TestGeminiCostGoldenAndUnpriced, TestVertexAIBuiltinDescriptor, TestVertexLanguageModels,
TestVertexAnthropicBuiltinDescriptor, and the nine subtests of TestVertexGardenMaaSFamilies.
That is unusually clean for a family this size; elsewhere in the matrix a cell can read declared (no e2e gate), and 7
descriptors carry no gated cell at all.
Gemini API, the simple door
upstreams:
aistudio:
provider: gemini
base_url: https://generativelanguage.googleapis.com
api_key_env: GEMINI_API_KEY
tiers:
- name: cheap
upstream: aistudio
model: gemini-2.0-flash
The key goes in the x-goog-api-key header, which is descriptor data, and the header scheme is what
makes gemini the one Google descriptor not on gcp_oauth. Streaming uses a distinct path with
alt=sse appended to the query, also descriptor data, and appended only when the request streams.
Gemini on Vertex
Vertex speaks the same wire and authenticates completely differently: a self-signed service-account JWT, minted from a key file whose path is in an environment variable.
upstreams:
vertex:
provider: vertex_ai_language
base_url: https://us-central1-aiplatform.googleapis.com
params:
project: my-gcp-project
location: us-central1
auth:
credentials_file_env: GOOGLE_APPLICATION_CREDENTIALS
The URL the descriptor builds is
/v1/projects/{project}/locations/{location}/publishers/google/models/{model}:generateContent,
with :streamGenerateContent for streams. project and location are the two variables you
supply; model comes from the tier. There is no region: field here: Vertex rates in the vendored
catalog are not region-qualified, so the location picks the host and nothing else.
Resolved against the committed snapshot, that is the difference:
PREFIX MODEL RESULT KEY / REASON
vertex_ai-language-models gemini-2.0-flash PRICED …/gemini-2.0-flash in=$0.10/M out=$0.40/M
vertex_ai gemini-2.0-flash UNPRICED model_unknown
vertex_ai-language-models gemini-3-pro-preview PRICED …/gemini-3-pro-preview in=$2.00/M out=$12.00/M
vertex_ai gemini-3-pro-preview PRICED …/gemini-3-pro-preview in=$2.00/M out=$12.00/M
That table is drawn here rather than captured from a program: each line is one lookup in the price
file compiled into the binary, and tokentriage prices show vertex_ai-language-models/gemini-2.0-flash
prints the first of them with its snapshot provenance attached. Where both prefixes price a model
they agree on the rate. Where only one does, it is vertex_ai_language.
The 13 keys under vertex_ai/ are not 13 Gemini generations, which is part of why the bucket
surprises people: 5 are Gemini 3.x rows, 4 are xAI Grok on Vertex, and the remaining 4 carry speech,
transcription or embedding rates that a chat request will not reach at all.
Claude on Vertex
Anthropic models served by Vertex use a different path verb and a different price namespace, and
they get their own wire driver for it: the Anthropic Messages body carried on Vertex’s
:rawPredict and :streamRawPredict paths.
upstreams:
vertex-claude:
provider: vertex_ai_anthropic
base_url: https://us-central1-aiplatform.googleapis.com
params:
project: my-gcp-project
location: us-central1
auth:
credentials_file_env: GOOGLE_APPLICATION_CREDENTIALS
tiers:
- name: cheap
upstream: vertex-claude
model: claude-3-5-haiku@20241022
Model ids here carry the Vertex @version suffix rather than Anthropic’s own naming, and prices
resolve under vertex_ai-anthropic_models/, which holds 35 keys. Buying the same model direct from
Anthropic is a different upstream and a different namespace.
Model Garden open models
Google serves open-weight families on Vertex behind a single OpenAI-compatible endpoint. Each family gets its own descriptor so that each prices under its own Model Garden namespace.
| Descriptor | Family | Prices under | Keys |
|---|---|---|---|
vertex_ai_mistral |
Mistral and Codestral | vertex_ai-mistral_models/ |
19 |
vertex_ai_llama |
Meta Llama | vertex_ai-llama_models/ |
11 |
vertex_ai_ai21 |
AI21 Jamba | vertex_ai-ai21_models/ |
5 |
vertex_ai_qwen |
Qwen | vertex_ai-qwen_models/ |
4 |
vertex_ai_deepseek |
DeepSeek | vertex_ai-deepseek_models/ |
3 |
vertex_ai_openai |
gpt-oss and Gemma | vertex_ai-openai_models/ |
3 |
vertex_ai_zai |
Z.ai GLM | vertex_ai-zai_models/ |
2 |
vertex_ai_minimax |
MiniMax | vertex_ai-minimax_models/ |
1 |
vertex_ai_moonshot |
Moonshot Kimi | vertex_ai-moonshot_models/ |
1 |
They share a config shape: the same two params:, the same credentials file, and an OpenAI-shaped
request to /v1/projects/{project}/locations/{location}/endpoints/openapi/chat/completions.
upstreams:
garden-llama:
provider: vertex_ai_llama
base_url: https://us-central1-aiplatform.googleapis.com
params: { project: my-gcp-project, location: us-central1 }
auth: { credentials_file_env: GOOGLE_APPLICATION_CREDENTIALS }
tiers:
- name: cheap
upstream: garden-llama
model: meta/llama-3.1-405b-instruct-maas
9 descriptors rather than 1 exist so that a Llama on Vertex is not billed at a Qwen rate. Most of
these model ids carry Google’s -maas suffix and the catalog keys them that way; AI21’s Jamba rows
are the exception, and they carry an @001 style suffix instead.
Reaching Gemini from an SDK you already have
On the translation axis, two client formats come in and four upstream dialects go out. An
OpenAI-format or an Anthropic-format client reaches a gemini upstream, streaming and not, and both
of the Vertex Gemini descriptors ride that same gemini driver. A Gemini-format client reaches only
a Gemini upstream, because gemini is a translation target rather than an inbound client format.
The asymmetry is a decision, and its reason is evidence rather than difficulty: no population of Gemini-SDK clients pointed at a third-party gateway has been observed, and switching on a direction nobody has verified would leave an unowned surface behind rather than a feature. It behaves like a decision too. A Gemini-format request is not rejected: the translator returns a named error, and the request is then served byte-faithfully on its own dialect, a Gemini client reaching a Gemini upstream, which loses nothing at all.
The full grid is drawn once, on the dialect matrix, rather than redrawn here. What a translated row records about fields that could not cross is on translation loss, and the streaming half of it is on streaming translation.
Prefixes that look missing and are not
Three vertex_ai-* prefixes in the price file sit outside the covered list, and none of the three
is a driver gap:
| Prefix | Keys | Why it is absent |
|---|---|---|
vertex_ai-embedding-models |
10 | Vertex :predict embeddings, a different wire from generateContent |
vertex_ai-image-models |
8 | Vertex :predict and Imagen, a different wire |
vertex_ai-text-models |
2 | Vertex text models; the parent vertex_ai prefix is covered |
All three belong to the published gap partition’s alias-or-sub-namespace class, whose own column
heading in the generated table reads “Why it’s not a driver gap”. The third one is pure data: a
pricing-bucket descriptor on the same pattern vertex_ai_language already follows would bind it,
with no new wire code. The first two are not, and the generated table says so: :predict is a
different request and response shape from generateContent, so binding either would be a wire, not
a bucket. The coverage page carries the whole partition.
Checking a whole Google config at once
All of these validate together, and the summary lists them:
$ tokentriage validate --config tokentriage.yaml
mode: ledger
listen: 127.0.0.1:8787 admin: 127.0.0.1:9090
upstreams: 4
- aistudio https://generativelanguage.googleapis.com
- garden-llama https://us-central1-aiplatform.googleapis.com
- vertex https://us-central1-aiplatform.googleapis.com
- vertex-claude https://us-central1-aiplatform.googleapis.com
tiers: 2 (cheapest -> most capable)
0. cheap -> aistudio/gemini-2.0-flash
1. frontier -> vertex/gemini-3-pro-preview
rules: 0
OK: configuration is valid
The four upstream names, their hosts and the two tiers are yours; the mode, listen and admin
lines are the shipped defaults. Upstreams print in sorted order, not in the order you wrote them,
and the column between the name and the host is left blank on every row: it prints the dialect:
key from your config, and configuring an upstream the recommended way with provider: leaves that
key unset. The alignment on the longest name slips by one space for the same reason.
Where the service-account key goes, and where it does not
GOOGLE_APPLICATION_CREDENTIALS holds a filesystem path. The file at that path is read once, away
from the request path, and the JWT is minted from it in a background refresher. Neither the path
nor the key material appears in a response, a log line or the admin status payload; what surfaces
is auth_scheme: gcp_oauth and a health state. The primary flow signs its own JWT and makes no
token-exchange call, so a Vertex upstream adds no network hop to authenticate.
What can go wrong
| Symptom | Cause | Fix |
|---|---|---|
requires template variable "project" (missing) |
no params: block |
both project and location are required |
the "gcp_oauth" scheme requires credentials_file_env (Vertex SA) or api_key_env (AI-Studio) |
no credential | add the key file, or use api_key_env for AI Studio |
private_key_file_env: not valid for the "gcp_oauth" scheme |
a PEM path instead of the JSON | the service-account PEM arrives inside the credentials JSON |
status shows unreadable_credentials |
the path is wrong or the JSON is malformed | check what the variable resolves to |
model_unknown on a Gemini model |
vertex_ai where vertex_ai_language was needed |
switch the descriptor |
model_unknown on a Claude model |
Anthropic’s model id instead of Vertex’s | use the @version form |
| 404 from Vertex | the location does not host that model | params.location must match where it is served |