From 754fddbc798f1d7791f73cebe67069cdac3033b5 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 31 Jul 2026 04:50:24 -0700 Subject: [PATCH] fix(anthropic-vertex): correct multi-region endpoints (#116757) Co-authored-by: Peter Steinberger --- extensions/anthropic-vertex/index.test.ts | 17 +++++++-- .../anthropic-vertex/provider-catalog.ts | 4 ++- extensions/anthropic-vertex/region.test.ts | 6 ++++ .../anthropic-vertex/stream-runtime.test.ts | 18 ++++++++++ .../anthropic-payload-policy.test.ts | 36 +++++++++++++++++++ .../transports/anthropic-payload-policy.ts | 2 ++ 6 files changed, 80 insertions(+), 3 deletions(-) create mode 100644 packages/ai/src/transports/anthropic-payload-policy.test.ts diff --git a/extensions/anthropic-vertex/index.test.ts b/extensions/anthropic-vertex/index.test.ts index 56d97a821ee5..b124d77751fc 100644 --- a/extensions/anthropic-vertex/index.test.ts +++ b/extensions/anthropic-vertex/index.test.ts @@ -102,6 +102,19 @@ describe("anthropic-vertex provider plugin", () => { expect(result.provider.models[4]?.thinkingLevelMap).toEqual({ xhigh: null, max: "max" }); }); + it.each([ + { region: "global", baseUrl: "https://aiplatform.googleapis.com" }, + { region: "us", baseUrl: "https://aiplatform.us.rep.googleapis.com" }, + { region: "eu", baseUrl: "https://aiplatform.eu.rep.googleapis.com" }, + { region: "us-east5", baseUrl: "https://us-east5-aiplatform.googleapis.com" }, + ])("publishes the SDK endpoint for the $region location", ({ region, baseUrl }) => { + expect( + buildAnthropicVertexProvider({ + env: { GOOGLE_CLOUD_LOCATION: region }, + }).baseUrl, + ).toBe(baseUrl); + }); + it.each(["global", "us", "eu"])("publishes Opus 5 for the %s endpoint", (region) => { const provider = buildAnthropicVertexProvider({ env: { GOOGLE_CLOUD_LOCATION: region }, @@ -194,7 +207,7 @@ describe("anthropic-vertex provider plugin", () => { name: "Claude Sonnet 5", api: "anthropic-messages", provider: "anthropic-vertex", - baseUrl: "https://us-aiplatform.googleapis.com", + baseUrl: "https://aiplatform.us.rep.googleapis.com", reasoning: true, input: ["text", "image"], contextWindow: 1_000_000, @@ -235,7 +248,7 @@ describe("anthropic-vertex provider plugin", () => { name: "Claude Opus 5", api: "anthropic-messages", provider: "anthropic-vertex", - baseUrl: "https://us-aiplatform.googleapis.com", + baseUrl: "https://aiplatform.us.rep.googleapis.com", reasoning: false, input: ["text"], cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 }, diff --git a/extensions/anthropic-vertex/provider-catalog.ts b/extensions/anthropic-vertex/provider-catalog.ts index e83408662305..da9bf0e506b7 100644 --- a/extensions/anthropic-vertex/provider-catalog.ts +++ b/extensions/anthropic-vertex/provider-catalog.ts @@ -238,7 +238,9 @@ export function buildAnthropicVertexProvider(params?: { const baseUrl = normalizeLowercaseStringOrEmpty(region) === "global" ? "https://aiplatform.googleapis.com" - : `https://${region}-aiplatform.googleapis.com`; + : region === "us" || region === "eu" + ? `https://aiplatform.${region}.rep.googleapis.com` + : `https://${region}-aiplatform.googleapis.com`; return { baseUrl, diff --git a/extensions/anthropic-vertex/region.test.ts b/extensions/anthropic-vertex/region.test.ts index 49a2ced78f94..30b8686039f1 100644 --- a/extensions/anthropic-vertex/region.test.ts +++ b/extensions/anthropic-vertex/region.test.ts @@ -25,6 +25,12 @@ describe("anthropic vertex region helpers", () => { ).toBe("europe-west4"); }); + it.each(["us", "eu"])("parses the %s multi-region Vertex endpoint", (region) => { + expect( + resolveAnthropicVertexRegionFromBaseUrl(`https://aiplatform.${region}.rep.googleapis.com`), + ).toBe(region); + }); + it("treats the global Vertex endpoint as global", () => { expect(resolveAnthropicVertexRegionFromBaseUrl("https://aiplatform.googleapis.com")).toBe( "global", diff --git a/extensions/anthropic-vertex/stream-runtime.test.ts b/extensions/anthropic-vertex/stream-runtime.test.ts index 00a56299ae8e..2cd043fba24a 100644 --- a/extensions/anthropic-vertex/stream-runtime.test.ts +++ b/extensions/anthropic-vertex/stream-runtime.test.ts @@ -589,6 +589,24 @@ describe("createAnthropicVertexStreamFn", () => { }); describe("createAnthropicVertexStreamFnForModel", () => { + it.each(["us", "eu"])("preserves the %s multi-region SDK endpoint", (region) => { + const { deps, anthropicVertexCtorMock, googleAuthClient } = createStreamDeps(); + const streamFn = createAnthropicVertexStreamFnForModel( + { baseUrl: `https://aiplatform.${region}.rep.googleapis.com` }, + { GOOGLE_CLOUD_PROJECT_ID: "vertex-project" } as NodeJS.ProcessEnv, + deps, + ); + + void streamFn(makeModel({ id: "claude-sonnet-5", maxTokens: 128_000 }), { messages: [] }, {}); + + expect(anthropicVertexCtorMock).toHaveBeenCalledWith({ + googleAuth: googleAuthClient, + projectId: "vertex-project", + region, + baseURL: `https://aiplatform.${region}.rep.googleapis.com/v1`, + }); + }); + it("derives project and region from the model and env", () => { const { deps, anthropicVertexCtorMock, googleAuthClient } = createStreamDeps(); const streamFn = createAnthropicVertexStreamFnForModel( diff --git a/packages/ai/src/transports/anthropic-payload-policy.test.ts b/packages/ai/src/transports/anthropic-payload-policy.test.ts new file mode 100644 index 000000000000..8603d1edbfbd --- /dev/null +++ b/packages/ai/src/transports/anthropic-payload-policy.test.ts @@ -0,0 +1,36 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { resolveAnthropicEphemeralCacheControl } from "./anthropic-payload-policy.js"; + +describe("resolveAnthropicEphemeralCacheControl", () => { + afterEach(() => { + vi.unstubAllEnvs(); + }); + + it.each([ + "https://aiplatform.googleapis.com", + "https://us-east5-aiplatform.googleapis.com", + "https://aiplatform.us.rep.googleapis.com", + "https://aiplatform.eu.rep.googleapis.com", + ])("preserves env-configured long retention for the official %s endpoint", (baseUrl) => { + vi.stubEnv("OPENCLAW_CACHE_RETENTION", "long"); + + expect(resolveAnthropicEphemeralCacheControl(baseUrl, undefined)).toEqual({ + type: "ephemeral", + ttl: "1h", + }); + }); + + it("keeps env-configured long retention restricted for custom proxy endpoints", () => { + vi.stubEnv("OPENCLAW_CACHE_RETENTION", "long"); + + expect( + resolveAnthropicEphemeralCacheControl("https://proxy.example.test/vertex", undefined), + ).toEqual({ type: "ephemeral" }); + }); + + it("preserves explicitly configured long retention for custom proxy endpoints", () => { + expect( + resolveAnthropicEphemeralCacheControl("https://proxy.example.test/vertex", "long"), + ).toEqual({ type: "ephemeral", ttl: "1h" }); + }); +}); diff --git a/packages/ai/src/transports/anthropic-payload-policy.ts b/packages/ai/src/transports/anthropic-payload-policy.ts index 265b1dfa1633..f17a1f4c67b0 100644 --- a/packages/ai/src/transports/anthropic-payload-policy.ts +++ b/packages/ai/src/transports/anthropic-payload-policy.ts @@ -56,6 +56,8 @@ function isLongTtlEligibleEndpoint(baseUrl: string | undefined): boolean { return ( hostname === "api.anthropic.com" || hostname === "aiplatform.googleapis.com" || + hostname === "aiplatform.us.rep.googleapis.com" || + hostname === "aiplatform.eu.rep.googleapis.com" || hostname.endsWith("-aiplatform.googleapis.com") ); }