Something went wrong. Try again.
opencode v2 mirror
Something went wrong. Try again.
TypeScript
at plugact
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537import { AISDK } from "@opencode/core/aisdk"import { App } from "@opencode/core/app"import { Agent } from "@opencode/schema/agent"import { Session } from "@opencode/core/session"import { Location } from "@opencode/core/location"import { describe, expect, test } from "bun:test"import { Effect } from "effect"import { Model } from "@opencode/core/model"import { ModelResolver } from "@opencode/core/model-resolver"import { Plugin } from "@opencode/core/plugin"import { PluginHost } from "@opencode/core/plugin/host"import { PluginHooks } from "@opencode/core/plugin/hooks"import { copilotBaseURL, copilotEntitlementError, copilotFetch, GithubCopilotPlugin,} from "@opencode/core/plugin/provider/github-copilot"import { Provider } from "@opencode/core/provider"import { Integration } from "@opencode/core/integration"import type { SessionRequestKind } from "@opencode/plugin/effect/session"import { fakeSelectorSdk } from "../fixture/selector"import { testEffect } from "../lib/effect"import { PluginTestLayer } from "./fixture"
const it = testEffect(PluginTestLayer)
const addPlugin = Effect.fn(function* () { const plugin = yield* Plugin.Service const host = yield* PluginHost.make(plugin) yield* GithubCopilotPlugin.effect(host)})
function required<T>(value: T | undefined): T { if (value === undefined) throw new Error("Expected value") return value}
const sessions = Effect.fn(function* () { const service = yield* Session.Service const location = yield* Location.Service const parent = yield* service.create({ location: { directory: location.directory } }) const child = yield* service.create({ parentID: parent.id }) return { parent: parent.id, child: child.id }})
const modelRequest = Effect.fn(function* (sessionID: Session.ID, kind: SessionRequestKind, agent = "build") { const hooks = yield* PluginHooks.Service return yield* hooks.trigger("session", "model.request", { sessionID, agent: Agent.ID.make(agent), model: Model.Ref.make({ providerID: Provider.ID.githubCopilot, id: Model.ID.make("gpt-5.4") }), kind, headers: {}, })})
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") })
test("rejects accounts without Copilot chat access", () => { expect(copilotEntitlementError({ chat_enabled: false, can_signup_for_limited: true })).toContain("Copilot Free") expect(copilotEntitlementError({ chat_enabled: false, can_signup_for_limited: false })).toContain( "subscription or a seat", ) expect(copilotEntitlementError({ chat_enabled: false })).toContain("subscription or a seat") })
test("only blocks on an explicit entitlement denial", () => { expect(copilotEntitlementError({ chat_enabled: true })).toBeUndefined() expect(copilotEntitlementError({ chat_enabled: true, can_signup_for_limited: true })).toBeUndefined() expect(copilotEntitlementError({})).toBeUndefined() })
it.effect("registers GitHub Copilot device OAuth", () => Effect.gen(function* () { yield* addPlugin() const integrations = yield* Integration.Service expect((yield* integrations.get(Integration.ID.make("github-copilot")))?.methods).toContainEqual({ id: Integration.MethodID.make("device"), type: "oauth", label: "Login with GitHub Copilot", form: expect.any(Array), }) }), )
it.effect("removes the generic key method", () => Effect.gen(function* () { const integrations = yield* Integration.Service yield* integrations.transform((editor) => { editor.method.update({ integrationID: Integration.ID.make("github-copilot"), method: { type: "key" }, }) editor.method.update({ integrationID: Integration.ID.make("github-copilot"), method: { type: "env", names: ["GITHUB_TOKEN"] }, }) }) yield* addPlugin() expect((yield* integrations.get(Integration.ID.make("github-copilot")))?.methods).toEqual([ { type: "env", names: ["GITHUB_TOKEN"] }, { id: Integration.MethodID.make("device"), type: "oauth", label: "Login with GitHub Copilot", form: expect.any(Array), }, ]) }), )
it.live("adds Copilot authentication and request metadata headers", () => Effect.gen(function* () { const requests: Headers[] = [] const send = copilotFetch( "token", async (_input: Parameters<typeof fetch>[0], init?: RequestInit) => { requests.push(new Headers(init?.headers)) return Response.json({ ok: true }) }, App.make({ name: "test", version: "1.2.3", channel: "beta" }), ) yield* Effect.promise(() => send("https://api.githubcopilot.com/chat/completions", { method: "POST", headers: { "x-api-key": "old" }, body: JSON.stringify({ messages: [{ role: "user", content: [{ type: "image_url", image_url: { url: "data:image/png" } }] }], }), }), ) expect(requests[0]?.get("authorization")).toBe("Bearer token") expect(requests[0]?.has("x-api-key")).toBe(false) expect(requests[0]?.get("x-initiator")).toBe("user") expect(requests[0]?.get("copilot-vision-request")).toBe("true") expect(requests[0]?.get("x-github-api-version")).toBe("2026-08-01") expect(requests[0]?.get("user-agent")).toBe("opencode/beta/1.2.3/test") }), )
it.effect("adds Copilot authentication to native Anthropic requests", () => Effect.gen(function* () { yield* addPlugin() const hooks = yield* PluginHooks.Service const event = yield* hooks.trigger("session", "http.request", { sessionID: Session.ID.make("ses_test"), agent: Agent.ID.make("build"), model: Model.Ref.make({ providerID: Provider.ID.githubCopilot, id: Model.ID.make("claude-sonnet-4.5") }), kind: "primary", request: new Request("https://api.githubcopilot.com/v1/messages", { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": "token" }, body: JSON.stringify({ messages: [{ role: "user", content: [{ type: "text", text: "hello" }] }] }), }), }) expect(event.request.headers.get("authorization")).toBe("Bearer token") expect(event.request.headers.has("x-api-key")).toBe(false) expect(event.request.headers.get("x-initiator")).toBe("user") expect(event.request.headers.get("anthropic-beta")).toBe("interleaved-thinking-2025-05-14") expect(event.request.headers.get("x-github-api-version")).toBe("2026-08-01") }), )
it.effect("classifies main-loop steps as agent interactions", () => Effect.gen(function* () { yield* addPlugin() const event = yield* modelRequest((yield* sessions()).parent, "primary") expect(event.headers).toEqual({ "X-Interaction-Type": "conversation-agent", "X-Interaction-Id": event.sessionID }) }), )
it.effect("classifies child-session steps as subagent interactions", () => Effect.gen(function* () { yield* addPlugin() const event = yield* modelRequest((yield* sessions()).child, "primary") expect(event.headers).toEqual({ "X-Interaction-Type": "conversation-subagent", "X-Interaction-Id": event.sessionID, "x-initiator": "agent", }) }), )
it.effect("classifies title generation as a background interaction", () => Effect.gen(function* () { yield* addPlugin() const event = yield* modelRequest((yield* sessions()).parent, "title") expect(event.headers).toEqual({ "X-Interaction-Type": "conversation-background", "X-Interaction-Id": event.sessionID, "x-initiator": "agent", }) }), )
it.effect("classifies compaction requests by kind rather than agent", () => Effect.gen(function* () { yield* addPlugin() const event = yield* modelRequest((yield* sessions()).child, "compaction", "build") expect(event.headers).toEqual({ "X-Interaction-Type": "conversation-compaction", "X-Interaction-Id": event.sessionID, "x-initiator": "agent", }) }), )
it.effect("does not classify by agent name", () => Effect.gen(function* () { yield* addPlugin() const event = yield* modelRequest((yield* sessions()).parent, "primary", "compaction") expect(event.headers).toEqual({ "X-Interaction-Type": "conversation-agent", "X-Interaction-Id": event.sessionID }) }), )
it.effect("ignores other providers' model requests", () => Effect.gen(function* () { yield* addPlugin() const hooks = yield* PluginHooks.Service const event = yield* hooks.trigger("session", "model.request", { sessionID: (yield* sessions()).parent, agent: Agent.ID.make("build"), model: Model.Ref.make({ providerID: Provider.ID.make("openai"), id: Model.ID.make("gpt-5.4") }), kind: "primary", headers: {}, }) expect(event.headers).toEqual({}) }), )
it.live("keeps a declared agent initiator when the body looks user-initiated", () => Effect.gen(function* () { const requests: Headers[] = [] const send = copilotFetch( "token", async (_input: Parameters<typeof fetch>[0], init?: RequestInit) => { requests.push(new Headers(init?.headers)) return Response.json({ ok: true }) }, App.make({ name: "test", version: "1.2.3", channel: "beta" }), ) yield* Effect.promise(() => send("https://api.githubcopilot.com/chat/completions", { method: "POST", headers: { "x-initiator": "agent" }, body: JSON.stringify({ messages: [{ role: "user", content: "summarize" }] }), }), ) expect(requests[0]?.get("x-initiator")).toBe("agent") }), )
it.effect("classifies session generation requests as agent interactions", () => Effect.gen(function* () { yield* addPlugin() const event = yield* modelRequest((yield* sessions()).parent, "generate") expect(event.headers).toEqual({ "X-Interaction-Type": "conversation-agent", "X-Interaction-Id": event.sessionID }) }), )
it.effect("creates the bundled Copilot SDK for the GitHub Copilot package", () => Effect.gen(function* () { const aisdk = yield* AISDK.Service yield* addPlugin() const ignored = yield* aisdk.runSDK({ model: Model.Info.make({ ...Model.Info.default(Provider.ID.make("github-copilot"), Model.ID.make("gpt-5")), modelID: Model.ID.make("gpt-5"), package: "aisdk:test-provider", }), package: "@ai-sdk/openai-compatible", options: { name: "github-copilot" }, }) const result = yield* aisdk.runSDK({ model: Model.Info.make({ ...Model.Info.default(Provider.ID.make("github-copilot"), Model.ID.make("gpt-5")), modelID: Model.ID.make("gpt-5"), package: "aisdk:test-provider", }), package: "@ai-sdk/github-copilot", options: { name: "github-copilot" }, }) expect(ignored.sdk).toBeUndefined() expect(result.sdk).toBeDefined() }), )
it.effect("rewrites models.dev fallback models to the GitHub Copilot package", () => Effect.gen(function* () { const providers = yield* Provider.Service const models = yield* Model.Service const aisdk = yield* AISDK.Service yield* providers.transform((editor) => { editor.update(Provider.ID.githubCopilot, (provider) => { provider.activation = "enabled" }) editor.models.update(Provider.ID.githubCopilot, Model.ID.make("gpt-5.6-sol"), (model) => { model.package = Provider.aisdk("@ai-sdk/openai-compatible") }) }) yield* addPlugin() const fallback = required(yield* models.get(Provider.ID.githubCopilot, Model.ID.make("gpt-5.6-sol"))) expect(fallback.package).toBe(Provider.aisdk("@ai-sdk/github-copilot"))
const resolved = yield* ModelResolver.fromCatalogModel(fallback, undefined, { loadPackage: () => Effect.die("Copilot must not load a native provider package"), loadAISDK: (model) => aisdk.model(model), }) expect(resolved.route.id).toBe("ai-sdk:@ai-sdk/github-copilot") expect(resolved.route.providerMetadataKey).toBe("copilot") }), )
it.effect("selects languageModel when responses and chat are absent", () => Effect.gen(function* () { const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() yield* aisdk.runLanguage({ model: Model.Info.make({ ...Model.Info.default(Provider.ID.make("github-copilot"), Model.ID.make("claude-sonnet-4")), modelID: Model.ID.make("claude-sonnet-4"), package: "aisdk:test-provider", }), sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, options: {}, }) expect(calls).toEqual(["languageModel:claude-sonnet-4"]) }), )
it.effect("selects languageModel with the API model ID when responses and chat are absent", () => Effect.gen(function* () { const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() yield* aisdk.runLanguage({ model: Model.Info.make({ ...Model.Info.default(Provider.ID.make("github-copilot"), Model.ID.make("alias")), modelID: Model.ID.make("claude-sonnet-4"), package: "aisdk:test-provider", }), sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, options: {}, }) expect(calls).toEqual(["languageModel:claude-sonnet-4"]) }), )
it.effect("uses responses for gpt-5 models except gpt-5-mini", () => Effect.gen(function* () { const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() yield* aisdk.runLanguage({ model: Model.Info.make({ ...Model.Info.default(Provider.ID.make("github-copilot"), Model.ID.make("gpt-5")), modelID: Model.ID.make("gpt-5"), package: "aisdk:test-provider", }), sdk: fakeSelectorSdk(calls), options: {}, }) yield* aisdk.runLanguage({ model: Model.Info.make({ ...Model.Info.default(Provider.ID.make("github-copilot"), Model.ID.make("gpt-5.1-codex")), modelID: Model.ID.make("gpt-5.1-codex"), package: "aisdk:test-provider", }), sdk: fakeSelectorSdk(calls), options: {}, }) yield* aisdk.runLanguage({ model: Model.Info.make({ ...Model.Info.default(Provider.ID.make("github-copilot"), Model.ID.make("gpt-4o")), modelID: Model.ID.make("gpt-4o"), package: "aisdk:test-provider", }), sdk: fakeSelectorSdk(calls), options: {}, }) yield* aisdk.runLanguage({ model: Model.Info.make({ ...Model.Info.default(Provider.ID.make("github-copilot"), Model.ID.make("gpt-5-mini")), modelID: Model.ID.make("gpt-5-mini"), package: "aisdk:test-provider", }), sdk: fakeSelectorSdk(calls), options: {}, }) yield* aisdk.runLanguage({ model: Model.Info.make({ ...Model.Info.default(Provider.ID.make("github-copilot"), Model.ID.make("gpt-5-mini-2025-08-07")), modelID: Model.ID.make("gpt-5-mini-2025-08-07"), package: "aisdk:test-provider", }), sdk: fakeSelectorSdk(calls), options: {}, }) expect(calls).toEqual([ "responses:gpt-5", "responses:gpt-5.1-codex", "chat:gpt-4o", "chat:gpt-5-mini", "chat:gpt-5-mini-2025-08-07", ]) }), )
it.effect("uses advertised Copilot endpoint metadata before model ID fallbacks", () => Effect.gen(function* () { const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() yield* aisdk.runLanguage({ model: Model.Info.make({ ...Model.Info.default(Provider.ID.make("github-copilot"), Model.ID.make("mai-code-1-flash-picker")), modelID: Model.ID.make("mai-code-1-flash-picker"), package: "aisdk:test-provider", settings: { endpoint: "responses" }, }), sdk: fakeSelectorSdk(calls), options: { endpoint: "responses" }, }) yield* aisdk.runLanguage({ model: Model.Info.make({ ...Model.Info.default(Provider.ID.make("github-copilot"), Model.ID.make("gpt-5")), modelID: Model.ID.make("gpt-5"), package: "aisdk:test-provider", settings: { endpoint: "chat" }, }), sdk: fakeSelectorSdk(calls), options: { endpoint: "chat" }, }) expect(calls).toEqual(["responses:mai-code-1-flash-picker", "chat:gpt-5"]) }), )
it.effect("uses the API model ID when selecting responses or chat", () => Effect.gen(function* () { const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() yield* aisdk.runLanguage({ model: Model.Info.make({ ...Model.Info.default(Provider.ID.make("github-copilot"), Model.ID.make("default")), modelID: Model.ID.make("gpt-5"), package: "aisdk:test-provider", }), sdk: fakeSelectorSdk(calls), options: {}, }) yield* aisdk.runLanguage({ model: Model.Info.make({ ...Model.Info.default(Provider.ID.make("github-copilot"), Model.ID.make("small")), modelID: Model.ID.make("gpt-5-mini"), package: "aisdk:test-provider", }), sdk: fakeSelectorSdk(calls), options: {}, }) yield* aisdk.runLanguage({ model: Model.Info.make({ ...Model.Info.default(Provider.ID.make("github-copilot"), Model.ID.make("sonnet")), modelID: Model.ID.make("claude-sonnet-4"), package: "aisdk:test-provider", }), sdk: fakeSelectorSdk(calls), options: {}, }) expect(calls).toEqual(["responses:gpt-5", "chat:gpt-5-mini", "chat:claude-sonnet-4"]) }), )
it.effect("disables gpt-5-chat-latest before Copilot language selection", () => Effect.gen(function* () { const providers = yield* Provider.Service const models = yield* Model.Service yield* providers.transform((editor) => { editor.update(Provider.ID.githubCopilot, (provider) => { provider.activation = "enabled" }) editor.models.update(Provider.ID.githubCopilot, Model.ID.make("gpt-5-chat-latest"), () => {}) }) yield* addPlugin() expect( required(yield* models.get(Provider.ID.githubCopilot, Model.ID.make("gpt-5-chat-latest"))) .enabled, ).toBe(false) }), )
it.effect("does not disable gpt-5-chat-latest for non-Copilot providers", () => Effect.gen(function* () { const providers = yield* Provider.Service const models = yield* Model.Service yield* providers.transform((editor) => { editor.update(Provider.ID.make("custom-copilot"), () => {}) editor.models.update(Provider.ID.make("custom-copilot"), Model.ID.make("gpt-5-chat-latest"), () => {}) }) yield* addPlugin() expect( required(yield* models.get(Provider.ID.make("custom-copilot"), Model.ID.make("gpt-5-chat-latest"))) .enabled, ).toBe(true) }), )
it.effect("ignores non-Copilot providers", () => Effect.gen(function* () { const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() const result = yield* aisdk.runLanguage({ model: Model.Info.make({ ...Model.Info.default(Provider.ID.make("openai"), Model.ID.make("gpt-5")), modelID: Model.ID.make("gpt-5"), package: "aisdk:test-provider", }), sdk: fakeSelectorSdk(calls), options: {}, }) expect(calls).toEqual([]) expect(result.language).toBeUndefined() }), )})