feat: add openai-compatible endpoint support for google-vertex provider (#10303)
Co-authored-by: BlueT - Matthew Lien - 練喆明 <BlueT@BlueT.org> Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
This commit is contained in:
@@ -57,6 +57,39 @@ export namespace Provider {
|
|||||||
return isGpt5OrLater(modelID) && !modelID.startsWith("gpt-5-mini")
|
return isGpt5OrLater(modelID) && !modelID.startsWith("gpt-5-mini")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function googleVertexVars(options: Record<string, any>) {
|
||||||
|
const project =
|
||||||
|
Env.get("GOOGLE_VERTEX_PROJECT") ??
|
||||||
|
options["project"] ??
|
||||||
|
Env.get("GOOGLE_CLOUD_PROJECT") ??
|
||||||
|
Env.get("GCP_PROJECT") ??
|
||||||
|
Env.get("GCLOUD_PROJECT")
|
||||||
|
const location =
|
||||||
|
Env.get("GOOGLE_VERTEX_LOCATION") ??
|
||||||
|
options["location"] ??
|
||||||
|
Env.get("GOOGLE_CLOUD_LOCATION") ??
|
||||||
|
Env.get("VERTEX_LOCATION") ??
|
||||||
|
"us-central1"
|
||||||
|
const endpoint =
|
||||||
|
Env.get("GOOGLE_VERTEX_ENDPOINT") ??
|
||||||
|
(location === "global" ? "aiplatform.googleapis.com" : `${location}-aiplatform.googleapis.com`)
|
||||||
|
return {
|
||||||
|
GOOGLE_VERTEX_PROJECT: project,
|
||||||
|
GOOGLE_VERTEX_LOCATION: location,
|
||||||
|
GOOGLE_VERTEX_ENDPOINT: endpoint,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadBaseURL(model: Model, options: Record<string, any>) {
|
||||||
|
const raw = options["baseURL"] ?? model.api.url
|
||||||
|
if (typeof raw !== "string") return raw
|
||||||
|
const vars = model.providerID === "google-vertex" ? googleVertexVars(options) : undefined
|
||||||
|
return raw.replace(/\$\{([^}]+)\}/g, (match, key) => {
|
||||||
|
const val = Env.get(String(key)) ?? vars?.[String(key) as keyof typeof vars]
|
||||||
|
return val ?? match
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const BUNDLED_PROVIDERS: Record<string, (options: any) => SDK> = {
|
const BUNDLED_PROVIDERS: Record<string, (options: any) => SDK> = {
|
||||||
"@ai-sdk/amazon-bedrock": createAmazonBedrock,
|
"@ai-sdk/amazon-bedrock": createAmazonBedrock,
|
||||||
"@ai-sdk/anthropic": createAnthropic,
|
"@ai-sdk/anthropic": createAnthropic,
|
||||||
@@ -353,9 +386,16 @@ export namespace Provider {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"google-vertex": async () => {
|
"google-vertex": async (provider) => {
|
||||||
const project = Env.get("GOOGLE_CLOUD_PROJECT") ?? Env.get("GCP_PROJECT") ?? Env.get("GCLOUD_PROJECT")
|
const project =
|
||||||
const location = Env.get("GOOGLE_CLOUD_LOCATION") ?? Env.get("VERTEX_LOCATION") ?? "us-east5"
|
provider.options?.project ??
|
||||||
|
Env.get("GOOGLE_CLOUD_PROJECT") ??
|
||||||
|
Env.get("GCP_PROJECT") ??
|
||||||
|
Env.get("GCLOUD_PROJECT")
|
||||||
|
|
||||||
|
const location =
|
||||||
|
provider.options?.location ?? Env.get("GOOGLE_CLOUD_LOCATION") ?? Env.get("VERTEX_LOCATION") ?? "us-central1"
|
||||||
|
|
||||||
const autoload = Boolean(project)
|
const autoload = Boolean(project)
|
||||||
if (!autoload) return { autoload: false }
|
if (!autoload) return { autoload: false }
|
||||||
return {
|
return {
|
||||||
@@ -363,6 +403,18 @@ export namespace Provider {
|
|||||||
options: {
|
options: {
|
||||||
project,
|
project,
|
||||||
location,
|
location,
|
||||||
|
fetch: async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||||
|
const { GoogleAuth } = await import(await BunProc.install("google-auth-library"))
|
||||||
|
const auth = new GoogleAuth()
|
||||||
|
const client = await auth.getApplicationDefault()
|
||||||
|
const credentials = await client.credential
|
||||||
|
const token = await credentials.getAccessToken()
|
||||||
|
|
||||||
|
const headers = new Headers(init?.headers)
|
||||||
|
headers.set("Authorization", `Bearer ${token.token}`)
|
||||||
|
|
||||||
|
return fetch(input, { ...init, headers })
|
||||||
|
},
|
||||||
},
|
},
|
||||||
async getModel(sdk: any, modelID: string) {
|
async getModel(sdk: any, modelID: string) {
|
||||||
const id = String(modelID).trim()
|
const id = String(modelID).trim()
|
||||||
@@ -994,11 +1046,16 @@ export namespace Provider {
|
|||||||
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.includes("@ai-sdk/openai-compatible")) {
|
||||||
|
delete options.fetch
|
||||||
|
}
|
||||||
|
|
||||||
if (model.api.npm.includes("@ai-sdk/openai-compatible") && options["includeUsage"] !== false) {
|
if (model.api.npm.includes("@ai-sdk/openai-compatible") && options["includeUsage"] !== false) {
|
||||||
options["includeUsage"] = true
|
options["includeUsage"] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!options["baseURL"]) options["baseURL"] = model.api.url
|
const baseURL = loadBaseURL(model, options)
|
||||||
|
if (baseURL !== undefined) options["baseURL"] = baseURL
|
||||||
if (options["apiKey"] === undefined && provider.key) options["apiKey"] = provider.key
|
if (options["apiKey"] === undefined && provider.key) options["apiKey"] = provider.key
|
||||||
if (model.headers)
|
if (model.headers)
|
||||||
options["headers"] = {
|
options["headers"] = {
|
||||||
|
|||||||
@@ -2127,3 +2127,95 @@ test("custom model with variants enabled and disabled", async () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("Google Vertex: retains baseURL for custom proxy", async () => {
|
||||||
|
await using tmp = await tmpdir({
|
||||||
|
init: async (dir) => {
|
||||||
|
await Bun.write(
|
||||||
|
path.join(dir, "opencode.json"),
|
||||||
|
JSON.stringify({
|
||||||
|
$schema: "https://opencode.ai/config.json",
|
||||||
|
provider: {
|
||||||
|
"vertex-proxy": {
|
||||||
|
name: "Vertex Proxy",
|
||||||
|
npm: "@ai-sdk/google-vertex",
|
||||||
|
api: "https://my-proxy.com/v1",
|
||||||
|
env: ["GOOGLE_APPLICATION_CREDENTIALS"], // Mock env var requirement
|
||||||
|
models: {
|
||||||
|
"gemini-pro": {
|
||||||
|
name: "Gemini Pro",
|
||||||
|
tool_call: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
project: "test-project",
|
||||||
|
location: "us-central1",
|
||||||
|
baseURL: "https://my-proxy.com/v1", // Should be retained
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await Instance.provide({
|
||||||
|
directory: tmp.path,
|
||||||
|
init: async () => {
|
||||||
|
Env.set("GOOGLE_APPLICATION_CREDENTIALS", "test-creds")
|
||||||
|
},
|
||||||
|
fn: async () => {
|
||||||
|
const providers = await Provider.list()
|
||||||
|
expect(providers["vertex-proxy"]).toBeDefined()
|
||||||
|
expect(providers["vertex-proxy"].options.baseURL).toBe("https://my-proxy.com/v1")
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Google Vertex: supports OpenAI compatible models", async () => {
|
||||||
|
await using tmp = await tmpdir({
|
||||||
|
init: async (dir) => {
|
||||||
|
await Bun.write(
|
||||||
|
path.join(dir, "opencode.json"),
|
||||||
|
JSON.stringify({
|
||||||
|
$schema: "https://opencode.ai/config.json",
|
||||||
|
provider: {
|
||||||
|
"vertex-openai": {
|
||||||
|
name: "Vertex OpenAI",
|
||||||
|
npm: "@ai-sdk/google-vertex",
|
||||||
|
env: ["GOOGLE_APPLICATION_CREDENTIALS"],
|
||||||
|
models: {
|
||||||
|
"gpt-4": {
|
||||||
|
name: "GPT-4",
|
||||||
|
provider: {
|
||||||
|
npm: "@ai-sdk/openai-compatible",
|
||||||
|
api: "https://api.openai.com/v1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
project: "test-project",
|
||||||
|
location: "us-central1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await Instance.provide({
|
||||||
|
directory: tmp.path,
|
||||||
|
init: async () => {
|
||||||
|
Env.set("GOOGLE_APPLICATION_CREDENTIALS", "test-creds")
|
||||||
|
},
|
||||||
|
fn: async () => {
|
||||||
|
const providers = await Provider.list()
|
||||||
|
const model = providers["vertex-openai"].models["gpt-4"]
|
||||||
|
|
||||||
|
expect(model).toBeDefined()
|
||||||
|
expect(model.api.npm).toBe("@ai-sdk/openai-compatible")
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user