Przeglądaj źródła

feat: add openai-compatible endpoint support for google-vertex provider (#10303)

Co-authored-by: BlueT - Matthew Lien - 練喆明 <[email protected]>
Co-authored-by: Aiden Cline <[email protected]>
Co-authored-by: Aiden Cline <[email protected]>
Jhin Lee 1 miesiąc temu
rodzic
commit
f7708efa5b

+ 61 - 4
packages/opencode/src/provider/provider.ts

@@ -57,6 +57,39 @@ export namespace Provider {
     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> = {
     "@ai-sdk/amazon-bedrock": createAmazonBedrock,
     "@ai-sdk/anthropic": createAnthropic,
@@ -353,9 +386,16 @@ export namespace Provider {
         },
       }
     },
-    "google-vertex": async () => {
-      const project = Env.get("GOOGLE_CLOUD_PROJECT") ?? Env.get("GCP_PROJECT") ?? Env.get("GCLOUD_PROJECT")
-      const location = Env.get("GOOGLE_CLOUD_LOCATION") ?? Env.get("VERTEX_LOCATION") ?? "us-east5"
+    "google-vertex": async (provider) => {
+      const project =
+        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)
       if (!autoload) return { autoload: false }
       return {
@@ -363,6 +403,18 @@ export namespace Provider {
         options: {
           project,
           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) {
           const id = String(modelID).trim()
@@ -994,11 +1046,16 @@ export namespace Provider {
       const provider = s.providers[model.providerID]
       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) {
         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 (model.headers)
         options["headers"] = {

+ 92 - 0
packages/opencode/test/provider/provider.test.ts

@@ -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")
+    },
+  })
+})
+