import { AISDK } from "@opencode-ai/core/aisdk" import { App } from "@opencode-ai/core/app" import { describe, expect, test } from "bun:test" import { Effect, Fiber } from "effect" import { Catalog } from "@opencode-ai/core/catalog" import { Credential } from "@opencode-ai/core/credential" import { ModelV2 } from "@opencode-ai/core/model" import { PluginV2 } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { copilotBaseURL, copilotFetch, GithubCopilotPlugin } from "@opencode-ai/core/plugin/provider/github-copilot" import { ProviderV2 } from "@opencode-ai/core/provider" import { Integration } from "@opencode-ai/core/integration" import type { LanguageModelV3 } from "@ai-sdk/provider" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* (credential?: Credential.OAuth) { const plugin = yield* PluginV2.Service const aisdk = yield* AISDK.Service const host = yield* PluginHost.make(plugin) yield* GithubCopilotPlugin.effect({ ...host, integration: { ...host.integration, connection: { active: () => Effect.succeed(credential ? ({ type: "env", name: "TEST" } as const) : undefined), resolve: () => Effect.succeed(credential), }, }, }) }) function required(value: T | undefined): T { if (value === undefined) throw new Error("Expected value") return value } function fakeSelectorSdk(calls: string[]) { const make = (method: string) => (id: string) => { calls.push(`${method}:${id}`) return { modelId: id, provider: method, specificationVersion: "v3" } as unknown as LanguageModelV3 } return { responses: make("responses"), messages: make("messages"), chat: make("chat"), languageModel: make("languageModel"), } } 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", () => Effect.gen(function* () { yield* addPlugin() expect((yield* (yield* Integration.Service).get(Integration.ID.make("github-copilot")))?.methods).toContainEqual({ id: Integration.MethodID.make("device"), type: "oauth", label: "Login with GitHub Copilot", prompts: expect.any(Array), }) }), ) it.live("waits for initial Copilot models before exposing the connected catalog", () => Effect.acquireUseRelease( Effect.sync(() => { const requested = Promise.withResolvers() const release = Promise.withResolvers() const server = Bun.serve({ port: 0, fetch: async () => { requested.resolve() await release.promise return Response.json({ data: [ { model_picker_enabled: true, id: "gpt-5", name: "GPT-5", version: "gpt-5-2026-06-01", supported_endpoints: ["/responses"], capabilities: { family: "gpt", limits: { max_output_tokens: 16_384, max_prompt_tokens: 180_000 }, supports: { tool_calls: true }, }, }, ], }) }, }) return { release, requested, server } }), ({ release, requested, server }) => Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((draft) => { draft.provider.update(ProviderV2.ID.githubCopilot, () => {}) draft.model.update(ProviderV2.ID.githubCopilot, ModelV2.ID.make("stale"), () => {}) }) const credential = Credential.OAuth.make({ type: "oauth", methodID: Integration.MethodID.make("device"), access: "access", refresh: "refresh", expires: 0, metadata: { apiEndpoint: server.url.origin }, }) const plugin = yield* addPlugin(credential).pipe(Effect.forkScoped({ startImmediately: true })) yield* Effect.promise(() => requested.promise) const pending = plugin.pollUnsafe() === undefined release.resolve() yield* Fiber.join(plugin) expect(pending).toBe(true) expect(yield* catalog.model.get(ProviderV2.ID.githubCopilot, ModelV2.ID.make("stale"))).toBeUndefined() expect(yield* catalog.model.get(ProviderV2.ID.githubCopilot, ModelV2.ID.make("gpt-5"))).toBeDefined() }), ({ release, server }) => { release.resolve() return Effect.promise(() => server.stop(true)) }, ), ) it.live("adds Copilot authentication and request metadata headers", () => Effect.gen(function* () { const requests: Headers[] = [] const send = copilotFetch( "token", async (_input: Parameters[0], init?: RequestInit) => { requests.push(new Headers(init?.headers)) return Response.json({ ok: true }) }, false, 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-06-01") expect(requests[0]?.get("user-agent")).toBe("opencode/beta/1.2.3/test") }), ) it.effect("creates the bundled Copilot SDK for the GitHub Copilot package", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service const aisdk = yield* AISDK.Service yield* addPlugin() const ignored = yield* aisdk.runSDK({ model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")), modelID: ModelV2.ID.make("gpt-5"), package: "aisdk:test-provider", }), package: "@ai-sdk/openai-compatible", options: { name: "github-copilot" }, }) const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")), modelID: ModelV2.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("selects languageModel when responses and chat are absent", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() yield* aisdk.runLanguage({ model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("claude-sonnet-4")), modelID: ModelV2.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 plugin = yield* PluginV2.Service const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() yield* aisdk.runLanguage({ model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("alias")), modelID: ModelV2.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 plugin = yield* PluginV2.Service const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() yield* aisdk.runLanguage({ model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")), modelID: ModelV2.ID.make("gpt-5"), package: "aisdk:test-provider", }), sdk: fakeSelectorSdk(calls), options: {}, }) yield* aisdk.runLanguage({ model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5.1-codex")), modelID: ModelV2.ID.make("gpt-5.1-codex"), package: "aisdk:test-provider", }), sdk: fakeSelectorSdk(calls), options: {}, }) yield* aisdk.runLanguage({ model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-4o")), modelID: ModelV2.ID.make("gpt-4o"), package: "aisdk:test-provider", }), sdk: fakeSelectorSdk(calls), options: {}, }) yield* aisdk.runLanguage({ model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-mini")), modelID: ModelV2.ID.make("gpt-5-mini"), package: "aisdk:test-provider", }), sdk: fakeSelectorSdk(calls), options: {}, }) yield* aisdk.runLanguage({ model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-mini-2025-08-07")), modelID: ModelV2.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 plugin = yield* PluginV2.Service const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() yield* aisdk.runLanguage({ model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("mai-code-1-flash-picker")), modelID: ModelV2.ID.make("mai-code-1-flash-picker"), package: "aisdk:test-provider", settings: { endpoint: "responses" }, }), sdk: fakeSelectorSdk(calls), options: { endpoint: "responses" }, }) yield* aisdk.runLanguage({ model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")), modelID: ModelV2.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 plugin = yield* PluginV2.Service const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() yield* aisdk.runLanguage({ model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("default")), modelID: ModelV2.ID.make("gpt-5"), package: "aisdk:test-provider", }), sdk: fakeSelectorSdk(calls), options: {}, }) yield* aisdk.runLanguage({ model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("small")), modelID: ModelV2.ID.make("gpt-5-mini"), package: "aisdk:test-provider", }), sdk: fakeSelectorSdk(calls), options: {}, }) yield* aisdk.runLanguage({ model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("sonnet")), modelID: ModelV2.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 catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { catalog.provider.update(ProviderV2.ID.make("github-copilot"), () => {}) catalog.model.update(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-chat-latest"), () => {}) }) yield* addPlugin() expect( required(yield* catalog.model.get(ProviderV2.ID.make("github-copilot"), ModelV2.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 catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { catalog.provider.update(ProviderV2.ID.make("custom-copilot"), () => {}) catalog.model.update(ProviderV2.ID.make("custom-copilot"), ModelV2.ID.make("gpt-5-chat-latest"), () => {}) }) yield* addPlugin() expect( required(yield* catalog.model.get(ProviderV2.ID.make("custom-copilot"), ModelV2.ID.make("gpt-5-chat-latest"))) .enabled, ).toBe(true) }), ) it.effect("ignores non-Copilot providers", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() const result = yield* aisdk.runLanguage({ model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("openai"), ModelV2.ID.make("gpt-5")), modelID: ModelV2.ID.make("gpt-5"), package: "aisdk:test-provider", }), sdk: fakeSelectorSdk(calls), options: {}, }) expect(calls).toEqual([]) expect(result.language).toBeUndefined() }), ) })