npm ci # installs pi as a peer — tsc and the tests need its types
npm run ci # lint + test; must pass before any commitPoint your local pi at a checkout while developing:
ln -s "$PWD" ~/.pi/agent/extensions/pi-xai-vertexSymlink the directory, not the file: pi resolves an extension's imports from the path it loaded
it by, so a lone .ts linked into extensions/ cannot see this repo's node_modules/.
pi documents three extension styles — a single .ts file, a directory with index.ts, and, for
extensions with npm dependencies, a package with its entry point under src/. This is the third,
because it depends on google-auth-library.
├── package.json # "pi": { "extensions": ["./src/index.ts"] } — how pi finds the entry point
├── src/
│ ├── index.ts # the whole extension
│ └── index.test.ts # node --test, co-located
└── .github/workflows/ci.yml
src/index.ts keeps all logic in pure exports — resolveProject, buildBaseUrl, authResultFrom,
GROK_4_6_COST, xaiVertexProviderConfig — with a thin default that registers them. The suite
therefore runs with no pi, no network, and no GCP credentials.
Two tests are worth more than the rest, because they check the config against pi's expectations
rather than ours: one runs pi's real createProvider, the other its real calculateCost (including
a 250K-token request that must bill request-wide). If they fail after a pi bump, the schema moved —
read the .d.ts rather than loosening the test.
Vertex serves Grok over the OpenAI-completions protocol at
/v1/projects/<project>/locations/global/endpoints/openapi, authenticated with a short-lived (~1h)
Google OAuth2 access token rather than a static key. So the provider is thin: api is pi's own
openAICompletionsApi(), and ambient auth.apiKey mints through google-auth-library. pi does
streaming, tools, usage, and credential caching.
It mirrors no pi internals, so it cannot drift against a pi release and needs no sync tooling. If a change starts requiring a copy of pi's source, stop and reconsider.
Each of these cost real debugging time. They are not obvious from pi's docs.
Import only from pi's allowlisted specifiers. The extension loader maps a fixed list to bundled
virtual modules: @earendil-works/pi-ai (aliased to compat), .../compat, .../oauth,
.../providers/all, and @earendil-works/pi-coding-agent. Anything else — including a real
subpath like .../api/openai-completions.lazy — falls through to filesystem resolution, where the
loader appends it to the alias file path and dies with Cannot find module .../dist/compat.js/api/openai-completions.lazy. This can appear to work on one machine and fail on
another. openAICompletionsApi is not on the package root's types, so it comes from /compat.
A failed extension is silent. pi omits it from --list-models and prints nothing. Load it with
-e <path> to see the actual error.
The prose docs are stale; trust the .d.ts. docs/custom-provider.md describes a top-level
oauth with refreshToken/getApiKey. The shipped types want auth nested under auth, and the
two shapes mean very different things — see below.
Use auth.apiKey with no login, never auth.oauth. This is the subtlest trap here.
auth.oauth means "an interactive login mints a credential pi persists to ~/.pi/agent/auth.json",
so pi refuses the provider until such a credential exists. ADC is not interactive — it is ambient,
discovered from the environment — and pi's ApiKeyAuth documents an absent login as exactly that
("Absent = ambient-only"), calling resolve() per request instead. Getting this wrong is nearly
invisible: on a machine that once ran pi login it works perfectly, and on every fresh one it fails
with No API key found for xai-vertex. It shipped in v0.1.0 and was caught only by running inside a
clean CI sandbox. To test it, remove the provider's entry from ~/.pi/agent/auth.json and run
pi -ne -e . --model xai-vertex/xai/grok-4.6 "hi".
One malformed entry breaks every provider. resolveCliModel iterates all registered providers'
models before picking one, so a bad model here takes down anthropic-vertex, google, everything.
After any provider or model change, run pi --list-models and confirm the others still resolve.
This is also why Model, ModelCost and AuthResult are imported from pi rather than
re-declared, and why there are no casts: a hand-rolled equivalent is subtly non-assignable
(pi's carries an index signature), and as never would silence exactly the check that catches a
schema break before production.
Model ids are the wire value. There is no separate wire-name field — Model.id is both the
registry key and what pi sends as model. Vertex needs the publisher-qualified xai/grok-4.6, so
the id keeps its slash, and the fully-qualified spec is xai-vertex/xai/grok-4.6. Registering it as
plain grok-4.6 does not error; it returns an empty completion.
location is fixed to global. Regional endpoints answer FAILED_PRECONDITION. It is a
property of the model, not a preference, so it is deliberately not configurable — vercel/ai and
TanStack/ai default to global for the same reason.
Peer deps are inert at runtime. pi loads @earendil-works/* from bundled virtual modules, so a
copy inside the extension's node_modules/ is never used — verified by deleting it from an installed
copy. Deployments still use --omit=peer, but for install size and supply-chain surface, not
correctness. (pi install is itself inconsistent about installing the peer tree across machines.)
GROK_4_6_COST is what pi reports spend with. Update it only from
docs.x.ai/docs/models — never a blog post, comparison table, or
memory — and update src/index.test.ts in the same commit. The long-context tiers entry is
request-wide by design; do not convert it to per-token splitting.
npm run cipasses.- Changes to the provider shape, endpoint, or auth also need one real call against a project with Grok enabled — the suite deliberately does not cover the network.
- Claims about Vertex, xAI, or pi behaviour need a source: the shipped
.d.ts, a live call, or vendor docs. - Commit format
{feat,fix,docs}: <concise message>, no emojis, and sign off:Signed-off-by: <name> <email>.
v0.1.0 declared auth.oauth, so a machine that ran pi login for this provider has a
{ "type": "oauth" } entry for it in ~/.pi/agent/auth.json. pi resolves auth by stored
credential type first, so that entry makes pi skip the ambient path entirely and the provider
disappears — on a machine that previously worked.
Remove the xai-vertex entry from that file (pi logout xai-vertex, or edit it out). The extension
detects the leftover entry at load and prints exactly this instruction, so the failure is
self-explaining rather than looking like the bug v0.1.1 fixed.
Tag and push; .github/workflows/release.yml does the rest.
git tag -a v0.2.0 -m "v0.2.0"
git push origin v0.2.0It re-runs lint and tests (never cut a release from a tree that does not pass), computes the SHA256 of the tag tarball, and publishes a release whose notes carry the tarball URL and digest — the pair pinned consumers need. fullsend's sandbox Containerfile fetches that tarball and verifies the digest, so it is published rather than left for each consumer to derive.
There is no npm publish step: the package is consumed with pi install git:..., so the git tag is
the artifact. Adding one is a few lines plus an NPM_TOKEN secret if that ever changes.
Not drop-in for pi, but the same idea, and worth reading before changing the auth path:
vercel/ai packages/google-vertex/src/xai
and TanStack/ai packages/ai-grok/src/vertex.
For Claude on Vertex under pi, see
twoGiants/pi-anthropic-vertex — a sibling, and a
different protocol bridge.