From 7a9724496b0f255fa8da13db58cb4ebb14841860 Mon Sep 17 00:00:00 2001 From: JPFrancoia Date: Fri, 22 May 2026 05:03:24 +0100 Subject: [PATCH] fix(vertex): Vertex (Antropic) provider: use .rep.googleapis.com for continental multi-region endpoints (us, eu) (#28347) Co-authored-by: Aiden Cline --- .../core/src/plugin/provider/google-vertex.ts | 26 ++++--- .../provider-google-vertex-anthropic.test.ts | 69 ++++++++++++++++++- .../plugin/provider-google-vertex.test.ts | 26 +++++++ packages/opencode/src/provider/provider.ts | 21 ++++++ .../opencode/test/provider/provider.test.ts | 50 ++++++++++++++ 5 files changed, 182 insertions(+), 10 deletions(-) diff --git a/packages/core/src/plugin/provider/google-vertex.ts b/packages/core/src/plugin/provider/google-vertex.ts index eaf24e00b6..2993152748 100644 --- a/packages/core/src/plugin/provider/google-vertex.ts +++ b/packages/core/src/plugin/provider/google-vertex.ts @@ -25,7 +25,8 @@ function resolveLocation(options: Record) { } function vertexEndpoint(location: string) { - return location === "global" ? "aiplatform.googleapis.com" : `${location}-aiplatform.googleapis.com` + if (location === "global") return "aiplatform.googleapis.com" + return `${location}-aiplatform.googleapis.com` } function replaceVertexVars(value: string, project: string | undefined, location: string) { @@ -131,16 +132,23 @@ export const GoogleVertexAnthropicPlugin = PluginV2.define({ "aisdk.sdk": Effect.fn(function* (evt) { if (evt.package !== "@ai-sdk/google-vertex/anthropic") return const mod = yield* Effect.promise(() => import("@ai-sdk/google-vertex/anthropic")) + const project = + typeof evt.options.project === "string" + ? evt.options.project + : (process.env.GOOGLE_CLOUD_PROJECT ?? process.env.GCP_PROJECT ?? process.env.GCLOUD_PROJECT) + const location = + typeof evt.options.location === "string" + ? evt.options.location + : (process.env.GOOGLE_CLOUD_LOCATION ?? process.env.VERTEX_LOCATION ?? "global") evt.sdk = mod.createVertexAnthropic({ ...evt.options, - project: - typeof evt.options.project === "string" - ? evt.options.project - : (process.env.GOOGLE_CLOUD_PROJECT ?? process.env.GCP_PROJECT ?? process.env.GCLOUD_PROJECT), - location: - typeof evt.options.location === "string" - ? evt.options.location - : (process.env.GOOGLE_CLOUD_LOCATION ?? process.env.VERTEX_LOCATION ?? "global"), + project, + location, + // Continental multi-regions (eu, us) require Regional Endpoint Platform + // domains; the default {region}-aiplatform.googleapis.com does not resolve. + ...((location === "eu" || location === "us") && project && !evt.options.baseURL + ? { baseURL: `https://aiplatform.${location}.rep.googleapis.com/v1/projects/${project}/locations/${location}/publishers/anthropic/models` } + : {}), }) }), "aisdk.language": Effect.fn(function* (evt) { diff --git a/packages/core/test/plugin/provider-google-vertex-anthropic.test.ts b/packages/core/test/plugin/provider-google-vertex-anthropic.test.ts index 85a11baf82..7cb2b24ffd 100644 --- a/packages/core/test/plugin/provider-google-vertex-anthropic.test.ts +++ b/packages/core/test/plugin/provider-google-vertex-anthropic.test.ts @@ -2,7 +2,7 @@ import { describe, expect } from "bun:test" import { Effect } from "effect" import { Catalog } from "@opencode-ai/core/catalog" import { PluginV2 } from "@opencode-ai/core/plugin" -import { GoogleVertexAnthropicPlugin } from "@opencode-ai/core/plugin/provider/google-vertex" +import { GoogleVertexAnthropicPlugin, GoogleVertexPlugin } from "@opencode-ai/core/plugin/provider/google-vertex" import { ProviderV2 } from "@opencode-ai/core/provider" import { fakeSelectorSdk, it, model, withEnv } from "./provider-helper" @@ -109,6 +109,73 @@ describe("GoogleVertexAnthropicPlugin", () => { ), ) + it.effect("creates SDKs for google-vertex Anthropic models with multi-region endpoints", () => + Effect.gen(function* () { + const plugin = yield* PluginV2.Service + yield* plugin.add(GoogleVertexAnthropicPlugin) + const result = yield* plugin.trigger( + "aisdk.sdk", + { + model: model("google-vertex", "claude-sonnet-4-5"), + package: "@ai-sdk/google-vertex/anthropic", + options: { name: "google-vertex", project: "project", location: "eu" }, + }, + {}, + ) + expect(result.sdk.languageModel("claude-sonnet-4-5").config.baseURL).toBe( + "https://aiplatform.eu.rep.googleapis.com/v1/projects/project/locations/eu/publishers/anthropic/models", + ) + }), + ) + + it.effect("keeps configured baseURL for google-vertex Anthropic models", () => + Effect.gen(function* () { + const plugin = yield* PluginV2.Service + yield* plugin.add(GoogleVertexAnthropicPlugin) + const result = yield* plugin.trigger( + "aisdk.sdk", + { + model: model("google-vertex", "claude-sonnet-4-5"), + package: "@ai-sdk/google-vertex/anthropic", + options: { name: "google-vertex", project: "project", location: "eu", baseURL: "https://proxy.example/v1" }, + }, + {}, + ) + expect(result.sdk.languageModel("claude-sonnet-4-5").config.baseURL).toBe("https://proxy.example/v1") + }), + ) + + it.effect("selects google-vertex Anthropic language models through V2 plugins", () => + Effect.gen(function* () { + const plugin = yield* PluginV2.Service + yield* plugin.add(GoogleVertexPlugin) + yield* plugin.add(GoogleVertexAnthropicPlugin) + const sdkResult = yield* plugin.trigger( + "aisdk.sdk", + { + model: model("google-vertex", " claude-sonnet-4-5 "), + package: "@ai-sdk/google-vertex/anthropic", + options: { name: "google-vertex", project: "project", location: "us" }, + }, + {}, + ) + const languageResult = yield* plugin.trigger( + "aisdk.language", + { + model: model("google-vertex", " claude-sonnet-4-5 "), + sdk: sdkResult.sdk, + options: {}, + }, + {}, + ) + const language = languageResult.language as unknown as { config: { baseURL: string }; modelId: string } + expect(language.config.baseURL).toBe( + "https://aiplatform.us.rep.googleapis.com/v1/projects/project/locations/us/publishers/anthropic/models", + ) + expect(language.modelId).toBe("claude-sonnet-4-5") + }), + ) + it.effect("trims model IDs before selecting language models", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service diff --git a/packages/core/test/plugin/provider-google-vertex.test.ts b/packages/core/test/plugin/provider-google-vertex.test.ts index 4c4e19ee85..2a9a18875c 100644 --- a/packages/core/test/plugin/provider-google-vertex.test.ts +++ b/packages/core/test/plugin/provider-google-vertex.test.ts @@ -160,6 +160,32 @@ describe("GoogleVertexPlugin", () => { ), ) + it.effect("keeps OpenAI-compatible Vertex endpoint templates regional for eu", () => + Effect.gen(function* () { + const plugin = yield* PluginV2.Service + const catalog = yield* Catalog.Service + yield* plugin.add(GoogleVertexPlugin) + const load = yield* catalog.loader() + yield* load((catalog) => + catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => { + provider.endpoint = { + type: "aisdk", + package: "@ai-sdk/openai-compatible", + url: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}", + } + provider.options.aisdk.provider.project = "config-project" + provider.options.aisdk.provider.location = "eu" + }), + ) + const provider = yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")) + expect(provider.endpoint).toEqual({ + type: "aisdk", + package: "@ai-sdk/openai-compatible", + url: "https://eu-aiplatform.googleapis.com/v1/projects/config-project/locations/eu", + }) + }), + ) + it.effect("defaults location to us-central1 when only project is configured", () => withEnv( { diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 2518021529..2fe2b7a4a5 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -85,6 +85,13 @@ function wrapSSE(res: Response, ms: number, ctl: AbortController) { }) } +function googleVertexAnthropicBaseURL(project: string | undefined, location: string | undefined) { + if (!project) return + if (location !== "eu" && location !== "us") return + // Continental multi-regions require Regional Endpoint Platform domains. + return `https://aiplatform.${location}.rep.googleapis.com/v1/projects/${project}/locations/${location}/publishers/anthropic/models` +} + type BundledSDK = { languageModel(modelId: string): LanguageModelV3 } @@ -507,11 +514,13 @@ function custom(dep: CustomDep): Record { const location = env["GOOGLE_CLOUD_LOCATION"] ?? env["VERTEX_LOCATION"] ?? "global" const autoload = Boolean(project) if (!autoload) return { autoload: false } + const baseURL = googleVertexAnthropicBaseURL(project, location) return { autoload: true, options: { project, location, + ...(baseURL && { baseURL }), }, async getModel(sdk: any, modelID) { const id = String(modelID).trim() @@ -1516,6 +1525,18 @@ export const layer = Layer.effect( const provider = s.providers[model.providerID] const options = { ...provider.options } + if ( + model.providerID === "google-vertex" && + model.api.npm === "@ai-sdk/google-vertex/anthropic" && + !options.baseURL + ) { + const baseURL = googleVertexAnthropicBaseURL( + typeof options.project === "string" ? options.project : undefined, + typeof options.location === "string" ? options.location : undefined, + ) + if (baseURL) options.baseURL = baseURL + } + if (model.providerID === "google-vertex" && !model.api.npm.includes("@ai-sdk/openai-compatible")) { delete options.fetch } diff --git a/packages/opencode/test/provider/provider.test.ts b/packages/opencode/test/provider/provider.test.ts index 069c2f5ca3..3a2d34f560 100644 --- a/packages/opencode/test/provider/provider.test.ts +++ b/packages/opencode/test/provider/provider.test.ts @@ -73,6 +73,8 @@ const paid = (providers: Record model.cost.input > 0).length } +const languageBaseURL = (language: unknown) => (language as { config: { baseURL: string } }).config.baseURL + const it = testEffect(Layer.mergeAll(Provider.defaultLayer, Env.defaultLayer, Plugin.defaultLayer)) const experimentalModels = testEffect(providerLayer({ enableExperimentalModels: true })) @@ -1546,6 +1548,54 @@ it.instance( }, ) +it.instance("Google Vertex: uses REP endpoint for Claude continental multi-regions", () => + Effect.gen(function* () { + yield* set("GOOGLE_CLOUD_PROJECT", "test-project") + yield* set("VERTEX_LOCATION", "eu") + const provider = yield* Provider.Service + const model = yield* provider.getModel( + ProviderID.make("google-vertex"), + ModelID.make("claude-sonnet-4-6@default"), + ) + const language = yield* provider.getLanguage(model) + expect(languageBaseURL(language)).toBe( + "https://aiplatform.eu.rep.googleapis.com/v1/projects/test-project/locations/eu/publishers/anthropic/models", + ) + }), +) + +it.instance("Google Vertex Anthropic: uses REP endpoint for continental multi-regions", () => + Effect.gen(function* () { + yield* set("GOOGLE_CLOUD_PROJECT", "test-project") + yield* set("VERTEX_LOCATION", "us") + const provider = yield* Provider.Service + const model = yield* provider.getModel( + ProviderID.make("google-vertex-anthropic"), + ModelID.make("claude-sonnet-4-6@default"), + ) + const language = yield* provider.getLanguage(model) + expect(languageBaseURL(language)).toBe( + "https://aiplatform.us.rep.googleapis.com/v1/projects/test-project/locations/us/publishers/anthropic/models", + ) + }), +) + +it.instance("Google Vertex: keeps regional Claude endpoints unchanged", () => + Effect.gen(function* () { + yield* set("GOOGLE_CLOUD_PROJECT", "test-project") + yield* set("VERTEX_LOCATION", "europe-west1") + const provider = yield* Provider.Service + const model = yield* provider.getModel( + ProviderID.make("google-vertex"), + ModelID.make("claude-sonnet-4-6@default"), + ) + const language = yield* provider.getLanguage(model) + expect(languageBaseURL(language)).toBe( + "https://europe-west1-aiplatform.googleapis.com/v1/projects/test-project/locations/europe-west1/publishers/anthropic/models", + ) + }), +) + it.instance("cloudflare-ai-gateway loads with env variables", () => Effect.gen(function* () { yield* set("CLOUDFLARE_ACCOUNT_ID", "test-account")