fix(core): discover Copilot API endpoint (#38184)
This commit is contained in:
parent
da35eac93f
commit
a0a6963beb
2 changed files with 57 additions and 13 deletions
|
|
@ -13,6 +13,7 @@ import type { PluginInternal } from "../internal"
|
||||||
|
|
||||||
const clientID = "Ov23li8tweQw6odWQebz"
|
const clientID = "Ov23li8tweQw6odWQebz"
|
||||||
const apiVersion = "2026-06-01"
|
const apiVersion = "2026-06-01"
|
||||||
|
const userApiVersion = "2025-04-01"
|
||||||
const pollingSafetyMargin = 3000
|
const pollingSafetyMargin = 3000
|
||||||
const methodID = Integration.MethodID.make("device")
|
const methodID = Integration.MethodID.make("device")
|
||||||
|
|
||||||
|
|
@ -27,6 +28,14 @@ const Token = Schema.Struct({
|
||||||
error: Schema.optional(Schema.String),
|
error: Schema.optional(Schema.String),
|
||||||
interval: Schema.optional(Schema.Number),
|
interval: Schema.optional(Schema.Number),
|
||||||
})
|
})
|
||||||
|
const User = Schema.Struct({
|
||||||
|
endpoints: Schema.optional(
|
||||||
|
Schema.Struct({
|
||||||
|
api: Schema.optional(Schema.String),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
const decodeUser = Schema.decodeUnknownOption(User)
|
||||||
const JsonBody = Schema.UnknownFromJsonString
|
const JsonBody = Schema.UnknownFromJsonString
|
||||||
const decodeBody = Schema.decodeUnknownOption(JsonBody)
|
const decodeBody = Schema.decodeUnknownOption(JsonBody)
|
||||||
|
|
||||||
|
|
@ -81,15 +90,35 @@ const oauth = (app: App.Info) => ({
|
||||||
Effect.map(Schema.decodeUnknownSync(Token)),
|
Effect.map(Schema.decodeUnknownSync(Token)),
|
||||||
Effect.flatMap((token) => {
|
Effect.flatMap((token) => {
|
||||||
if (token.access_token) {
|
if (token.access_token) {
|
||||||
return Effect.succeed(
|
const access = token.access_token
|
||||||
Credential.OAuth.make({
|
return request(
|
||||||
type: "oauth",
|
`${domain === "github.com" ? "https://api.github.com" : `https://api.${domain}`}/copilot_internal/user`,
|
||||||
methodID,
|
{
|
||||||
refresh: token.access_token,
|
headers: {
|
||||||
access: token.access_token,
|
Accept: "application/json",
|
||||||
expires: 0,
|
Authorization: `Bearer ${access}`,
|
||||||
...(enterprise ? { metadata: { enterpriseUrl: domain } } : {}),
|
"User-Agent": App.useragent(app),
|
||||||
}),
|
"X-GitHub-Api-Version": userApiVersion,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
).pipe(
|
||||||
|
Effect.map((user) => Option.getOrUndefined(decodeUser(user))?.endpoints?.api?.replace(/\/+$/, "")),
|
||||||
|
Effect.catch(() => Effect.succeed(undefined)),
|
||||||
|
Effect.map((apiEndpoint) =>
|
||||||
|
Credential.OAuth.make({
|
||||||
|
type: "oauth",
|
||||||
|
methodID,
|
||||||
|
refresh: access,
|
||||||
|
access,
|
||||||
|
expires: 0,
|
||||||
|
...((enterprise || apiEndpoint) && {
|
||||||
|
metadata: {
|
||||||
|
...(enterprise ? { enterpriseUrl: domain } : {}),
|
||||||
|
...(apiEndpoint ? { apiEndpoint } : {}),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (token.error === "authorization_pending")
|
if (token.error === "authorization_pending")
|
||||||
|
|
@ -141,8 +170,7 @@ export const GithubCopilotPlugin = define({
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const enterprise = credential.metadata?.enterpriseUrl
|
loaded.baseURL = copilotBaseURL(credential.metadata)
|
||||||
loaded.baseURL = baseURL(typeof enterprise === "string" ? enterprise : undefined)
|
|
||||||
const provider = yield* catalog.provider.get(ProviderV2.ID.githubCopilot)
|
const provider = yield* catalog.provider.get(ProviderV2.ID.githubCopilot)
|
||||||
const existing = (yield* catalog.model.all()).filter((model) => model.providerID === ProviderV2.ID.githubCopilot)
|
const existing = (yield* catalog.model.all()).filter((model) => model.providerID === ProviderV2.ID.githubCopilot)
|
||||||
loaded.models = yield* Effect.tryPromise({
|
loaded.models = yield* Effect.tryPromise({
|
||||||
|
|
@ -262,6 +290,13 @@ function baseURL(enterprise?: string) {
|
||||||
return enterprise ? `https://copilot-api.${normalizeDomain(enterprise)}` : "https://api.githubcopilot.com"
|
return enterprise ? `https://copilot-api.${normalizeDomain(enterprise)}` : "https://api.githubcopilot.com"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function copilotBaseURL(metadata?: Readonly<Record<string, unknown>>) {
|
||||||
|
const endpoint = metadata?.apiEndpoint
|
||||||
|
if (typeof endpoint === "string" && endpoint) return endpoint
|
||||||
|
const enterprise = metadata?.enterpriseUrl
|
||||||
|
return baseURL(typeof enterprise === "string" ? enterprise : undefined)
|
||||||
|
}
|
||||||
|
|
||||||
function headers(app: App.Info) {
|
function headers(app: App.Info) {
|
||||||
return {
|
return {
|
||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import { AISDK } from "@opencode-ai/core/aisdk"
|
import { AISDK } from "@opencode-ai/core/aisdk"
|
||||||
import { App } from "@opencode-ai/core/app"
|
import { App } from "@opencode-ai/core/app"
|
||||||
import { describe, expect } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { Catalog } from "@opencode-ai/core/catalog"
|
import { Catalog } from "@opencode-ai/core/catalog"
|
||||||
import { ModelV2 } from "@opencode-ai/core/model"
|
import { ModelV2 } from "@opencode-ai/core/model"
|
||||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||||
import { PluginHost } from "@opencode-ai/core/plugin/host"
|
import { PluginHost } from "@opencode-ai/core/plugin/host"
|
||||||
import { copilotFetch, GithubCopilotPlugin } from "@opencode-ai/core/plugin/provider/github-copilot"
|
import { copilotBaseURL, copilotFetch, GithubCopilotPlugin } from "@opencode-ai/core/plugin/provider/github-copilot"
|
||||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||||
import { Integration } from "@opencode-ai/core/integration"
|
import { Integration } from "@opencode-ai/core/integration"
|
||||||
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
||||||
|
|
@ -41,6 +41,15 @@ function fakeSelectorSdk(calls: string[]) {
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("GithubCopilotPlugin", () => {
|
describe("GithubCopilotPlugin", () => {
|
||||||
|
test("prefers the account-specific Copilot API endpoint", () => {
|
||||||
|
expect(
|
||||||
|
copilotBaseURL({
|
||||||
|
enterpriseUrl: "company.ghe.com",
|
||||||
|
apiEndpoint: "https://api.business.githubcopilot.com",
|
||||||
|
}),
|
||||||
|
).toBe("https://api.business.githubcopilot.com")
|
||||||
|
})
|
||||||
|
|
||||||
it.effect("registers GitHub Copilot device OAuth", () =>
|
it.effect("registers GitHub Copilot device OAuth", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue