feat(core): port GitHub Copilot OAuth (#36336)

Co-authored-by: Dax Raad <d@ironbay.co>
This commit is contained in:
opencode-agent[bot]
2026-07-10 21:11:12 -04:00
committed by GitHub
co-authored by Dax Raad
parent ed6c117184
commit d1550cb599
4 changed files with 643 additions and 10 deletions
+214
View File
@@ -0,0 +1,214 @@
export * as CopilotModels from "./models"
import { Money } from "@opencode-ai/schema/money"
import { Option, Schema } from "effect"
import { ModelV2 } from "../model"
import { ProviderV2 } from "../provider"
const RemoteModel = Schema.Struct({
model_picker_enabled: Schema.Boolean,
id: Schema.String,
name: Schema.String,
version: Schema.String,
supported_endpoints: Schema.optional(Schema.Array(Schema.String)),
policy: Schema.optional(Schema.Struct({ state: Schema.optional(Schema.String) })),
billing: Schema.optional(
Schema.Struct({
token_prices: Schema.optional(
Schema.Struct({
batch_size: Schema.Number,
default: Schema.Struct({
cache_price: Schema.Number,
input_price: Schema.Number,
output_price: Schema.Number,
}),
}),
),
}),
),
capabilities: Schema.Struct({
family: Schema.String,
limits: Schema.optional(
Schema.Struct({
max_context_window_tokens: Schema.optional(Schema.Number),
max_output_tokens: Schema.optional(Schema.Number),
max_prompt_tokens: Schema.optional(Schema.Number),
vision: Schema.optional(
Schema.Struct({
max_prompt_image_size: Schema.Number,
max_prompt_images: Schema.Number,
supported_media_types: Schema.Array(Schema.String),
}),
),
}),
),
supports: Schema.Struct({
adaptive_thinking: Schema.optional(Schema.Boolean),
max_thinking_budget: Schema.optional(Schema.Number),
min_thinking_budget: Schema.optional(Schema.Number),
reasoning_effort: Schema.optional(Schema.Array(Schema.String)),
streaming: Schema.optional(Schema.Boolean),
structured_outputs: Schema.optional(Schema.Boolean),
tool_calls: Schema.optional(Schema.Boolean),
vision: Schema.optional(Schema.Boolean),
}),
}),
})
const Response = Schema.Struct({ data: Schema.Array(Schema.Unknown) })
const decodeResponse = Schema.decodeUnknownSync(Response)
const decodeModel = Schema.decodeUnknownOption(RemoteModel)
type RemoteModel = typeof RemoteModel.Type
type UsableModel = RemoteModel & {
capabilities: RemoteModel["capabilities"] & {
limits: NonNullable<RemoteModel["capabilities"]["limits"]> & {
max_output_tokens: number
max_prompt_tokens: number
}
supports: RemoteModel["capabilities"]["supports"] & { tool_calls: boolean }
}
}
export async function get(baseURL: string, headers: RequestInit["headers"], existing: readonly ModelV2.Info[]) {
const response = await fetch(`${baseURL}/models`, {
headers,
signal: AbortSignal.timeout(5_000),
})
if (!response.ok) throw new Error(`Failed to fetch Copilot models: ${response.status}`)
const remote = new Map(
decodeResponse(await response.json()).data.flatMap((raw) => {
const model = Option.getOrUndefined(decodeModel(raw))
return model && usable(model) ? ([[model.id, model]] as const) : []
}),
)
const result = new Map(existing.map((model) => [model.id, model]))
// Keep aliases and local metadata, but only when their advertised API model
// still exists. A partial or malformed item cannot create a broken model.
for (const [id, model] of result) {
const current = remote.get(model.modelID)
if (!current) {
result.delete(id)
continue
}
result.set(id, build(id, current, baseURL, model))
}
for (const [id, model] of remote) {
const key = ModelV2.ID.make(id)
if (result.has(key)) continue
result.set(key, build(key, model, baseURL))
}
return result
}
function usable(model: RemoteModel): model is UsableModel {
return (
model.policy?.state !== "disabled" &&
model.capabilities.limits?.max_output_tokens !== undefined &&
model.capabilities.limits.max_prompt_tokens !== undefined &&
model.capabilities.supports.tool_calls !== undefined
)
}
function build(id: ModelV2.ID, remote: UsableModel, baseURL: string, previous?: ModelV2.Info) {
const messages = remote.supported_endpoints?.includes("/v1/messages") ?? false
const endpoint = messages
? "messages"
: remote.supported_endpoints?.includes("/responses")
? "responses"
: remote.supported_endpoints?.includes("/chat/completions")
? "chat"
: undefined
const image =
(remote.capabilities.supports.vision ?? false) ||
(remote.capabilities.limits.vision?.supported_media_types ?? []).some((item) => item.startsWith("image/"))
const prices = remote.billing?.token_prices
// Copilot reports AIC per billing batch; OpenCode stores USD per million tokens.
const usdPerMillion = prices && prices.batch_size > 0 ? 10_000 / prices.batch_size : 0
const version = remote.version.startsWith(`${remote.id}-`)
? remote.version.slice(remote.id.length + 1)
: remote.version
const released = previous?.time.released || Date.parse(version)
return ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.githubCopilot, id),
id,
modelID: ModelV2.ID.make(remote.id),
providerID: ProviderV2.ID.githubCopilot,
family: previous?.family ?? ModelV2.Family.make(remote.capabilities.family),
name: previous?.name ?? remote.name,
package: ProviderV2.aisdk(messages ? "@ai-sdk/anthropic" : "@ai-sdk/github-copilot"),
settings: ProviderV2.mergeOverlay(previous?.settings, {
baseURL: messages ? `${baseURL}/v1` : baseURL,
...(endpoint ? { endpoint } : {}),
}),
headers: previous?.headers,
body: previous?.body,
capabilities: {
tools: remote.capabilities.supports.tool_calls,
input: image ? ["text", "image"] : ["text"],
output: ["text"],
},
variants: variants(remote, messages),
time: { released: Number.isFinite(released) ? released : 0 },
cost: [
{
input: Money.USDPerMillionTokens.make((prices?.default.input_price ?? 0) * usdPerMillion),
output: Money.USDPerMillionTokens.make((prices?.default.output_price ?? 0) * usdPerMillion),
cache: {
read: Money.USDPerMillionTokens.make((prices?.default.cache_price ?? 0) * usdPerMillion),
write: Money.USDPerMillionTokens.zero,
},
},
],
status: "active",
enabled: remote.model_picker_enabled,
limit: {
context: remote.capabilities.limits.max_context_window_tokens ?? remote.capabilities.limits.max_prompt_tokens,
input: remote.capabilities.limits.max_prompt_tokens,
output: remote.capabilities.limits.max_output_tokens,
},
})
}
function variants(remote: UsableModel, messages: boolean): ModelV2.Info["variants"] {
const efforts = remote.capabilities.supports.reasoning_effort ?? []
if (!messages && efforts.length) {
return efforts.map((effort) => ({
id: ModelV2.VariantID.make(effort),
settings: {
reasoningEffort: effort,
reasoningSummary: "auto",
include: ["reasoning.encrypted_content"],
},
}))
}
if (efforts.length && remote.capabilities.supports.adaptive_thinking) {
return efforts.map((effort) => ({
id: ModelV2.VariantID.make(effort),
settings: {
thinking: {
type: "adaptive",
...(remote.id.includes("opus-4.7") ? { display: "summarized" } : {}),
},
effort,
},
}))
}
const max = remote.capabilities.supports.max_thinking_budget
if (max === undefined) return []
return [
{
id: ModelV2.VariantID.make("max"),
settings: { thinking: { type: "enabled", budgetTokens: max - 1 } },
},
{
id: ModelV2.VariantID.make("high"),
settings: { thinking: { type: "enabled", budgetTokens: Math.floor(max / 2) } },
},
]
}
@@ -1,7 +1,115 @@
import { Effect } from "effect"
import type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/v2/effect/integration"
import { Effect, Option, Schema, Semaphore, Stream } from "effect"
import { Catalog } from "../../catalog"
import { Credential } from "../../credential"
import { EventV2 } from "../../event"
import { CopilotModels } from "../../github-copilot/models"
import { InstallationVersion } from "../../installation/version"
import { Integration } from "../../integration"
import { ModelV2 } from "../../model"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { ProviderV2 } from "../../provider"
import type { PluginInternal } from "../internal"
const clientID = "Ov23li8tweQw6odWQebz"
const apiVersion = "2026-06-01"
const pollingSafetyMargin = 3000
const 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 JsonBody = Schema.UnknownFromJsonString
const decodeBody = Schema.decodeUnknownOption(JsonBody)
const oauth = {
integrationID: Integration.ID.make("github-copilot"),
method: {
id: methodID,
type: "oauth",
label: "Login with GitHub Copilot",
prompts: [
{
type: "select",
key: "deploymentType",
message: "Select GitHub deployment type",
options: [
{ label: "GitHub.com", value: "github.com", hint: "Public" },
{ label: "GitHub Enterprise", value: "enterprise", hint: "Data residency or self-hosted" },
],
},
{
type: "text",
key: "enterpriseUrl",
message: "Enter your GitHub Enterprise URL or domain",
placeholder: "company.ghe.com or https://company.ghe.com",
when: { key: "deploymentType", op: "eq", value: "enterprise" },
},
],
},
authorize: (inputs) =>
Effect.gen(function* () {
const enterprise = inputs.deploymentType === "enterprise"
if (enterprise && !inputs.enterpriseUrl) return yield* Effect.fail(new Error("Enterprise URL is required"))
const domain = enterprise ? normalizeDomain(inputs.enterpriseUrl ?? "") : "github.com"
const urls = oauthURLs(domain)
const device = yield* request(urls.device, {
method: "POST",
headers: headers(),
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(),
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) {
return Effect.succeed(
Credential.OAuth.make({
type: "oauth",
methodID,
refresh: token.access_token,
access: token.access_token,
expires: 0,
...(enterprise ? { metadata: { enterpriseUrl: domain } } : {}),
}),
)
}
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
function shouldUseResponses(modelID: string) {
// Copilot supports Responses for GPT-5 class models, except mini variants
@@ -14,19 +122,103 @@ function shouldUseResponses(modelID: string) {
export const GithubCopilotPlugin = define({
id: "opencode.provider.github-copilot",
effect: Effect.fn(function* (ctx) {
const catalog = yield* Catalog.Service
const events = yield* EventV2.Service
const loading = Semaphore.makeUnsafe(1)
const loaded: {
baseURL?: string
models?: Map<ModelV2.ID, ModelV2.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.catch(() => Effect.succeed(undefined)))
: undefined
if (credential?.type !== "oauth") {
loaded.baseURL = undefined
loaded.models = undefined
return
}
const enterprise = credential.metadata?.enterpriseUrl
loaded.baseURL = baseURL(typeof enterprise === "string" ? enterprise : undefined)
const provider = yield* catalog.provider.get(ProviderV2.ID.githubCopilot)
const existing = (yield* catalog.model.all()).filter((model) => model.providerID === ProviderV2.ID.githubCopilot)
loaded.models = yield* Effect.tryPromise({
try: () =>
CopilotModels.get(
loaded.baseURL ?? baseURL(),
{
...provider?.headers,
Authorization: `Bearer ${credential.refresh}`,
"User-Agent": `opencode/${InstallationVersion}`,
"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.update(oauth)
})
yield* ctx.catalog.transform((evt) => {
const item = evt.provider.get(ProviderV2.ID.githubCopilot)
if (!item || !item.models.has(ModelV2.ID.make("gpt-5-chat-latest"))) return
evt.model.update(item.provider.id, ModelV2.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
})
if (!item) return
if (loaded.models) {
for (const id of item.models.keys()) {
if (!loaded.models.has(ModelV2.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 if (loaded.baseURL) {
for (const id of item.models.keys()) {
evt.model.update(item.provider.id, id, (model) => {
model.settings = ProviderV2.mergeOverlay(model.settings, { baseURL: loaded.baseURL })
})
}
}
if (item.models.has(ModelV2.ID.make("gpt-5-chat-latest"))) {
evt.model.update(item.provider.id, ModelV2.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* events.subscribe(Integration.Event.ConnectionUpdated).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.package !== "@ai-sdk/github-copilot") return
if (evt.model.providerID !== ProviderV2.ID.githubCopilot) return
if (evt.package !== "@ai-sdk/github-copilot" && evt.package !== "@ai-sdk/anthropic") return
evt.options.fetch = copilotFetch(
typeof evt.options.apiKey === "string" ? evt.options.apiKey : undefined,
evt.options.fetch,
evt.package === "@ai-sdk/anthropic",
)
if (evt.package === "@ai-sdk/anthropic") {
evt.options.headers = {
...evt.options.headers,
"anthropic-beta": "interleaved-thinking-2025-05-14",
}
const mod = yield* Effect.promise(() => import("@ai-sdk/anthropic"))
evt.sdk = mod.createAnthropic(evt.options)
return
}
const mod = yield* Effect.promise(() => import("../../github-copilot/copilot-provider"))
evt.sdk = mod.createOpenaiCompatible(evt.options)
}),
@@ -52,4 +244,114 @@ export const GithubCopilotPlugin = define({
}),
)
}),
})
} 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"
}
function headers() {
return {
Accept: "application/json",
"Content-Type": "application/json",
"User-Agent": `opencode/${InstallationVersion}`,
}
}
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, anthropic: boolean): Fetch {
const send = upstream ?? fetch
return async (input, init) => {
const requestHeaders = new Headers(init?.headers)
if (token) {
requestHeaders.delete("authorization")
requestHeaders.delete("x-api-key")
requestHeaders.set("Authorization", `Bearer ${token}`)
}
requestHeaders.set("User-Agent", `opencode/${InstallationVersion}`)
requestHeaders.set("Openai-Intent", "conversation-edits")
requestHeaders.set("X-GitHub-Api-Version", apiVersion)
if (anthropic) requestHeaders.set("anthropic-beta", "interleaved-thinking-2025-05-14")
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
const metadata = requestMetadata(url, body)
requestHeaders.set("x-initiator", metadata.agent ? "agent" : "user")
if (metadata.vision) requestHeaders.set("Copilot-Vision-Request", "true")
return send(input, { ...init, headers: requestHeaders })
}
}
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)
}
@@ -0,0 +1,76 @@
import { expect, test } from "bun:test"
import { CopilotModels } from "@opencode-ai/core/github-copilot/models"
import { ModelV2 } from "@opencode-ai/core/model"
import { ProviderV2 } from "@opencode-ai/core/provider"
test("defensively syncs advertised Copilot models", async () => {
const server = Bun.serve({
port: 0,
fetch: () =>
Response.json({
data: [
{
model_picker_enabled: true,
id: "gpt-5",
name: "GPT-5 remote",
version: "gpt-5-2026-06-01",
supported_endpoints: ["/responses"],
billing: {
token_prices: {
batch_size: 0,
default: { input_price: 10, output_price: 20, cache_price: 5 },
},
},
capabilities: {
family: "gpt",
limits: {
max_context_window_tokens: 200000,
max_output_tokens: 16384,
max_prompt_tokens: 180000,
},
supports: { tool_calls: true, reasoning_effort: ["low", "high"] },
},
},
{
model_picker_enabled: false,
id: "utility",
name: "Utility",
version: "utility-2026-06-01",
capabilities: {
family: "utility",
limits: { max_output_tokens: 1000, max_prompt_tokens: 8000 },
supports: { tool_calls: false },
},
},
{ model_picker_enabled: true, id: "incomplete" },
],
}),
})
try {
const existing = ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.githubCopilot, ModelV2.ID.make("gpt-5")),
modelID: ModelV2.ID.make("gpt-5"),
name: "GPT-5 local",
})
const stale = ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.githubCopilot, ModelV2.ID.make("stale")),
modelID: ModelV2.ID.make("stale"),
})
const models = await CopilotModels.get(server.url.origin, {}, [existing, stale])
const model = models.get(ModelV2.ID.make("gpt-5"))
expect(model?.name).toBe("GPT-5 local")
expect(model?.settings).toMatchObject({ baseURL: server.url.origin, endpoint: "responses" })
expect(model?.cost[0]).toMatchObject({ input: 0, output: 0, cache: { read: 0, write: 0 } })
expect(model?.variants.map((variant) => variant.id)).toEqual([
ModelV2.VariantID.make("low"),
ModelV2.VariantID.make("high"),
])
expect(models.get(ModelV2.ID.make("utility"))?.enabled).toBe(false)
expect(models.has(ModelV2.ID.make("stale"))).toBe(false)
expect(models.has(ModelV2.ID.make("incomplete"))).toBe(false)
} finally {
await server.stop(true)
}
})
@@ -5,8 +5,9 @@ import { Catalog } from "@opencode-ai/core/catalog"
import { ModelV2 } from "@opencode-ai/core/model"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { PluginHost } from "@opencode-ai/core/plugin/host"
import { GithubCopilotPlugin } from "@opencode-ai/core/plugin/provider/github-copilot"
import { 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"
@@ -39,6 +40,46 @@ function fakeSelectorSdk(calls: string[]) {
}
describe("GithubCopilotPlugin", () => {
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("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 })
},
false,
)
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")
}),
)
it.effect("creates the bundled Copilot SDK for the GitHub Copilot package", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service