vertex-doctor
Probe, diagnose and unblock model access on Google Cloud Agent Platform (formerly Vertex AI): which publisher models a project and credentia
Open source Open in the app JSON README (API)
About
Probe, diagnose and unblock model access on Google Cloud Agent Platform (formerly Vertex AI): which publisher models a project and credential can call, in which location, and the one command that fixes each failing cell. Includes quota planning and a guided doctor loop. Prototype / personal hobby project.
Details
- Kind
- Plugins
- Topic
- Cloud & DevOps
- Publisher
- zeroasterisk
- Origin
- gemini
- Category
- ferramentas
- Version
- 0.1.0
- Last push
- 2026-09-08T14:15:38Z
- Repository state
- ativo
- Language
- Python
- License
- Apache-2.0
- Added
- 2026-09-12 17:04:37
- Updated
- 2026-09-12 17:04:37
- Origin id
zeroasterisk/vertex-doctor
README
# vertex-doctor
> **Prototype. Personal hobby project. Not a Google product, not supported, not affiliated with
> anything official.** It makes real API calls against your project (tiny ones, with cost gates)
> and can mutate project settings only when you pass `--yes`. Read the output before you trust it;
> file issues with the JSON records when it is wrong.
`vertex-doctor` answers one question about Google Cloud **Agent Platform** (the product formerly
branded Vertex AI; the API host is still `aiplatform.googleapis.com`):
> Which publisher models can *this credential + this project* call, in *which location*, through
> *which invocation surface*, and for every cell that fails, what is the one action that unblocks it?
Model access is the intersection of seven independent gates (credential binding, API enablement
and quota project, IAM, org policy, per-publisher agreements, per-location per-model quota, model
lifecycle), each failing with its own status code and message shape. The console, `gcloud ai
model-garden` and gateway health checks do not compose them. This tool probes a matrix of
`projects x credentials x models x locations`, classifies every result into a fixed taxonomy, and
prints the fix once per cause.
## Start here: one model, one location
```sh
vertex-doctor journey -m claude-fable-5-1@default -l global --rpm 400 # show the path
vertex-doctor journey -m claude-fable-5-1@default -l global --rpm 400 --fix # walk it (asks before each change)
```
The journey is the same ten steps every time, in the order the gates are actually hit. Each step
is `[ok]`, `[next]` (the one thing to do now, and who does it), `[later]`, or `[you]` (a human
step the tool will not take: linking billing, a quota request the automated adjuster refuses).
<p align="center">
<img src="docs/gifs/journey.gif" alt="vertex-doctor journey: ten ordered steps from credential to gateway config, with the one human step called out and the quota request form ready to paste" width="900">
<br><sub>A brand-new project, a brand-new Claude lineage: nine steps handled, one left for a person, form ready to paste.</sub>
</p>
<details>
<summary>The happy path (all ten green, then the curl and SDK call that work)</summary>
<p align="center"><img src="docs/gifs/journey-ok.gif" alt="vertex-doctor journey for gemini-flash-latest: all ten steps green and the LiteLLM entry printed" width="900"></p>
</details>
| # | step | who fixes it |
|---|------|--------------|
| 1 | credential resolves and is not certificate-bound | you (`gcloud auth application-default login`) |
| 2 | billing account linked | you, in the console (the tool prints the two links) |
| 3 | APIs enabled | the tool |
| 4 | caller holds `aiplatform.endpoints.predict` | a project admin (the tool prints the command) |
| 5 | model exists in the catalog and is spelled the way the REST path wants | the tool (re-probes the corrected id) |
| 6 | model is served at this location | the tool (tells you which location works) |
| 7 | project is entitled to the publisher (data sharing / EULA / enablement) | the tool, after you confirm |
| 8 | quota bundle exists and is above zero | the tool files it; if the adjuster denies, you send the printed form to your account team |
| 9 | a real request returns 200 | the tool |
| 10 | ready to use | the tool prints the curl that worked and the official SDK call; any other client on request (see below) |
`--rpm N` is the only number you need: token quotas are sized from what sibling models already
have in the project. Without `--rpm` the tool recommends a value the same way.
**What can surprise you, and what the tool does about it**
- Changes take a minute or two to propagate (an enabled API, the data-sharing flag, a quota
grant). After every change the tool re-probes until the answer changes instead of reporting the
stale one.
- A brand-new project fails on five things before the first real gate (Resource Manager, Billing,
Agent Platform, Cloud Quotas and procurement APIs, then billing itself). The tool enables the
APIs; billing is yours.
- The catalog is not access: models listed there can be uncallable, and callable models can be
missing from it. Only the real request counts, so every step 5 to 9 is proven by one.
- `claude-x@default` is only valid when the catalog `versionId` is `default`; older models want
the dated version. The tool re-probes the right spelling.
- Newer Claude lineages are served at `global` (and `us`/`eu`), not in most regions, and their
quota bucket is the lineage (`anthropic-claude-fable`), not the model id. The tool re-targets the
request and names the bucket.
- New projects have zero quota for partner models. The automated adjuster grants some requests in
a minute and denies others just as fast without saying why, and it phrases a refusal at least two
ways ("Quota request denied" / "We cannot grant ... '0' was granted"), so the tool decides on
the granted numbers, never the sentence. When it denies, re-filing does not help; the tool says
so and hands you the form instead of looping.
<details>
<summary>Probing a whole matrix instead (models x locations)</summary>
<p align="center">
<img src="docs/gifs/probe.gif" alt="vertex-doctor probe: a matrix of models by location, each failure classified, one fix per cause" width="900">
</p>
```
$ vertex-doctor probe -m claude-sonnet-4-6@default,claude-sonnet-4-5@default,gemini-flash-latest -l global,us-east5
my-gcp-project anthropic [adc]
model global us-east5
claude-sonnet-4-6@default ok 2292ms ok 1345ms
claude-sonnet-4-5@default x A17 404 x A17 404
my-gcp-project google [adc]
model global us-east5
gemini-flash-latest ok 523ms x A16 404
What to do
1. [A17] WRONG_SURFACE_OR_SPELLING (high) 2 cell(s)
version suffix '@default' is not valid for this model; the catalog versionId is '20250929'
run: vertex-doctor probe --project my-gcp-project --model anthropic/claude-sonnet-4-5@20250929 --location global
2. [A16] MODEL_NOT_SERVED_IN_LOCATION (high) 1 cell(s)
google/gemini-flash-latest is not served in us-east5 but works at global
run: vertex-doctor probe --project my-gcp-project --model google/gemini-flash-latest --location global
```
</details>
<p align="center"><b><a href="https://zeroasterisk.github.io/vertex-doctor/">Watch the full walkthrough</a></b> (asciinema, with chapters), recorded on a project created minutes earlier.</p>
## Install
Python 3.11+. No published package yet; run from the repo or from git with `uv`:
```sh
uvx --from git+https://github.com/zeroasterisk/vertex-doctor vertex-doctor --help
# or
uv tool install git+https://github.com/zeroasterisk/vertex-doctor
# or, from a checkout
uv sync && uv run vertex-doctor
```
## Quick start
```sh
vertex-doctor # interactive wizard on a TTY (help when piped); the journey is the first option
vertex-doctor journey -m MODEL -l global --rpm 60 [--fix] # one model: the ordered path to callable
vertex-doctor doctor # many models x locations: env -> probe -> diagnose -> plan fixes (dry-run) -> report
vertex-doctor doctor --fix # also re-probe corrected cells; mutations ask on a TTY (or need --yes)
vertex-doctor env # zero-cost environment check: gcloud/CBA, ADC, APIs, IAM
vertex-doctor discover -P anthropic # catalog with the exact spelling for each surface
vertex-doctor probe -m anthropic,google -l default --save probe.jsonl
vertex-doctor quota-plan -m claude-fable-5-1@default -l us-east5 --rpm 100 --input-tpm 40000000 --output-tpm 1000000
vertex-doctor client-config --from probe.jsonl # curl + how to call it; --format list for SDKs, harnesses, gateways, frameworks
```
### The doctor loop
`doctor` is the "walk through everything" command. It runs the environment check, fixes blockers
it is allowed to fix (API enablement, ADC quota project), probes the matrix, turns every failing
cell into a **Fix** of one of four kinds, applies the allowed ones, re-probes the affected cells and
prints before -> after:
| kind | examples | needs |
|------|----------|-------|
| `mutate` | enable an API, set Anthropic data sharing, accept a EULA + `enableModel` | `--fix` plus `--yes` or a TTY confirmation |
| `shell` | `gcloud auth application-default set-quota-project` | same |
| `reprobe` | try `global` when a region 404s/400s, re-send with the corrected `@version`, re-send with ADC after a CBA 401 | `--fix` (costs one tiny probe per cell) |
| `advice` | IAM grants, org policy, retired models, quota requests (with the exact `quota-plan` command) | a human |
Without `--fix` every mutation is dry-run and printed with its HTTP request and `gcloud`
equivalent, so an agent can show the plan and a human can approve it.
<p align="center"><img src="docs/gifs/doctor.gif" alt="vertex-doctor doctor --fix: a wrong @version suffix and a wrong region are re-probed and fixed; an enablement is left as a dry-run" width="900"></p>
Project, credential and locations are all configurable; the wizard sniffs what the machine already
has (env vars, gcloud config, the ADC file, client configs and previous runs in the current
directory) and leads with a concrete recommendation such as "use my-gcp-project (gcloud config)"
or "global + us-east5 ($CLOUDSDK_COMPUTE_REGION)". The flags use the same detection:
| Thing | Flag | Default |
|------------|------------------|-------------------------------------------------------------------------|
| project | `-p/--project` | `$VERTEX_DOCTOR_PROJECT`, `$GOOGLE_CLOUD_PROJECT`, ADC quota project, `gcloud config` project |
| credential | `-c/--credential`| `auto` = `$VERTEX_DOCTOR_ACCESS_TOKEN`, then ADC, then the gcloud user token (warned) |
| location | `-l/--location` | `default` = `global` plus a small curated region list per publisher |
| models | `-m/--model` | required; any spelling, a bare publisher name (newest catalog models), or `@file` |
Credential specs: `auto`, `adc`, `gcloud`, `sa:/path/key.json`, `impersonate:SA_EMAIL`, `token:RAW`.
`impersonate:` is the one to use when the question is "what can the *gateway's* service account
call?" from a developer laptop.
### Cost gates
Probing sends one tiny fixed prompt ("Reply with the single word: pong", `max_tokens` 512,
thinking disabled where the surface allows it) per cell. Three gates:
- `--no-cost`: only free calls (`countTokens`, catalog, config reads). Proves auth, API
enablement and IAM. Gemini `countTokens` also validates model + location; the Anthropic
`count-tokens` endpoint accepts any model name, so it proves less. No free surface exists for the
OpenAI-compatible MaaS endpoint; those cells are skipped.
- `--low-cost` (default): real predictions on non-premium models. Premium patterns (`opus`,
image/video/audio/TTS/OCR) are skipped.
- `--any-cost`: everything.
Plus `--max-cells` (200), `--concurrency` (4), `--max-total-output-tokens` (20k) and `--dry-run`,
which prints every request with the token redacted and sends nothing.
### Output for agents
`--json` / `--jsonl` emit one schema-versioned record per cell (see `src/vertex_doctor/schema.py`).
Diagnostics go to stderr. Color is off when stdout is not a TTY or `NO_COLOR` is set. Exit codes:
| code | meaning |
|------|---------|
| 0 | every probed cell is callable |
| 1 | at least one cell failed with a known diagnosis |
| 2 | nothing could be probed (auth/config/plan error) |
| 3 | at least one cell is `UNKNOWN` (please file the record) |
## Commands
| command | what it does | mutates? |
|---------|--------------|----------|
| `env` | gcloud version and CBA config, ADC file and quota project, principal per credential, live zero-cost differential (`countTokens`), Service Usage state for aiplatform / cloudquotas / procurement, `testIamPermissions`, Anthropic publisher config | no |
| `discover` | Model Garden catalog (`publishers.models.list`, `v1beta1`) with `versionId`, `launchStage` and the canonical spelling for each surface | no |
| `probe` | the matrix probe, corroborating reads, classification, a client-neutral `client` block per callable cell | sends predictions |
| `diagnose --from` | re-classify saved records offline (no network) | no |
| `client-config --from` | how to use the callable cells from a client: `curl` (default), official SDKs, agent harnesses, gateways, app frameworks; `--format list` | no |
| `diff --config` | read a client's config file (`--format litellm` or `bifrost`), probe exactly those cells, bucket them: dead, misplaced, unenabled, truncating | sends predictions |
| `watch --baseline` | re-probe a saved matrix and exit 1 on regressions (`--interval` to loop, `--update` to roll the baseline) | sends predictions |
| `enable-api` | Service Usage `batchEnable` for aiplatform / cloudquotas / procurement | with `--yes` |
| `enable-publisher` | Anthropic: `setPublisherModelConfig(dataSharingEnabledProvider=ANTHROPIC)`; others: `modelGardenEula:check`/`:accept` then `enableModel` | with `--yes` |
| `journey` | one model, one location: the ten ordered gates with status, the next action and who does it; `--fix` walks them (propagation-aware re-probes; billing and denied quota are human steps) | with `--fix` (mutations ask, or `--yes`) |
| `doctor` | many models x locations: env -> probe -> fix plan -> apply (`--fix`, `--yes`) -> re-probe -> before/after | with `--fix --yes` |
| `quota-plan` | the quota **bundle** a model runs on (QPM, input TPM, output TPM, or combined TPM) at this endpoint and comparable ones, the `base_model` bucket to name, what to ask for, a filled request form | no |
| `request-quota` | file one `QuotaPreference` per ask from the plan (`--rpm/--input-tpm/--output-tpm/--tpm`) | with `--yes` |
| `interactive` | the wizard (also what bare `vertex-doctor` runs on a TTY) | asks |
Every mutation has an idempotent pre-check, `--dry-run` that prints the exact HTTP request, a
`gcloud`/`curl` equivalent, and the IAM it needs.
## Diagnosis taxonomy
Matching is on HTTP status + gRPC status + `google.rpc.ErrorInfo.reason` + message fragment +
corroborating reads (catalog `get`, `fetchPublisherModelConfig`, `modelGardenEula:check`,
`QuotaInfo`, sibling cells in the same run). Precedence: auth > API/quota project > IAM/org
policy > publisher agreement > location > quota > existence > response shape.
| rule | code | trigger | primary remediation |
|------|------|---------|---------------------|
| A1 | `AUTH_CBA_BOUND_TOKEN` | 401 from a `gcloud` token with `reason=ACCESS_TOKEN_TYPE_UNSUPPORTED`, or ADC succeeds on the same request, or `context_aware/use_client_certificate=true` | use ADC / mTLS host / a service account |
| A2 | `AUTH_INVALID_OR_EXPIRED` | other 401 | `gcloud auth application-default login` |
| A3 | `QUOTA_PROJECT_MISSING_OR_DISABLED` | 403 `SERVICE_DISABLED` naming a consumer that is not the target project, or "requires a quota project" | `set-quota-project`, `x-goog-user-project` |
| A3b | `QUOTA_PROJECT_USE_DENIED` | 403 `USER_PROJECT_DENIED`: the caller can see the project but lacks `serviceusage.services.use` on it | grant `roles/serviceusage.serviceUsageConsumer` (Editor/Owner include it) |
| A4 | `API_DISABLED` | 403 `SERVICE_DISABLED` naming the target project | `enable-api` |
| A4b | `BILLING_DISABLED` | 403 `BILLING_DISABLED`: no billing account linked (every brand-new project) | `gcloud billing projects link P --billing-account=ID`; the doctor only does this with an explicit `--billing-account` |
| A5 | `IAM_PREDICT_DENIED` | 403 naming `aiplatform.endpoints.predict` | grant `roles/aiplatform.user` |
| A6 | `PUBLISHER_DATA_SHARING_REQUIRED` | 403 "requires data sharing to be enabled for publisher" | `enable-publisher --publisher anthropic` |
| A7 | `EULA_NOT_ACCEPTED` | 403/400 mentioning EULA / license / terms | `modelGardenEula:accept` |
| A8 | `MODEL_NOT_ENABLED_FOR_PROJECT` | 403 mentioning enablement / questionnaire / private offer / procurement | `enableModel`, then console |
| A9 | `ORG_POLICY_BLOCKED` | 403 mentioning organization policy / `vertexai.allowedModels` / procurement API | org admin |
| A10 | `PERMISSION_DENIED_UNCLASSIFIED` | any other 403 | `env`; file as a new rule |
| A11 | `REGIONAL_QUOTA_ZERO` | 429 in a region where `QuotaInfo` has no row or 0 for `(region, base_model)`, or the same model is OK at `global` | use `global` |
| A12 | `QUOTA_EXHAUSTED` | 429 where `QuotaInfo` shows a positive limit | `quota-plan`, then `request-quota`, or lower concurrency |
| A12b | `QUOTA_ZERO_FOR_PROJECT` | 429 at `global` where the project's row is unset/0 (every brand-new project, for partner models) | `quota-plan` for the bundle, then `request-quota` or the form |
| A13 | (annotation `RATE_LIMITED_TRANSIENT`) | 429 then success on the backoff retry | none |
| A14 | `GLOBAL_ONLY_MODEL` | 400 "only available via global endpoint" | use `global` |
| A15 | `MODEL_RETIRED_OR_UNKNOWN` | 404 and the catalog has no such model (only for publishers the catalog lists) | `discover` |
| A16 | `MODEL_NOT_SERVED_IN_LOCATION` | 404 / 400 "not servable in region" / "not available in region" with success elsewhere | move location |
| A17 | `WRONG_SURFACE_OR_SPELLING` | 404 with a surface that does not match the publisher; 404 where the `@version` suffix does not match the catalog `versionId`; 400 "cannot be called directly via" | use the printed canonical spelling |
| A18 | `NOT_FOUND_POSSIBLY_ENTITLEMENT` | 404 where the catalog knows the model and no location works | `enable-publisher --dry-run`, try `global` |
| A19 | `REQUEST_SHAPE_INVALID` | 400 `INVALID_ARGUMENT` about the body | tool bug or wrong `--surface`; file it |
| A20 | `OK_TRUNCATED_BY_REASONING` | 200 with `finish_reason` length/MAX_TOKENS, no text, reasoning tokens spent | access confirmed; raise budgets, set `health_check_max_tokens` |
| A21 | `OK` | 200 with content | ready to use; `client-config` prints how |
| A21f | `OK_FREE_PROBE` | `--no-cost` success | run a real probe to confirm |
| A22 | `OK_PREVIEW_OR_UNSTABLE` | 200 but catalog says EXPERIMENTAL / PRIVATE_PREVIEW / UNSTABLE | pin before production |
| A23 | `UPSTREAM_ERROR` | 5xx / transport error after one retry | retry later |
| A24 | `UNKNOWN` | anything else | exit 3; file the record |
Annotations that ride along independently: `CATALOG_FLAG_MISMATCH` (callable but not in the
catalog, or vice versa: listing is not access), `PUBLIC_PREVIEW`, `TRUNCATED_AT_DEFAULT_BUDGET`,
`RATE_LIMITED_TRANSIENT`.
## Model ID spellings
All of these normalize to the same `(publisher, model_id)`; `discover` prints the right one for
each surface:
| where | spelling |
|-------|----------|
| Model Garden catalog | `anthropic/claude-sonnet-4-6` (no version; `versionId` is a field) |
| REST path (Anthropic, Gemini, embeddings, Mistral) | `claude-sonnet-4-6@default`, `claude-sonnet-4-5@20250929`, `gemini-flash-latest` |
| OpenAI-compatible endpoint body (`xai`, `openai`, `meta`, `deepseek-ai`, `qwen`) | `xai/grok-4.1-fast-non-reasoning` |
| third-party clients | each has its own spelling; `client-config --format <name>` prints it (LiteLLM: `vertex_ai/claude-sonnet-4-6@default`, `vertex_ai/xai/grok-4.1-fast-non-reasoning`; Anthropic SDK: `claude-sonnet-4-6`; Bifrost: `vertex/claude-sonnet-4-6@default`) |
Surfaces: Anthropic `rawPredict` (`anthropic_version: vertex-2023-10-16`), Gemini
`generateContent` (`thinkingBudget: 0`, dropped on 400), embeddings `predict`, Mistral
`rawPredict` with a Mistral-native body, and `endpoints/openapi/chat/completions` for the rest.
Host is `aiplatform.googleapis.com` for `global`, `{location}-aiplatform.googleapis.com` otherwise.
## Things the probe taught us (and now checks for)
- **`@default` is not universal.** The Anthropic REST path takes `name@versionId` from the
catalog. `@default` works only for models whose catalog `versionId` is `default`; older ones want
the dated version (`claude-sonnet-4-5@20250929`). The wrong suffix is a 404 that looks exactly
like a retired model; A17 tells them apart from the catalog.
- **CBA tokens have a server-side tell.** A certificate-bound `gcloud auth print-access-token`
token fails `tokeninfo` with `invalid_token` and fails `aiplatform` with 401
`reason=ACCESS_TOKEN_TYPE_UNSUPPORTED`, while ADC works. `env` shows both.
- **Listing is not access, in both directions.** `openai`, `meta`, `deepseek-ai` and `qwen` MaaS
models are absent from the un-projected catalog yet callable; some catalog entries are not
callable by a given project; a retired dated Claude version was still callable while absent from
the catalog. Only a real request is ground truth.
- **429 messages name the quota *metric*; `QuotaInfo` ids are CamelCase.** The message says
`online_prediction_input_tokens_per_minute_per_base_model`; the id you file against is
`OnlinePredictionInputTokensPerMinutePerRegionPerBaseModel`. `quota-plan` resolves one from the
other (and prefers the per-minute quota when a per-day one shares the metric).
- **Newest Claude lineages have no regional quota rows at all.** `QuotaInfo` for this project
lists `anthropic-claude-fable`, `-sonnet`, `-opus` buckets only under the `Global...` and
`Us/Eu...` multi-region quotas; regional rows exist only for older per-version buckets. A regional
429 for those models is "quota is zero here", not "you are burning it". The lineage fallback in
`request-quota` maps `anthropic-claude-fable-5-1` to `anthropic-claude-fable`.
- **Data sharing is per model generation.** In the same project, older Claude models answered
while the newest returned `PUBLISHER_DATA_SHARING_REQUIRED`. `fetchPublisherModelConfig` returns
404 until something is set, so absence of a config does not predict the gate.
- **The OpenAI-compatible endpoint wraps errors in a list** (`[{"error": {...}}]`). Handled.
- **A brand-new project fails five times before the first real gate.** On a project created
minutes earlier the doctor found, in order: `cloudresourcemanager`, `cloudbilling`, `aiplatform`,
`cloudquotas` and the procurement API disabled (it enabled them), then `BILLING_DISABLED` on every
cell. Billing is a money decision, so that one is never taken automatically: the doctor prints the
console links and stops. Once billing was linked, Gemini, embeddings and Grok answered
immediately while Claude returned 429 with an *unset* global quota row: partner models start at
zero and the project has to ask for the bundle (`QUOTA_ZERO_FOR_PROJECT`).
- **Some catalog entries want another API entirely** (`... only supported in the Interactions API
and cannot be called directly via generateContent`). Classified A17, not treated as a tool bug.
## Where will you use the model? (clients)
The core never names a client. Once a cell is callable, `client-config` (or the last question of
the wizard) renders the `client` block through an emitter. `--format list` prints the registry:
| category | emitters | notes |
|---|---|---|
| raw | `curl` (default) | the exact request that worked, with the ADC token |
| official SDKs | `google-genai`, `anthropic-sdk`, `openai-sdk`, `mistral-sdk` | one per surface; what Google and the publishers document |
| agent harnesses | `opencode`, `gemini-cli` | `opencode.json` provider block; env + `settings.json` |
| gateways / proxies | `litellm`, `bifrost`, `envoy-ai-gateway` | `litellm` and `bifrost` also have importers for `diff --config` |
| app frameworks | `langchain`, `adk`, `ai-sdk`, `pydantic-ai` | constructor lines with project/location/credentials |
Each emitter only drives the surfaces its target documents (an SDK for Claude does not pretend to
call Grok); cells it cannot drive are listed on stderr. Every emitter was written against the
target's primary docs; the module docstring cites the URL and marks anything unverified. Adding one
is a ~40-line module in `src/vertex_doctor/emitters/` that registers itself; the selection above is
the top of each category by adoption and documented Vertex partner-model support, not an endorsement.
## Quotas: what to ask for
A real quota request is a bundle for exactly one project, one `base_model` bucket and one endpoint
type, not one number. `quota-plan` reads `QuotaInfo` and answers the four questions people get
wrong:
1. **Which family?** Anthropic / Mistral run on `online_prediction_*` quotas; Gemini and the
OpenAI-compatible MaaS partners (grok, gpt-oss, ...) on `generate_content_*`; embeddings on
`embed_content_*` (regional request quota under `online_prediction`). A model always needs the
**requests/min** quota plus either **input + output tokens/min** (current lineages) or one
**combined tokens/min** (older ones).
2. **Which endpoint type?** Prefix `""` (regional, dimensioned on `region` + `base_model`),
`global_`, `us_multi_region_`, `eu_multi_region_`. Each is a different quota with a different
CamelCase id (`OnlinePredictionInputTokensPerMinutePerRegionPerBaseModel` vs
`GlobalOnlinePredictionInputTokensPerMinutePerBaseModel`).
3. **Which bucket?** The `base_model` dimension is not the model id: `claude-fable-5-1@default`
is metered as `anthropic-claude-fable`, `mistral-small-2503` as `mistralai-mistral-small-2503`,
`gemini-embedding-001` as `gemini-embedding`, `grok-4.1-fast-reasoning` as itself. The planner
walks the lineage (`-5-1` -> `-5` -> family) until QuotaInfo has a row.
4. **Does the row exist where you are asking?** For the newest Claude lineages there are no
regional rows at all; a regional request cannot be granted. The plan re-targets to the endpoint
that has rows (global / us / eu) and says so on the form.
<p align="center"><img src="docs/gifs/quota-plan.gif" alt="vertex-doctor quota-plan re-targets a regional request to global, names the lineage bucket and prints a filled request form" width="900"></p>
That form is the same shape support / account-team request templates use; `request-quota --yes`
files the same three values as `QuotaPreference`s, waits up to 90 s for the automated adjuster,
and reports granted / denied / pending per quota. With no targets, `quota-plan` **recommends**
values: the median of what sibling buckets of the same publisher already have at that endpoint in
this project (a number that was granted before is the most defensible ask), or a starter tier
(60 QPM, 2M input, 200k output tokens/min) when there is nothing to compare with;
`request-quota --recommended` files those. Existing requests for the bucket are listed with their
state, and a denial comes with the three usual reasons (not yet entitled to the model, bucket not
self-serve, ask too large). Other per-base-model quota varieties exist
(`responses_*` for the Responses API, bidi/live concurrency, image/audio/video generation and
per-resolution input quotas, long-running predictions for video); they are listed by `QuotaInfo`
and out of scope for the planner today.
## Seed data
`src/vertex_doctor/ids.py` carries a small table per publisher: surface, default locations, a
fallback model list for publishers the catalog does not list, id style (bare vs publisher-prefixed), quota
`base_model` prefix. It is a starting point the catalog and the probe correct, not a serving
guarantee. `gemini-flash-latest` is a good evergreen alias for Gemini and is global-only.
## For agents
Use `--json`/`--jsonl`, read `diagnosis.code`, `diagnosis.remediation.command` and the exit code.
Never run `enable-*` / `request-quota` without `--dry-run` first. See
[`skills/vertex-doctor/SKILL.md`](skills/vertex-doctor/SKILL.md) and [`AGENTS.md`](AGENTS.md).
The repo is packaged the way [google/skills](https://github.com/google/skills) packages its
`google-cloud-developer` plugin (`plugin.json`, `.claude-plugin/`, `.codex-plugin/`,
`gemini-extension.json`, `skills/<name>/SKILL.md` with a category and a "Use when / Don't use for"
description), and the skill defers to that catalog's `google-cloud-recipe-auth`, `gcloud`,
`agent-platform-inference` and `agent-platform-troubleshooting` skills for what they own:
```sh
npx skills add google/skills # the official companions
claude plugin marketplace add zeroasterisk/vertex-doctor && claude plugin install vertex-doctor@vertex-doctor
```
## Demos and recordings
The GIFs above come from [`demo/*.tape`](demo) (rendered with [vhs](https://github.com/charmbracelet/vhs))
and the walkthrough at [zeroasterisk.github.io/vertex-doctor](https://zeroasterisk.github.io/vertex-doctor/)
from [`demo/record.py`](demo/record.py), which drives the real CLI in a pty, answers its prompts,
redacts identifiers and refuses to export if one survives. Re-record with:
```sh
for t in journey journey-ok quota-plan probe doctor; do VD_DEMO_PROJECT=... vhs demo/$t.tape; done # -> docs/gifs/
uv run python demo/record.py all # -> docs/casts/
```
Both make the same tiny real requests the tool always makes, against `VD_DEMO_PROJECT`, and
replace every `real=replacement` pair listed in `demo/redact.local` (gitignored; see
`demo/redact.local.example`) before anything is written. The recorder refuses to export a cast
in which a real identifier survived.
## Development
```sh
uv sync
uv run pytest # offline: fixtures under tests/fixtures are real, scrubbed responses
uv run ruff check src tests && uv run ruff format src tests
```
Adding a rule: put the scrubbed error body in `tests/fixtures/errors/`, add a branch to
`classify()` in `diagnose.py` in precedence order, add a test in `tests/test_diagnose.py`, add the
row to the table above.
## Not goals
Deploying self-hosted Model Garden models, benchmarking, Provisioned Throughput, replacing the
gateway's own runtime health checks (the emitters configure them instead), being a general
`gcloud` wrapper.
## License
Apache-2.0. Contributions welcome but expectations should be low: this is a hobby prototype.