diff --git a/packages/opencode/src/plugin/github-copilot/copilot.ts b/packages/opencode/src/plugin/github-copilot/copilot.ts index c0425b7efe..5f61f013d9 100644 --- a/packages/opencode/src/plugin/github-copilot/copilot.ts +++ b/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 { 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 { 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() diff --git a/packages/opencode/src/plugin/github-copilot/models.ts b/packages/opencode/src/plugin/github-copilot/models.ts index b6b27d0340..dfd6ceceaa 100644 --- a/packages/opencode/src/plugin/github-copilot/models.ts +++ b/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", diff --git a/packages/opencode/test/plugin/github-copilot-models.test.ts b/packages/opencode/test/plugin/github-copilot-models.test.ts index 78fe40aead..0b67588a7e 100644 --- a/packages/opencode/test/plugin/github-copilot-models.test.ts +++ b/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") +})