Something went wrong. Try again.
opencode v2 mirror
Something went wrong. Try again.
TypeScript
at systemd
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412import type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/effect/integration"import { Effect, Option, Schema, Semaphore, Stream } from "effect"import { Catalog } from "../../catalog.js"import { Credential } from "../../credential.js"import { Bus } from "../../bus.js"import { CopilotModels } from "../../github-copilot/models.js"import { App } from "../../app.js"import { Agent } from "../../agent.js"import { Integration } from "../../integration.js"import { Model } from "../../model.js"import { define } from "@opencode-ai/plugin/effect/plugin"import { Provider } from "../../provider.js"import type { PluginInternal } from "../internal.js"
const clientID = "Ov23li8tweQw6odWQebz"const apiVersion = "2026-06-01"const userApiVersion = "2025-04-01"const pollingSafetyMargin = 3000const methodID = Integration.MethodID.make("device")
const Device = Schema.Struct({ verification_uri: Schema.String, user_code: Schema.String, device_code: Schema.String, interval: Schema.Number,})const Token = Schema.Struct({ access_token: Schema.optional(Schema.String), error: Schema.optional(Schema.String), 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.fromJsonString(Schema.Unknown)const decodeBody = Schema.decodeUnknownOption(JsonBody)
const oauth = (app: App.Info) => ({ integrationID: Integration.ID.make("github-copilot"), method: { id: methodID, type: "oauth", label: "Login with GitHub Copilot", form: [ { type: "string", key: "deploymentType", title: "Select GitHub deployment type", required: true, options: [ { label: "GitHub.com", value: "github.com", description: "Public" }, { label: "GitHub Enterprise", value: "enterprise", description: "Data residency or self-hosted" }, ], }, { type: "string", key: "enterpriseUrl", title: "Enter your GitHub Enterprise URL or domain", placeholder: "company.ghe.com or https://company.ghe.com", required: true, when: [{ key: "deploymentType", op: "eq", value: "enterprise" }], }, ], }, authorize: (answer) => Effect.gen(function* () { const enterprise = answer.deploymentType === "enterprise" const enterpriseUrl = typeof answer.enterpriseUrl === "string" ? answer.enterpriseUrl : undefined if (enterprise && !enterpriseUrl) return yield* Effect.fail(new Error("Enterprise URL is required")) const domain = enterprise ? normalizeDomain(enterpriseUrl ?? "") : "github.com" const urls = oauthURLs(domain) const device = yield* request(urls.device, { method: "POST", headers: headers(app), body: JSON.stringify({ client_id: clientID, scope: "read:user" }), }).pipe(Effect.map(Schema.decodeUnknownSync(Device))) const interval = Math.max(device.interval, 1) * 1000
const poll = (wait: number): Effect.Effect<Credential.OAuth, unknown> => request(urls.token, { method: "POST", headers: headers(app), body: JSON.stringify({ client_id: clientID, device_code: device.device_code, grant_type: "urn:ietf:params:oauth:grant-type:device_code", }), }).pipe( Effect.map(Schema.decodeUnknownSync(Token)), Effect.flatMap((token) => { if (token.access_token) { const access = token.access_token return request( `${domain === "github.com" ? "https://api.github.com" : `https://api.${domain}`}/copilot_internal/user`, { headers: { Accept: "application/json", Authorization: `Bearer ${access}`, "User-Agent": App.useragent(app), "X-GitHub-Api-Version": userApiVersion, }, }, ).pipe( Effect.map((user) => Option.getOrUndefined(decodeUser(user))?.endpoints?.api?.replace(/\/+$/, "")), Effect.orElseSucceed(() => 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") return Effect.sleep(wait + pollingSafetyMargin).pipe(Effect.andThen(poll(wait))) if (token.error === "slow_down") { const next = token.interval && token.interval > 0 ? token.interval * 1000 : wait + 5000 return Effect.sleep(next + pollingSafetyMargin).pipe(Effect.andThen(poll(next))) } return Effect.fail(new Error(`Device authorization failed${token.error ? `: ${token.error}` : ""}`)) }), )
return { mode: "auto" as const, url: device.verification_uri, instructions: `Enter code: ${device.user_code}`, callback: poll(interval), } }), }) satisfies IntegrationOAuthMethodRegistration
export const GithubCopilotPlugin = define({ id: "opencode.provider.github.copilot", effect: Effect.fn(function* (ctx) { const catalog = yield* Catalog.Service const bus = yield* Bus.Service const loading = Semaphore.makeUnsafe(1) const loaded: { baseURL?: string models?: Map<Model.ID, Model.Info> } = {}
const load = Effect.fn("GithubCopilotPlugin.load")(function* () { const connection = yield* ctx.integration.connection.active("github-copilot") const credential = connection ? yield* ctx.integration.connection.resolve(connection).pipe(Effect.orElseSucceed(() => undefined)) : undefined if (credential?.type !== "oauth") { loaded.baseURL = undefined loaded.models = undefined return }
loaded.baseURL = copilotBaseURL(credential.metadata) const provider = yield* catalog.provider.get(Provider.ID.githubCopilot) const existing = (yield* catalog.model.all()).filter((model) => model.providerID === Provider.ID.githubCopilot) loaded.models = yield* Effect.tryPromise({ try: () => CopilotModels.get( loaded.baseURL ?? baseURL(), { ...provider?.headers, Authorization: `Bearer ${credential.refresh}`, "User-Agent": App.useragent(ctx.app), "X-GitHub-Api-Version": apiVersion, }, existing, ), catch: (cause) => cause, }).pipe( Effect.catch((cause) => Effect.logWarning("failed to sync GitHub Copilot models", { cause }).pipe(Effect.as(undefined)), ), ) })
yield* ctx.integration.transform((draft) => { draft.method.remove("github-copilot", { type: "key" }) draft.method.update(oauth(ctx.app)) }) yield* ctx.catalog.transform((evt) => { const item = evt.provider.get(Provider.ID.githubCopilot) if (!item) return if (loaded.models) { for (const id of item.models.keys()) { if (!loaded.models.has(Model.ID.make(id))) evt.model.remove(item.provider.id, id) } for (const [id, model] of loaded.models) { evt.model.update(item.provider.id, id, (draft) => Object.assign(draft, structuredClone(model))) } } else { for (const id of item.models.keys()) { evt.model.update(item.provider.id, id, (model) => { model.package = Provider.aisdk("@ai-sdk/github-copilot") if (loaded.baseURL) model.settings = Provider.mergeOverlay(model.settings, { baseURL: loaded.baseURL }) }) } } if (item.models.has(Model.ID.make("gpt-5-chat-latest"))) { evt.model.update(item.provider.id, Model.ID.make("gpt-5-chat-latest"), (model) => { // This chat-only alias conflicts with the Copilot GPT-5 Responses route, // so hide it only for Copilot rather than for every provider catalog. model.enabled = false }) } }) const refresh = () => loading.withPermit(load().pipe(Effect.andThen(ctx.catalog.reload()))) yield* bus.subscribe(Credential.Event.Switched).pipe( Stream.filter((event) => event.data.integrationID === Integration.ID.make("github-copilot")), Stream.runForEach(refresh), Effect.forkScoped({ startImmediately: true }), ) yield* refresh().pipe(Effect.forkScoped) yield* ctx.aisdk.hook( "sdk", Effect.fn(function* (evt) { if (evt.model.providerID !== Provider.ID.githubCopilot) return if (evt.package !== "@ai-sdk/github-copilot") return evt.options.fetch = copilotFetch( typeof evt.options.apiKey === "string" ? evt.options.apiKey : undefined, evt.options.fetch, ctx.app, ) const mod = yield* Effect.promise(() => import("../../github-copilot/copilot-provider.js")) evt.sdk = mod.createOpenaiCompatible(evt.options) }), ) yield* ctx.session.hook( "http.request", (evt) => Effect.gen(function* () { if (evt.model.providerID !== Provider.ID.githubCopilot) return if (evt.agent === Agent.ID.make("title")) evt.request.headers.set("X-Interaction-Type", "conversation-background") if (evt.agent === Agent.ID.make("compaction")) evt.request.headers.set("X-Interaction-Type", "conversation-compaction") const token = evt.request.headers.get("x-api-key") if (!token) return const text = yield* Effect.promise(() => evt.request.clone().text()) const body = Option.getOrUndefined(decodeBody(text)) applyHeaders(evt.request.headers, token, ctx.app, requestMetadata(evt.request.url, body), true) }), { providerID: Provider.ID.githubCopilot }, ) yield* ctx.aisdk.hook( "language", Effect.fn(function* (evt) { if (evt.model.providerID !== Provider.ID.githubCopilot) return if (evt.sdk.responses === undefined && evt.sdk.chat === undefined) { evt.language = evt.sdk.languageModel(evt.model.modelID ?? evt.model.id) return } if (evt.options.endpoint === "responses" && evt.sdk.responses) { evt.language = evt.sdk.responses(evt.model.modelID ?? evt.model.id) return } if (evt.options.endpoint === "chat" && evt.sdk.chat) { evt.language = evt.sdk.chat(evt.model.modelID ?? evt.model.id) return } const id = evt.model.modelID ?? evt.model.id const match = /^gpt-(\d+)/.exec(id) evt.language = match && Number(match[1]) >= 5 && !id.startsWith("gpt-5-mini") ? evt.sdk.responses(id) : evt.sdk.chat(id) }), ) }),} satisfies PluginInternal.InternalPlugin)
function normalizeDomain(input: string) { return input.replace(/^https?:\/\//, "").replace(/\/$/, "")}
function oauthURLs(domain: string) { return { device: `https://${domain}/login/device/code`, token: `https://${domain}/login/oauth/access_token`, }}
function baseURL(enterprise?: string) { 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) { return { Accept: "application/json", "Content-Type": "application/json", "User-Agent": App.useragent(app), }}
function request(url: string, init: RequestInit) { return Effect.tryPromise({ try: async (signal) => { const response = await fetch(url, { ...init, signal }) if (!response.ok) throw new Error(`Request failed: ${response.status}`) return response.json() }, catch: (cause) => cause, })}
type Fetch = (input: Parameters<typeof fetch>[0], init?: RequestInit) => Promise<Response>
export function copilotFetch(token: string | undefined, upstream: Fetch | undefined, app: App.Info): Fetch { const send = upstream ?? fetch return async (input, init) => { const requestHeaders = new Headers(init?.headers) const url = input instanceof URL ? input.href : typeof input === "string" ? input : input.url const body = typeof init?.body === "string" ? Option.getOrUndefined(decodeBody(init.body)) : undefined applyHeaders(requestHeaders, token, app, requestMetadata(url, body), false) return send(input, { ...init, headers: requestHeaders }) }}
function applyHeaders( headers: Headers, token: string | undefined, app: App.Info, metadata: RequestMetadata, anthropic: boolean,) { if (token) { headers.delete("authorization") headers.delete("x-api-key") headers.set("Authorization", `Bearer ${token}`) } headers.set("User-Agent", App.useragent(app)) headers.set("Openai-Intent", "conversation-edits") headers.set("X-GitHub-Api-Version", apiVersion) headers.set("x-initiator", metadata.agent ? "agent" : "user") if (metadata.vision) headers.set("Copilot-Vision-Request", "true") if (anthropic) headers.set("anthropic-beta", "interleaved-thinking-2025-05-14")}
type RequestMetadata = ReturnType<typeof requestMetadata>
function requestMetadata(url: string, body: unknown) { if (!record(body)) return { agent: false, vision: false } if (Array.isArray(body.input)) { const last = body.input.at(-1) return { agent: !record(last) || last.role !== "user", vision: body.input.some( (item) => record(item) && Array.isArray(item.content) && item.content.some((part) => record(part) && part.type === "input_image"), ), } } if (!Array.isArray(body.messages)) return { agent: false, vision: false } const last = body.messages.at(-1) if (url.includes("completions")) { return { agent: !record(last) || last.role !== "user", vision: body.messages.some( (message) => record(message) && Array.isArray(message.content) && message.content.some((part) => record(part) && part.type === "image_url"), ), } } const content = record(last) && Array.isArray(last.content) ? last.content : [] return { agent: !record(last) || last.role !== "user" || !content.some((part) => record(part) && part.type !== "tool_result"), vision: body.messages.some( (message) => record(message) && Array.isArray(message.content) && message.content.some( (part) => record(part) && (part.type === "image" || (part.type === "tool_result" && Array.isArray(part.content) && part.content.some((nested) => record(nested) && nested.type === "image"))), ), ), }}
function record(input: unknown): input is Record<string, unknown> { return typeof input === "object" && input !== null && !Array.isArray(input)}