summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAiden Cline <[email protected]>2026-04-11 23:06:35 -0500
committerGitHub <[email protected]>2026-04-11 23:06:35 -0500
commitcdb951ec2f2db96b08b3579bcf7af75f02d056d2 (patch)
tree6bead917d95fb3fb10ddec75fc3b159ff0ed6831
parentfc01cad2b842a4025e7a04dc79130e843b1d56b7 (diff)
downloadopencode-cdb951ec2f2db96b08b3579bcf7af75f02d056d2.tar.gz
opencode-cdb951ec2f2db96b08b3579bcf7af75f02d056d2.zip
feat: make gh copilot use msgs api when available (#22106)
-rw-r--r--packages/opencode/src/plugin/github-copilot/copilot.ts18
-rw-r--r--packages/opencode/src/plugin/github-copilot/models.ts6
-rw-r--r--packages/opencode/test/plugin/github-copilot-models.test.ts43
3 files changed, 57 insertions, 10 deletions
diff --git a/packages/opencode/src/plugin/github-copilot/copilot.ts b/packages/opencode/src/plugin/github-copilot/copilot.ts
index c0425b7ef..5f61f013d 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<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()
diff --git a/packages/opencode/src/plugin/github-copilot/models.ts b/packages/opencode/src/plugin/github-copilot/models.ts
index b6b27d034..dfd6cecea 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 78fe40aea..0b67588a7 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")
+})