Compare commits

...

2 Commits

Author SHA1 Message Date
Aiden Cline 5e2dc53ff9 fix(opencode): clarify Copilot login domain 2026-07-20 17:03:26 -05:00
Aiden Cline 5e3e1372e3 fix(opencode): discover Copilot API endpoint 2026-07-20 16:58:52 -05:00
2 changed files with 84 additions and 9 deletions
@@ -8,6 +8,7 @@ import { MessageV2 } from "@/session/message-v2"
const CLIENT_ID = "Ov23li8tweQw6odWQebz"
const API_VERSION = "2026-06-01"
const USER_API_VERSION = "2025-04-01"
const UTILITY_MODELS = ["gpt-5.4-nano", "gpt-4.1", "gpt-4o", "gpt-4o-mini"]
// Add a small safety buffer when polling to avoid hitting the server
// slightly too early due to clock skew / timer drift.
@@ -66,9 +67,28 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise<Hooks> {
}
const auth = ctx.auth
const fallback = base(auth.enterpriseUrl)
const url = await fetch(
`${auth.enterpriseUrl ? `https://api.${normalizeDomain(auth.enterpriseUrl)}` : "https://api.github.com"}/copilot_internal/user`,
{
headers: {
Accept: "application/json",
Authorization: `Bearer ${auth.refresh}`,
"X-GitHub-Api-Version": USER_API_VERSION,
},
signal: AbortSignal.timeout(5_000),
},
)
.then(async (response) => {
if (!response.ok) return fallback
const result = (await response.json()) as { endpoints?: { api?: unknown } }
const endpoint = result.endpoints?.api
return typeof endpoint === "string" && endpoint ? endpoint.replace(/\/+$/, "") : fallback
})
.catch(() => fallback)
return CopilotModels.get(
base(auth.enterpriseUrl),
url,
{
...(provider.options?.headers as Record<string, string> | undefined),
Authorization: `Bearer ${auth.refresh}`,
@@ -85,9 +105,7 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise<Hooks> {
})
.catch((error) => {
models = {}
return Object.fromEntries(
Object.entries(provider.models).map(([id, model]) => [id, fix(model, base(auth.enterpriseUrl))]),
)
return Object.fromEntries(Object.entries(provider.models).map(([id, model]) => [id, fix(model, url)]))
})
},
},
@@ -187,17 +205,17 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise<Hooks> {
{
type: "select",
key: "deploymentType",
message: "Select GitHub deployment type",
message: "Select GitHub login domain",
options: [
{
label: "GitHub.com",
label: "github.com",
value: "github.com",
hint: "Public",
hint: "Default",
},
{
label: "GitHub Enterprise",
label: "Custom domain",
value: "enterprise",
hint: "Data residency or self-hosted",
hint: "GHE.com or GitHub Enterprise Server",
},
],
},
@@ -422,3 +422,60 @@ test("remaps fallback oauth model urls to the enterprise host", async () => {
expect(models.claude.api.url).toBe("https://copilot-api.ghe.example.com")
expect(models.claude.api.npm).toBe("@ai-sdk/github-copilot")
})
test("uses the account-specific Copilot API endpoint", async () => {
const requests: string[] = []
globalThis.fetch = mock((input: RequestInfo | URL) => {
const url = input instanceof URL ? input.href : typeof input === "string" ? input : input.url
requests.push(url)
if (url === "https://api.github.com/copilot_internal/user") {
return Promise.resolve(
new Response(JSON.stringify({ endpoints: { api: "https://api.business.githubcopilot.com/" } })),
)
}
return Promise.reject(new Error("timeout"))
}) as unknown as typeof fetch
const hooks = await CopilotAuthPlugin({
client: {} as never,
project: {} as never,
directory: "",
worktree: "",
experimental_workspace: {
register() {},
},
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,
} as never,
},
)
expect(requests).toEqual([
"https://api.github.com/copilot_internal/user",
"https://api.business.githubcopilot.com/models",
])
expect(models.claude.api.url).toBe("https://api.business.githubcopilot.com")
})