Sfoglia il codice sorgente

feat: make gh copilot use msgs api when available (#22106)

Aiden Cline 5 giorni fa
parent
commit
cdb951ec2f

+ 10 - 8
packages/opencode/src/plugin/github-copilot/copilot.ts

@@ -27,11 +27,12 @@ function base(enterpriseUrl?: string) {
   return enterpriseUrl ? `https://copilot-api.${normalizeDomain(enterpriseUrl)}` : "https://api.githubcopilot.com"
 }
 
-function fix(model: Model): Model {
+function fix(model: Model, url: string): Model {
   return {
     ...model,
     api: {
       ...model.api,
+      url,
       npm: "@ai-sdk/github-copilot",
     },
   }
@@ -44,19 +45,23 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise<Hooks> {
       id: "github-copilot",
       async models(provider, ctx) {
         if (ctx.auth?.type !== "oauth") {
-          return Object.fromEntries(Object.entries(provider.models).map(([id, model]) => [id, fix(model)]))
+          return Object.fromEntries(Object.entries(provider.models).map(([id, model]) => [id, fix(model, base())]))
         }
 
+        const auth = ctx.auth
+
         return CopilotModels.get(
-          base(ctx.auth.enterpriseUrl),
+          base(auth.enterpriseUrl),
           {
-            Authorization: `Bearer ${ctx.auth.refresh}`,
+            Authorization: `Bearer ${auth.refresh}`,
             "User-Agent": `opencode/${Installation.VERSION}`,
           },
           provider.models,
         ).catch((error) => {
           log.error("failed to fetch copilot models", { error })
-          return Object.fromEntries(Object.entries(provider.models).map(([id, model]) => [id, fix(model)]))
+          return Object.fromEntries(
+            Object.entries(provider.models).map(([id, model]) => [id, fix(model, base(auth.enterpriseUrl))]),
+          )
         })
       },
     },
@@ -66,10 +71,7 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise<Hooks> {
         const info = await getAuth()
         if (!info || info.type !== "oauth") return {}
 
-        const baseURL = base(info.enterpriseUrl)
-
         return {
-          baseURL,
           apiKey: "",
           async fetch(request: RequestInfo | URL, init?: RequestInit) {
             const info = await getAuth()

+ 4 - 2
packages/opencode/src/plugin/github-copilot/models.ts

@@ -52,13 +52,15 @@ export namespace CopilotModels {
       (remote.capabilities.supports.vision ?? false) ||
       (remote.capabilities.limits.vision?.supported_media_types ?? []).some((item) => item.startsWith("image/"))
 
+    const isMsgApi = remote.supported_endpoints?.includes("/v1/messages")
+
     return {
       id: key,
       providerID: "github-copilot",
       api: {
         id: remote.id,
-        url,
-        npm: "@ai-sdk/github-copilot",
+        url: isMsgApi ? `${url}/v1` : url,
+        npm: isMsgApi ? "@ai-sdk/anthropic" : "@ai-sdk/github-copilot",
       },
       // API response wins
       status: "active",

+ 43 - 0
packages/opencode/test/plugin/github-copilot-models.test.ts

@@ -1,5 +1,6 @@
 import { afterEach, expect, mock, test } from "bun:test"
 import { CopilotModels } from "@/plugin/github-copilot/models"
+import { CopilotAuthPlugin } from "@/plugin/github-copilot/copilot"
 
 const originalFetch = globalThis.fetch
 
@@ -115,3 +116,45 @@ test("preserves temperature support from existing provider models", async () =>
   expect(models["gpt-4o"].capabilities.temperature).toBe(true)
   expect(models["brand-new"].capabilities.temperature).toBe(true)
 })
+
+test("remaps fallback oauth model urls to the enterprise host", async () => {
+  globalThis.fetch = mock(() => Promise.reject(new Error("timeout"))) as unknown as typeof fetch
+
+  const hooks = await CopilotAuthPlugin({
+    client: {} as never,
+    project: {} as never,
+    directory: "",
+    worktree: "",
+    serverUrl: new URL("https://example.com"),
+    $: {} as never,
+  })
+
+  const models = await hooks.provider!.models!(
+    {
+      id: "github-copilot",
+      models: {
+        claude: {
+          id: "claude",
+          providerID: "github-copilot",
+          api: {
+            id: "claude-sonnet-4.5",
+            url: "https://api.githubcopilot.com/v1",
+            npm: "@ai-sdk/anthropic",
+          },
+        },
+      },
+    } as never,
+    {
+      auth: {
+        type: "oauth",
+        refresh: "token",
+        access: "token",
+        expires: Date.now() + 60_000,
+        enterpriseUrl: "ghe.example.com",
+      } as never,
+    },
+  )
+
+  expect(models.claude.api.url).toBe("https://copilot-api.ghe.example.com")
+  expect(models.claude.api.npm).toBe("@ai-sdk/github-copilot")
+})