Support Claude on Vertex AI (vendor + register vertex-anthropic provider) — Option B1 #27
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Add support for running pi-loop's stage sessions on Claude models served via Google Cloud Vertex AI ("Claude on Vertex"), by vendoring the community
pi-provider-vertex-anthropiclogic into pi-loop and registering it directly on the SDKModelRuntime. This is opt-in via an environment flag; all existing providers (Gemini via the builtingoogle-vertex, direct Anthropic, etc.) are unaffected when the flag is off.This is Option B / Approach B1 from the LLM-provider investigation. See in-repo docs:
docs/plan-B1-vertex-anthropic-provider.md— the detailed implementation plan (source of truth for tasks).docs/scope-option-B-claude-on-vertex.md— scoping + why B3 (config-only) was ruled out.Background / motivation
pi-loop uses its own bundled SDK (
@earendil-works/pi-coding-agent@0.81.1) and callsModelRuntime.create()directly insrc/stage/services/createPiStageSession.ts. It does not load the extension packages listed in~/.pi/agent/settings.json, so the customvertex-anthropicprovider used by interactive pi is not available to pi-loop.Investigation findings (all verified live):
google-vertexprovider works for Gemini (gemini-2.5-proreturns OK) but not Claude: it uses@google/genaigenerateContentStream(the GeminigenerateContentprotocol, hardwired topublishers/google). Claude on Vertex needspublishers/anthropic/...:streamRawPredictwith the Anthropic Messages body.anthropic-messagesprovider wraps the official@anthropic-ai/sdkcalling the direct/v1/messagesAPI withx-api-key; it has no Vertex mode and cannot be config-pointed at the Vertex URL/auth. → B3 (models.json only) is impossible.POST https://aiplatform.googleapis.com/v1/projects/<project>/locations/global/publishers/anthropic/models/claude-opus-4-8:streamRawPredictbody
{"anthropic_version":"vertex-2023-10-16","messages":[...],"max_tokens":N,"stream":true}→ HTTP 200, standard Anthropic Messages SSE.~/.config/gcloud/application_default_credentials.json,authorized_userrefresh-token flow). The vendoredauth.tschain (service-account JSON → ADC →gcloudCLI) resolves a bearer token.location=global+claude-opus-4-8. (us-east5→ 429 quota;us-central1→ 400; pinned@dateids andclaude-sonnet-4-5*→ 404.)ModelRuntime.registerProvider(id, config)accepts aProviderConfigInputthat includes a customstreamSimple— this is exactly why B1 is feasible.createAssistantMessageEventStream,calculateCost) are exported by@earendil-works/pi-ai@0.81.1(currently only present nested under the SDK; B1 adds it as a direct, version-pinned dependency).Goal / acceptance criteria
PILOOP_VERTEX_ANTHROPIC=1andPILOOP_MODEL="vertex-anthropic/claude-opus-4-8", a pi-loop stage session runs against Claude on Vertex and returns a non-empty streamed response using existing GCP/ADC credentials.ModelRuntime.registerProvider(...)increatePiStageSession.ts, gated by the env flag.PILOOP_E2E=1) live smoke test proves streaming + auth end-to-end and asserts only on exit conditions (non-empty text, tool invoked), never exact text.npm run lint,npm test, andnpm run buildall pass.Source to vendor
From
~/.pi/agent/npm/node_modules/pi-provider-vertex-anthropic@0.1.3/src/(MIT). Target dir:src/model/vertexAnthropic/.config.tsstreamRawPredictURLauth.tsgetAccessToken(): service-account JWT → ADC refresh →gcloudCLI; 55-min cacheshell.tsspawn/exechelpers used byauth.tsmodels.tsVERTEX_MODELS,PROVIDER_NAME='vertex-anthropic',API_NAME='vertex-anthropic-api'messages.ts@mariozechner/pi-ai→@earendil-works/pi-aivertex-api.tsstreamVertexAnthropic(customstreamSimple): SSE parse → SDK stream events@mariozechner/pi-ai(types +calculateCost,createAssistantMessageEventStream) →@earendil-works/pi-aipre-register.tsandindex.ts(theExtensionAPIentry) are not vendored — pi-loop registers the provider itself.Tasks
Step 0 — De-risk: streaming-type compatibility spike (do first)
@earendil-works/pi-ai@0.81.1(exact) topackage.json.vertex-api.ts+messages.tsagainst@earendil-works/pi-aitypes (AssistantMessage,Model,Context,SimpleStreamOptions,AssistantMessageEventStream,ThinkingContent,ToolCall) and runtime helpers (createAssistantMessageEventStream,calculateCost).start,text_start/_delta/_end,thinking_*,toolcall_*,done,error) and field names (contentIndex,partial,delta,content,toolCall,reason) match.Step 1 — Dependency
"@earendil-works/pi-ai": "0.81.1"(exact) to dependencies;npm install; confirmnpm run buildpasses.Step 2 — Vendor pure modules
config.ts,auth.ts,shell.ts,models.tsintosrc/model/vertexAnthropic/(no logic changes).console.errorthrough pi-loop's logger if trivial, else leave and confirm no secrets printed).Step 3 — Vendor streaming + messages (import re-point)
messages.tsandvertex-api.ts, changing only imports@mariozechner/pi-ai→@earendil-works/pi-ai; apply Step 0 shape adjustments if any.Step 4 — Registration entry point
src/model/vertexAnthropic/registerVertexAnthropic.tsexportingregisterVertexAnthropic(modelRuntime)that callsmodelRuntime.registerProvider('vertex-anthropic', { name, baseUrl, api: 'vertex-anthropic-api', apiKey: 'vertex-anthropic', models: VERTEX_MODELS, streamSimple: streamVertexAnthropic }). Omit the interactiveoauthblock (unattended).Step 5 — Wire into stage session (env-gated)
createPiStageSession.ts, afterModelRuntime.create(), add:if (process.env.PILOOP_VERTEX_ANTHROPIC === '1') registerVertexAnthropic(modelRuntime);Step 6 — Config surface
PILOOP_VERTEX_ANTHROPIC,PILOOP_MODEL,GOOGLE_CLOUD_PROJECT/ANTHROPIC_VERTEX_PROJECT_ID, region vars (CLOUD_ML_REGION/VERTEX_LOCATION/VERTEXAI_LOCATION; decide whether to also honourGOOGLE_CLOUD_LOCATIONfor consistency with the builtin), andGOOGLE_APPLICATION_CREDENTIALS.config.tsisus-east5; document that this project needsglobal.Step 7 — Unit tests (deterministic seams only)
config.test.ts— project/region precedence,buildEndpointHost(global vs regional),buildStreamUrlshape.auth.test.ts— strategy selection with mockedfs/fetch/child_process(SA JSON, ADC, gcloud fallback, cache hit/expiry, no-creds error); assert no token logged.messages.test.ts—convertMessages(text-only vs image, tool_result batching, cache_control on last user block),convertTools,mapStopReason,normalizeToolCallId,sanitizeSurrogates, orphaned-tool-call synthesis.registerVertexAnthropic.test.ts— fakeModelRuntime; assert oneregisterProvidercall with the expected id/baseUrl/api/models.createPiStageSession— env flag gates registration (flag on → registered, off → not); existing tests stay green.parseSSEunit test with a canned SSE byte stream.Step 8 — Live smoke (gated)
PILOOP_E2E=1e2e mirroringrunStage.e2e.test.ts, run withPILOOP_VERTEX_ANTHROPIC=1 PILOOP_MODEL="vertex-anthropic/claude-opus-4-8" GOOGLE_CLOUD_PROJECT=teg-gemini-ent-prod VERTEX_LOCATION=global. Assert on exit conditions only.Step 9 — Docs
README.md+AGENTS.md: add a "Claude on Vertex (optional)" note (env vars + working model/region caveat).docs/scope-option-B-claude-on-vertex.md.Step 10 — Verify
npm run lint && npm test && npm run build, then the gated live smoke.Risks & mitigations
@mariozechner/pi-aiand@earendil-works/pi-aiapiname withstreamSimplelocation=globalunsupported for some models/projectsglobal+claude-opus-4-8); make region configurablepi-aivs bundled SDK)@earendil-works/pi-aito exact0.81.1; bump with the SDKOut of scope
/loginOAuth flow (unattended pipeline; env/ADC only).degradedcleanup (tracked elsewhere).Effort estimate
~2 days: Step 0 spike ~0.5d; vendor + register + wire + unit tests ~1d; live smoke + docs + verify ~0.5d.
Note on
claude-opus-4-8model availability (affects Step 2 — vendoringmodels.ts)claude-opus-4-8was manually added to the local copy of the extension'ssrc/models.ts; it is not listed by default in the pristine upstreampi-provider-vertex-anthropic@0.1.3package. (The localVERTEX_MODELScurrently containsclaude-opus-4-8,claude-opus-4-6,claude-opus-4-5@20251101,claude-sonnet-4-5@20250929, etc.)Implications for B1:
npm installof the upstream package would containclaude-opus-4-8. When vendoringmodels.tsintosrc/model/vertexAnthropic/, treat the local, edited file as the source of truth and explicitly includeclaude-opus-4-8.models.tsrecording thatclaude-opus-4-8(andclaude-opus-4-6) were added beyond the upstream defaults, so future re-syncs don't silently drop them.global+claude-opus-4-8is the only confirmed-working model/region for this project.