# 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.

Source: https://tokentriage.com/docs/build/providers-google

---
import Callout from '../../../components/Callout.astro';
import Details from '../../../components/Details.astro';
import NextSteps from '../../../components/NextSteps.astro';

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](/docs/build/providers-reference). 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

```yaml
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.

```yaml
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.

<Callout type="key">
Two descriptors serve Gemini on Vertex, and they bind different slices of the price table. Prefer
`vertex_ai_language`: it prices both the 2.x and 3.x generations. `vertex_ai` binds a narrower
namespace of 13 entries that covers 3.x but not 2.x, so `gemini-2.0-flash` on it resolves
`model_unknown`.
</Callout>

Resolved against the committed snapshot, that is the difference:

```text
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.

```yaml
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`.

```yaml
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](/docs/build/providers-dialect-matrix), rather
than redrawn here. What a translated row records about fields that could not cross is on
[translation loss](/docs/build/providers-translation-loss), and the streaming half of it is on
[streaming translation](/docs/build/providers-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](/docs/build/providers-coverage) carries the whole partition.

## Checking a whole Google config at once

All of these validate together, and the summary lists them:

```console
$ 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.

<Details summary="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.

</Details>

## 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 |

<NextSteps items={[
  {title: "Which pricing key your model lands under", href: "/docs/build/providers-pricing-keys", description: "Why the Vertex namespaces above cannot borrow from each other, and how to read a model_unknown row."},
  {title: "Authentication schemes", href: "/docs/build/providers-auth", description: "What the gcp_oauth refresher does between requests, and the closed set of states it will report about your key file."},
  {title: "Provider reference", href: "/docs/build/providers-reference", description: "The 13 Google descriptors listed beside the other 79, with the wire, scheme and endpoints of each."}
]} />
