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 & { 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) } }, }, ] }