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

This commit is contained in:
Aiden Cline
2026-04-11 23:06:35 -05:00
committed by GitHub
parent fc01cad2b8
commit cdb951ec2f
3 changed files with 57 additions and 10 deletions

View File

@@ -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()

View File

@@ -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",

View File

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