fix(vertex): Vertex (Antropic) provider: use .rep.googleapis.com for continental multi-region endpoints (us, eu) (#28347)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
This commit is contained in:
co-authored by
Aiden Cline
parent
3e931152d1
commit
7a9724496b
@@ -25,7 +25,8 @@ function resolveLocation(options: Record<string, any>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function vertexEndpoint(location: string) {
|
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) {
|
function replaceVertexVars(value: string, project: string | undefined, location: string) {
|
||||||
@@ -131,16 +132,23 @@ export const GoogleVertexAnthropicPlugin = PluginV2.define({
|
|||||||
"aisdk.sdk": Effect.fn(function* (evt) {
|
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||||
if (evt.package !== "@ai-sdk/google-vertex/anthropic") return
|
if (evt.package !== "@ai-sdk/google-vertex/anthropic") return
|
||||||
const mod = yield* Effect.promise(() => import("@ai-sdk/google-vertex/anthropic"))
|
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.sdk = mod.createVertexAnthropic({
|
||||||
...evt.options,
|
...evt.options,
|
||||||
project:
|
project,
|
||||||
typeof evt.options.project === "string"
|
location,
|
||||||
? evt.options.project
|
// Continental multi-regions (eu, us) require Regional Endpoint Platform
|
||||||
: (process.env.GOOGLE_CLOUD_PROJECT ?? process.env.GCP_PROJECT ?? process.env.GCLOUD_PROJECT),
|
// domains; the default {region}-aiplatform.googleapis.com does not resolve.
|
||||||
location:
|
...((location === "eu" || location === "us") && project && !evt.options.baseURL
|
||||||
typeof evt.options.location === "string"
|
? { baseURL: `https://aiplatform.${location}.rep.googleapis.com/v1/projects/${project}/locations/${location}/publishers/anthropic/models` }
|
||||||
? evt.options.location
|
: {}),
|
||||||
: (process.env.GOOGLE_CLOUD_LOCATION ?? process.env.VERTEX_LOCATION ?? "global"),
|
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
"aisdk.language": Effect.fn(function* (evt) {
|
"aisdk.language": Effect.fn(function* (evt) {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { describe, expect } from "bun:test"
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { Catalog } from "@opencode-ai/core/catalog"
|
import { Catalog } from "@opencode-ai/core/catalog"
|
||||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
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 { ProviderV2 } from "@opencode-ai/core/provider"
|
||||||
import { fakeSelectorSdk, it, model, withEnv } from "./provider-helper"
|
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", () =>
|
it.effect("trims model IDs before selecting language models", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
|
|||||||
@@ -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", () =>
|
it.effect("defaults location to us-central1 when only project is configured", () =>
|
||||||
withEnv(
|
withEnv(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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 = {
|
type BundledSDK = {
|
||||||
languageModel(modelId: string): LanguageModelV3
|
languageModel(modelId: string): LanguageModelV3
|
||||||
}
|
}
|
||||||
@@ -507,11 +514,13 @@ function custom(dep: CustomDep): Record<string, CustomLoader> {
|
|||||||
const location = env["GOOGLE_CLOUD_LOCATION"] ?? env["VERTEX_LOCATION"] ?? "global"
|
const location = env["GOOGLE_CLOUD_LOCATION"] ?? env["VERTEX_LOCATION"] ?? "global"
|
||||||
const autoload = Boolean(project)
|
const autoload = Boolean(project)
|
||||||
if (!autoload) return { autoload: false }
|
if (!autoload) return { autoload: false }
|
||||||
|
const baseURL = googleVertexAnthropicBaseURL(project, location)
|
||||||
return {
|
return {
|
||||||
autoload: true,
|
autoload: true,
|
||||||
options: {
|
options: {
|
||||||
project,
|
project,
|
||||||
location,
|
location,
|
||||||
|
...(baseURL && { baseURL }),
|
||||||
},
|
},
|
||||||
async getModel(sdk: any, modelID) {
|
async getModel(sdk: any, modelID) {
|
||||||
const id = String(modelID).trim()
|
const id = String(modelID).trim()
|
||||||
@@ -1516,6 +1525,18 @@ export const layer = Layer.effect(
|
|||||||
const provider = s.providers[model.providerID]
|
const provider = s.providers[model.providerID]
|
||||||
const options = { ...provider.options }
|
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")) {
|
if (model.providerID === "google-vertex" && !model.api.npm.includes("@ai-sdk/openai-compatible")) {
|
||||||
delete options.fetch
|
delete options.fetch
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,6 +73,8 @@ const paid = (providers: Record<string, { models: Record<string, { cost: { input
|
|||||||
return Object.values(item.models).filter((model) => model.cost.input > 0).length
|
return Object.values(item.models).filter((model) => 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 it = testEffect(Layer.mergeAll(Provider.defaultLayer, Env.defaultLayer, Plugin.defaultLayer))
|
||||||
const experimentalModels = testEffect(providerLayer({ enableExperimentalModels: true }))
|
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", () =>
|
it.instance("cloudflare-ai-gateway loads with env variables", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* set("CLOUDFLARE_ACCOUNT_ID", "test-account")
|
yield* set("CLOUDFLARE_ACCOUNT_ID", "test-account")
|
||||||
|
|||||||
Reference in New Issue
Block a user