import z from "zod" import fuzzysort from "fuzzysort" import { Config } from "../config/config" import { mapValues, mergeDeep, sortBy } from "remeda" import { NoSuchModelError, type Provider as SDK } from "ai" import { Log } from "../util/log" import { BunProc } from "../bun" import { Plugin } from "../plugin" import { ModelsDev } from "./models" import { NamedError } from "@opencode-ai/util/error" import { Auth } from "../auth" import { Env } from "../env" import { Instance } from "../project/instance" import { Flag } from "../flag/flag" import { iife } from "@/util/iife" // Direct imports for bundled providers import { createAmazonBedrock } from "@ai-sdk/amazon-bedrock" import { createAnthropic } from "@ai-sdk/anthropic" import { createAzure } from "@ai-sdk/azure" import { createGoogleGenerativeAI } from "@ai-sdk/google" import { createVertex } from "@ai-sdk/google-vertex" import { createVertexAnthropic } from "@ai-sdk/google-vertex/anthropic" import { createOpenAI } from "@ai-sdk/openai" import { createOpenAICompatible } from "@ai-sdk/openai-compatible" import { createOpenRouter, type LanguageModelV2 } from "@openrouter/ai-sdk-provider" import { createOpenaiCompatible as createGitHubCopilotOpenAICompatible } from "./sdk/openai-compatible/src" export namespace Provider { const log = Log.create({ service: "provider" }) const BUNDLED_PROVIDERS: Record SDK> = { "@ai-sdk/amazon-bedrock": createAmazonBedrock, "@ai-sdk/anthropic": createAnthropic, "@ai-sdk/azure": createAzure, "@ai-sdk/google": createGoogleGenerativeAI, "@ai-sdk/google-vertex": createVertex, "@ai-sdk/google-vertex/anthropic": createVertexAnthropic, "@ai-sdk/openai": createOpenAI, "@ai-sdk/openai-compatible": createOpenAICompatible, "@openrouter/ai-sdk-provider": createOpenRouter, // @ts-ignore (TODO: kill this code so we dont have to maintain it) "@ai-sdk/github-copilot": createGitHubCopilotOpenAICompatible, } type CustomModelLoader = (sdk: any, modelID: string, options?: Record) => Promise type CustomLoader = (provider: Info) => Promise<{ autoload: boolean getModel?: CustomModelLoader options?: Record }> const CUSTOM_LOADERS: Record = { async anthropic() { return { autoload: false, options: { headers: { "anthropic-beta": "claude-code-20250219,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14", }, }, } }, async opencode(input) { const hasKey = await (async () => { const env = Env.all() if (input.env.some((item) => env[item])) return true if (await Auth.get(input.id)) return true return false })() if (!hasKey) { for (const [key, value] of Object.entries(input.models)) { if (value.cost.input === 0) continue delete input.models[key] } } return { autoload: Object.keys(input.models).length > 0, options: hasKey ? {} : { apiKey: "public" }, } }, openai: async () => { return { autoload: false, async getModel(sdk: any, modelID: string, _options?: Record) { return sdk.responses(modelID) }, options: {}, } }, "github-copilot": async () => { return { autoload: false, async getModel(sdk: any, modelID: string, _options?: Record) { if (modelID.includes("codex")) { return sdk.responses(modelID) } return sdk.chat(modelID) }, options: {}, } }, "github-copilot-enterprise": async () => { return { autoload: false, async getModel(sdk: any, modelID: string, _options?: Record) { if (modelID.includes("codex")) { return sdk.responses(modelID) } return sdk.chat(modelID) }, options: {}, } }, azure: async () => { return { autoload: false, async getModel(sdk: any, modelID: string, options?: Record) { if (options?.["useCompletionUrls"]) { return sdk.chat(modelID) } else { return sdk.responses(modelID) } }, options: {}, } }, "azure-cognitive-services": async () => { const resourceName = Env.get("AZURE_COGNITIVE_SERVICES_RESOURCE_NAME") return { autoload: false, async getModel(sdk: any, modelID: string, options?: Record) { if (options?.["useCompletionUrls"]) { return sdk.chat(modelID) } else { return sdk.responses(modelID) } }, options: { baseURL: resourceName ? `https://${resourceName}.cognitiveservices.azure.com/openai` : undefined, }, } }, "amazon-bedrock": async () => { const [awsProfile, awsAccessKeyId, awsBearerToken, awsRegion] = await Promise.all([ Env.get("AWS_PROFILE"), Env.get("AWS_ACCESS_KEY_ID"), Env.get("AWS_BEARER_TOKEN_BEDROCK"), Env.get("AWS_REGION"), ]) if (!awsProfile && !awsAccessKeyId && !awsBearerToken) return { autoload: false } const region = awsRegion ?? "us-east-1" const { fromNodeProviderChain } = await import(await BunProc.install("@aws-sdk/credential-providers")) return { autoload: true, options: { region, credentialProvider: fromNodeProviderChain(), }, async getModel(sdk: any, modelID: string, _options?: Record) { // Skip region prefixing if model already has global prefix if (modelID.startsWith("global.")) { return sdk.languageModel(modelID) } let regionPrefix = region.split("-")[0] switch (regionPrefix) { case "us": { const modelRequiresPrefix = [ "nova-micro", "nova-lite", "nova-pro", "nova-premier", "claude", "deepseek", ].some((m) => modelID.includes(m)) const isGovCloud = region.startsWith("us-gov") if (modelRequiresPrefix && !isGovCloud) { modelID = `${regionPrefix}.${modelID}` } break } case "eu": { const regionRequiresPrefix = [ "eu-west-1", "eu-west-2", "eu-west-3", "eu-north-1", "eu-central-1", "eu-south-1", "eu-south-2", ].some((r) => region.includes(r)) const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "llama3", "pixtral"].some((m) => modelID.includes(m), ) if (regionRequiresPrefix && modelRequiresPrefix) { modelID = `${regionPrefix}.${modelID}` } break } case "ap": { const isAustraliaRegion = ["ap-southeast-2", "ap-southeast-4"].includes(region) if ( isAustraliaRegion && ["anthropic.claude-sonnet-4-5", "anthropic.claude-haiku"].some((m) => modelID.includes(m)) ) { regionPrefix = "au" modelID = `${regionPrefix}.${modelID}` } else { const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "nova-pro"].some((m) => modelID.includes(m), ) if (modelRequiresPrefix) { regionPrefix = "apac" modelID = `${regionPrefix}.${modelID}` } } break } } return sdk.languageModel(modelID) }, } }, openrouter: async () => { return { autoload: false, options: { headers: { "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode", }, }, } }, vercel: async () => { return { autoload: false, options: { headers: { "http-referer": "https://opencode.ai/", "x-title": "opencode", }, }, } }, "google-vertex": async () => { const project = Env.get("GOOGLE_CLOUD_PROJECT") ?? Env.get("GCP_PROJECT") ?? Env.get("GCLOUD_PROJECT") const location = Env.get("GOOGLE_CLOUD_LOCATION") ?? Env.get("VERTEX_LOCATION") ?? "us-east5" const autoload = Boolean(project) if (!autoload) return { autoload: false } return { autoload: true, options: { project, location, }, async getModel(sdk: any, modelID: string) { const id = String(modelID).trim() return sdk.languageModel(id) }, } }, "google-vertex-anthropic": async () => { const project = Env.get("GOOGLE_CLOUD_PROJECT") ?? Env.get("GCP_PROJECT") ?? Env.get("GCLOUD_PROJECT") const location = Env.get("GOOGLE_CLOUD_LOCATION") ?? Env.get("VERTEX_LOCATION") ?? "global" const autoload = Boolean(project) if (!autoload) return { autoload: false } return { autoload: true, options: { project, location, }, async getModel(sdk: any, modelID) { const id = String(modelID).trim() return sdk.languageModel(id) }, } }, "sap-ai-core": async () => { const auth = await Auth.get("sap-ai-core") const envServiceKey = iife(() => { const envAICoreServiceKey = Env.get("AICORE_SERVICE_KEY") if (envAICoreServiceKey) return envAICoreServiceKey if (auth?.type === "api") { Env.set("AICORE_SERVICE_KEY", auth.key) return auth.key } return undefined }) const deploymentId = Env.get("AICORE_DEPLOYMENT_ID") const resourceGroup = Env.get("AICORE_RESOURCE_GROUP") return { autoload: !!envServiceKey, options: envServiceKey ? { deploymentId, resourceGroup } : {}, async getModel(sdk: any, modelID: string) { return sdk(modelID) }, } }, zenmux: async () => { return { autoload: false, options: { headers: { "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode", }, }, } }, cerebras: async () => { return { autoload: false, options: { headers: { "X-Cerebras-3rd-Party-Integration": "opencode", }, }, } }, } export const Model = z .object({ id: z.string(), providerID: z.string(), api: z.object({ id: z.string(), url: z.string(), npm: z.string(), }), name: z.string(), family: z.string().optional(), capabilities: z.object({ temperature: z.boolean(), reasoning: z.boolean(), attachment: z.boolean(), toolcall: z.boolean(), input: z.object({ text: z.boolean(), audio: z.boolean(), image: z.boolean(), video: z.boolean(), pdf: z.boolean(), }), output: z.object({ text: z.boolean(), audio: z.boolean(), image: z.boolean(), video: z.boolean(), pdf: z.boolean(), }), interleaved: z.union([ z.boolean(), z.object({ field: z.enum(["reasoning_content", "reasoning_details"]), }), ]), }), cost: z.object({ input: z.number(), output: z.number(), cache: z.object({ read: z.number(), write: z.number(), }), experimentalOver200K: z .object({ input: z.number(), output: z.number(), cache: z.object({ read: z.number(), write: z.number(), }), }) .optional(), }), limit: z.object({ context: z.number(), output: z.number(), }), status: z.enum(["alpha", "beta", "deprecated", "active"]), options: z.record(z.string(), z.any()), headers: z.record(z.string(), z.string()), release_date: z.string(), }) .meta({ ref: "Model", }) export type Model = z.infer export const Info = z .object({ id: z.string(), name: z.string(), source: z.enum(["env", "config", "custom", "api"]), env: z.string().array(), key: z.string().optional(), options: z.record(z.string(), z.any()), models: z.record(z.string(), Model), }) .meta({ ref: "Provider", }) export type Info = z.infer function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model): Model { return { id: model.id, providerID: provider.id, name: model.name, family: model.family, api: { id: model.id, url: provider.api!, npm: model.provider?.npm ?? provider.npm ?? provider.id, }, status: model.status ?? "active", headers: model.headers ?? {}, options: model.options ?? {}, cost: { input: model.cost?.input ?? 0, output: model.cost?.output ?? 0, cache: { read: model.cost?.cache_read ?? 0, write: model.cost?.cache_write ?? 0, }, experimentalOver200K: model.cost?.context_over_200k ? { cache: { read: model.cost.context_over_200k.cache_read ?? 0, write: model.cost.context_over_200k.cache_write ?? 0, }, input: model.cost.context_over_200k.input, output: model.cost.context_over_200k.output, } : undefined, }, limit: { context: model.limit.context, output: model.limit.output, }, capabilities: { temperature: model.temperature, reasoning: model.reasoning, attachment: model.attachment, toolcall: model.tool_call, input: { text: model.modalities?.input?.includes("text") ?? false, audio: model.modalities?.input?.includes("audio") ?? false, image: model.modalities?.input?.includes("image") ?? false, video: model.modalities?.input?.includes("video") ?? false, pdf: model.modalities?.input?.includes("pdf") ?? false, }, output: { text: model.modalities?.output?.includes("text") ?? false, audio: model.modalities?.output?.includes("audio") ?? false, image: model.modalities?.output?.includes("image") ?? false, video: model.modalities?.output?.includes("video") ?? false, pdf: model.modalities?.output?.includes("pdf") ?? false, }, interleaved: model.interleaved ?? false, }, release_date: model.release_date, } } export function fromModelsDevProvider(provider: ModelsDev.Provider): Info { return { id: provider.id, source: "custom", name: provider.name, env: provider.env ?? [], options: {}, models: mapValues(provider.models, (model) => fromModelsDevModel(provider, model)), } } const state = Instance.state(async () => { using _ = log.time("state") const config = await Config.get() const modelsDev = await ModelsDev.get() const database = mapValues(modelsDev, fromModelsDevProvider) const disabled = new Set(config.disabled_providers ?? []) const enabled = config.enabled_providers ? new Set(config.enabled_providers) : null function isProviderAllowed(providerID: string): boolean { if (enabled && !enabled.has(providerID)) return false if (disabled.has(providerID)) return false return true } const providers: { [providerID: string]: Info } = {} const languages = new Map() const modelLoaders: { [providerID: string]: CustomModelLoader } = {} const sdk = new Map() log.info("init") const configProviders = Object.entries(config.provider ?? {}) // Add GitHub Copilot Enterprise provider that inherits from GitHub Copilot if (database["github-copilot"]) { const githubCopilot = database["github-copilot"] database["github-copilot-enterprise"] = { ...githubCopilot, id: "github-copilot-enterprise", name: "GitHub Copilot Enterprise", models: mapValues(githubCopilot.models, (model) => ({ ...model, providerID: "github-copilot-enterprise", })), } } function mergeProvider(providerID: string, provider: Partial) { const existing = providers[providerID] if (existing) { // @ts-expect-error providers[providerID] = mergeDeep(existing, provider) return } const match = database[providerID] if (!match) return // @ts-expect-error providers[providerID] = mergeDeep(match, provider) } // extend database from config for (const [providerID, provider] of configProviders) { const existing = database[providerID] const parsed: Info = { id: providerID, name: provider.name ?? existing?.name ?? providerID, env: provider.env ?? existing?.env ?? [], options: mergeDeep(existing?.options ?? {}, provider.options ?? {}), source: "config", models: existing?.models ?? {}, } for (const [modelID, model] of Object.entries(provider.models ?? {})) { const existingModel = parsed.models[model.id ?? modelID] const name = iife(() => { if (model.name) return model.name if (model.id && model.id !== modelID) return modelID return existingModel?.name ?? modelID }) const parsedModel: Model = { id: modelID, api: { id: model.id ?? existingModel?.api.id ?? modelID, npm: model.provider?.npm ?? provider.npm ?? existingModel?.api.npm ?? modelsDev[providerID]?.npm ?? providerID, url: provider?.api ?? existingModel?.api.url ?? modelsDev[providerID]?.api, }, status: model.status ?? existingModel?.status ?? "active", name, providerID, capabilities: { temperature: model.temperature ?? existingModel?.capabilities.temperature ?? false, reasoning: model.reasoning ?? existingModel?.capabilities.reasoning ?? false, attachment: model.attachment ?? existingModel?.capabilities.attachment ?? false, toolcall: model.tool_call ?? existingModel?.capabilities.toolcall ?? true, input: { text: model.modalities?.input?.includes("text") ?? existingModel?.capabilities.input.text ?? true, audio: model.modalities?.input?.includes("audio") ?? existingModel?.capabilities.input.audio ?? false, image: model.modalities?.input?.includes("image") ?? existingModel?.capabilities.input.image ?? false, video: model.modalities?.input?.includes("video") ?? existingModel?.capabilities.input.video ?? false, pdf: model.modalities?.input?.includes("pdf") ?? existingModel?.capabilities.input.pdf ?? false, }, output: { text: model.modalities?.output?.includes("text") ?? existingModel?.capabilities.output.text ?? true, audio: model.modalities?.output?.includes("audio") ?? existingModel?.capabilities.output.audio ?? false, image: model.modalities?.output?.includes("image") ?? existingModel?.capabilities.output.image ?? false, video: model.modalities?.output?.includes("video") ?? existingModel?.capabilities.output.video ?? false, pdf: model.modalities?.output?.includes("pdf") ?? existingModel?.capabilities.output.pdf ?? false, }, interleaved: model.interleaved ?? false, }, cost: { input: model?.cost?.input ?? existingModel?.cost?.input ?? 0, output: model?.cost?.output ?? existingModel?.cost?.output ?? 0, cache: { read: model?.cost?.cache_read ?? existingModel?.cost?.cache.read ?? 0, write: model?.cost?.cache_write ?? existingModel?.cost?.cache.write ?? 0, }, }, options: mergeDeep(existingModel?.options ?? {}, model.options ?? {}), limit: { context: model.limit?.context ?? existingModel?.limit?.context ?? 0, output: model.limit?.output ?? existingModel?.limit?.output ?? 0, }, headers: mergeDeep(existingModel?.headers ?? {}, model.headers ?? {}), family: model.family ?? existingModel?.family ?? "", release_date: model.release_date ?? existingModel?.release_date ?? "", } parsed.models[modelID] = parsedModel } database[providerID] = parsed } // load env const env = Env.all() for (const [providerID, provider] of Object.entries(database)) { if (disabled.has(providerID)) continue const apiKey = provider.env.map((item) => env[item]).find(Boolean) if (!apiKey) continue mergeProvider(providerID, { source: "env", key: provider.env.length === 1 ? apiKey : undefined, }) } // load apikeys for (const [providerID, provider] of Object.entries(await Auth.all())) { if (disabled.has(providerID)) continue if (provider.type === "api") { mergeProvider(providerID, { source: "api", key: provider.key, }) } } for (const plugin of await Plugin.list()) { if (!plugin.auth) continue const providerID = plugin.auth.provider if (disabled.has(providerID)) continue // For github-copilot plugin, check if auth exists for either github-copilot or github-copilot-enterprise let hasAuth = false const auth = await Auth.get(providerID) if (auth) hasAuth = true // Special handling for github-copilot: also check for enterprise auth if (providerID === "github-copilot" && !hasAuth) { const enterpriseAuth = await Auth.get("github-copilot-enterprise") if (enterpriseAuth) hasAuth = true } if (!hasAuth) continue if (!plugin.auth.loader) continue // Load for the main provider if auth exists if (auth) { const options = await plugin.auth.loader(() => Auth.get(providerID) as any, database[plugin.auth.provider]) mergeProvider(plugin.auth.provider, { source: "custom", options: options, }) } // If this is github-copilot plugin, also register for github-copilot-enterprise if auth exists if (providerID === "github-copilot") { const enterpriseProviderID = "github-copilot-enterprise" if (!disabled.has(enterpriseProviderID)) { const enterpriseAuth = await Auth.get(enterpriseProviderID) if (enterpriseAuth) { const enterpriseOptions = await plugin.auth.loader( () => Auth.get(enterpriseProviderID) as any, database[enterpriseProviderID], ) mergeProvider(enterpriseProviderID, { source: "custom", options: enterpriseOptions, }) } } } } for (const [providerID, fn] of Object.entries(CUSTOM_LOADERS)) { if (disabled.has(providerID)) continue const result = await fn(database[providerID]) if (result && (result.autoload || providers[providerID])) { if (result.getModel) modelLoaders[providerID] = result.getModel mergeProvider(providerID, { source: "custom", options: result.options, }) } } // load config for (const [providerID, provider] of configProviders) { const partial: Partial = { source: "config" } if (provider.env) partial.env = provider.env if (provider.name) partial.name = provider.name if (provider.options) partial.options = provider.options mergeProvider(providerID, partial) } for (const [providerID, provider] of Object.entries(providers)) { if (!isProviderAllowed(providerID)) { delete providers[providerID] continue } if (providerID === "github-copilot" || providerID === "github-copilot-enterprise") { provider.models = mapValues(provider.models, (model) => ({ ...model, api: { ...model.api, npm: "@ai-sdk/github-copilot", }, })) } const configProvider = config.provider?.[providerID] for (const [modelID, model] of Object.entries(provider.models)) { model.api.id = model.api.id ?? model.id ?? modelID if (modelID === "gpt-5-chat-latest" || (providerID === "openrouter" && modelID === "openai/gpt-5-chat")) delete provider.models[modelID] if (model.status === "alpha" && !Flag.OPENCODE_ENABLE_EXPERIMENTAL_MODELS) delete provider.models[modelID] if ( (configProvider?.blacklist && configProvider.blacklist.includes(modelID)) || (configProvider?.whitelist && !configProvider.whitelist.includes(modelID)) ) delete provider.models[modelID] } if (Object.keys(provider.models).length === 0) { delete providers[providerID] continue } log.info("found", { providerID }) } return { models: languages, providers, sdk, modelLoaders, } }) export async function list() { return state().then((state) => state.providers) } async function getSDK(model: Model) { try { using _ = log.time("getSDK", { providerID: model.providerID, }) const s = await state() const provider = s.providers[model.providerID] const options = { ...provider.options } if (model.api.npm.includes("@ai-sdk/openai-compatible") && options["includeUsage"] !== false) { options["includeUsage"] = true } if (!options["baseURL"]) options["baseURL"] = model.api.url if (options["apiKey"] === undefined && provider.key) options["apiKey"] = provider.key if (model.headers) options["headers"] = { ...options["headers"], ...model.headers, } const key = Bun.hash.xxHash32(JSON.stringify({ npm: model.api.npm, options })) const existing = s.sdk.get(key) if (existing) return existing const customFetch = options["fetch"] options["fetch"] = async (input: any, init?: BunFetchRequestInit) => { // Preserve custom fetch if it exists, wrap it with timeout logic const fetchFn = customFetch ?? fetch const opts = init ?? {} if (options["timeout"] !== undefined && options["timeout"] !== null) { const signals: AbortSignal[] = [] if (opts.signal) signals.push(opts.signal) if (options["timeout"] !== false) signals.push(AbortSignal.timeout(options["timeout"])) const combined = signals.length > 1 ? AbortSignal.any(signals) : signals[0] opts.signal = combined } return fetchFn(input, { ...opts, // @ts-ignore see here: https://github.com/oven-sh/bun/issues/16682 timeout: false, }) } // Special case: google-vertex-anthropic uses a subpath import const bundledKey = model.providerID === "google-vertex-anthropic" ? "@ai-sdk/google-vertex/anthropic" : model.api.npm const bundledFn = BUNDLED_PROVIDERS[bundledKey] if (bundledFn) { log.info("using bundled provider", { providerID: model.providerID, pkg: bundledKey }) const loaded = bundledFn({ name: model.providerID, ...options, }) s.sdk.set(key, loaded) return loaded as SDK } let installedPath: string if (!model.api.npm.startsWith("file://")) { installedPath = await BunProc.install(model.api.npm, "latest") } else { log.info("loading local provider", { pkg: model.api.npm }) installedPath = model.api.npm } const mod = await import(installedPath) const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!] const loaded = fn({ name: model.providerID, ...options, }) s.sdk.set(key, loaded) return loaded as SDK } catch (e) { throw new InitError({ providerID: model.providerID }, { cause: e }) } } export async function getProvider(providerID: string) { return state().then((s) => s.providers[providerID]) } export async function getModel(providerID: string, modelID: string) { const s = await state() const provider = s.providers[providerID] if (!provider) { const availableProviders = Object.keys(s.providers) const matches = fuzzysort.go(providerID, availableProviders, { limit: 3, threshold: -10000 }) const suggestions = matches.map((m) => m.target) throw new ModelNotFoundError({ providerID, modelID, suggestions }) } const info = provider.models[modelID] if (!info) { const availableModels = Object.keys(provider.models) const matches = fuzzysort.go(modelID, availableModels, { limit: 3, threshold: -10000 }) const suggestions = matches.map((m) => m.target) throw new ModelNotFoundError({ providerID, modelID, suggestions }) } return info } export async function getLanguage(model: Model): Promise { const s = await state() const key = `${model.providerID}/${model.id}` if (s.models.has(key)) return s.models.get(key)! const provider = s.providers[model.providerID] const sdk = await getSDK(model) try { const language = s.modelLoaders[model.providerID] ? await s.modelLoaders[model.providerID](sdk, model.api.id, provider.options) : sdk.languageModel(model.api.id) s.models.set(key, language) return language } catch (e) { if (e instanceof NoSuchModelError) throw new ModelNotFoundError( { modelID: model.id, providerID: model.providerID, }, { cause: e }, ) throw e } } export async function closest(providerID: string, query: string[]) { const s = await state() const provider = s.providers[providerID] if (!provider) return undefined for (const item of query) { for (const modelID of Object.keys(provider.models)) { if (modelID.includes(item)) return { providerID, modelID, } } } } export async function getSmallModel(providerID: string) { const cfg = await Config.get() if (cfg.small_model) { const parsed = parseModel(cfg.small_model) return getModel(parsed.providerID, parsed.modelID) } const provider = await state().then((state) => state.providers[providerID]) if (provider) { let priority = [ "claude-haiku-4-5", "claude-haiku-4.5", "3-5-haiku", "3.5-haiku", "gemini-2.5-flash", "gpt-5-nano", ] // claude-haiku-4.5 is considered a premium model in github copilot, we shouldn't use premium requests for title gen if (providerID === "github-copilot") { priority = priority.filter((m) => m !== "claude-haiku-4.5") } if (providerID.startsWith("opencode")) { priority = ["gpt-5-nano"] } for (const item of priority) { for (const model of Object.keys(provider.models)) { if (model.includes(item)) return getModel(providerID, model) } } } // Check if opencode provider is available before using it const opencodeProvider = await state().then((state) => state.providers["opencode"]) if (opencodeProvider && opencodeProvider.models["gpt-5-nano"]) { return getModel("opencode", "gpt-5-nano") } return undefined } const priority = ["gpt-5", "claude-sonnet-4", "big-pickle", "gemini-3-pro"] export function sort(models: Model[]) { return sortBy( models, [(model) => priority.findIndex((filter) => model.id.includes(filter)), "desc"], [(model) => (model.id.includes("latest") ? 0 : 1), "asc"], [(model) => model.id, "desc"], ) } export async function defaultModel() { const cfg = await Config.get() if (cfg.model) return parseModel(cfg.model) const provider = await list() .then((val) => Object.values(val)) .then((x) => x.find((p) => !cfg.provider || Object.keys(cfg.provider).includes(p.id))) if (!provider) throw new Error("no providers found") const [model] = sort(Object.values(provider.models)) if (!model) throw new Error("no models found") return { providerID: provider.id, modelID: model.id, } } export function parseModel(model: string) { const [providerID, ...rest] = model.split("/") return { providerID: providerID, modelID: rest.join("/"), } } export const ModelNotFoundError = NamedError.create( "ProviderModelNotFoundError", z.object({ providerID: z.string(), modelID: z.string(), suggestions: z.array(z.string()).optional(), }), ) export const InitError = NamedError.create( "ProviderInitError", z.object({ providerID: z.string(), }), ) }