import z from "zod" import { App } from "../app/app" import { Config } from "../config/config" import { mapValues, sortBy } from "remeda" import { NoSuchModelError, type LanguageModel, type Provider as SDK } from "ai" import { Log } from "../util/log" import path from "path" import { Global } from "../global" import { BunProc } from "../bun" import { BashTool } from "../tool/bash" import { EditTool } from "../tool/edit" import { WebFetchTool } from "../tool/webfetch" import { GlobTool } from "../tool/glob" import { GrepTool } from "../tool/grep" import { ListTool } from "../tool/ls" import { LspDiagnosticTool } from "../tool/lsp-diagnostics" import { LspHoverTool } from "../tool/lsp-hover" import { PatchTool } from "../tool/patch" import { ReadTool } from "../tool/read" import type { Tool } from "../tool/tool" import { WriteTool } from "../tool/write" import { TodoReadTool, TodoWriteTool } from "../tool/todo" import { AuthAnthropic } from "../auth/anthropic" import { ModelsDev } from "./models" export namespace Provider { const log = Log.create({ service: "provider" }) export const Model = z .object({ id: z.string(), name: z.string().optional(), attachment: z.boolean(), reasoning: z.boolean().optional(), cost: z.object({ input: z.number(), inputCached: z.number(), output: z.number(), outputCached: z.number(), }), limit: z.object({ context: z.number(), output: z.number(), }), }) .openapi({ ref: "Provider.Model", }) export type Model = z.output export const Info = z .object({ id: z.string(), name: z.string(), models: z.record(z.string(), Model), }) .openapi({ ref: "Provider.Info", }) export type Info = z.output type Autodetector = (provider: Info) => Promise | false> function env(...keys: string[]): Autodetector { return async () => { for (const key of keys) { if (process.env[key]) return {} } return false } } const AUTODETECT: Record< string, (provider: Info) => Promise | false> > = { anthropic: async () => { const result = await AuthAnthropic.load() if (result) return { apiKey: "", headers: { authorization: `Bearer ${result.accessToken}`, "anthropic-beta": "oauth-2025-04-20", }, } return env("ANTHROPIC_API_KEY") }, google: env("GOOGLE_GENERATIVE_AI_API_KEY"), openai: env("OPENAI_API_KEY"), } const state = App.state("provider", async () => { log.info("loading config") const config = await Config.get() log.info("loading providers") const database: Record = await ModelsDev.get() const providers: { [providerID: string]: { info: Provider.Info options: Record } } = {} const models = new Map() const sdk = new Map() log.info("loading") for (const [providerID, fn] of Object.entries(AUTODETECT)) { const provider = database[providerID] if (!provider) continue const options = await fn(provider) if (!options) continue providers[providerID] = { info: provider, options, } } for (const [providerID, options] of Object.entries(config.provider ?? {})) { const existing = providers[providerID] if (existing) { existing.options = { ...existing.options, ...options, } continue } providers[providerID] = { info: database[providerID], options, } } return { models, providers, sdk, } }) export async function active() { return state().then((state) => mapValues(state.providers, (item) => item.info), ) } async function getSDK(providerID: string) { const s = await state() if (s.sdk.has(providerID)) return s.sdk.get(providerID)! const dir = path.join( Global.Path.cache, `node_modules`, `@ai-sdk`, providerID, ) if (!(await Bun.file(path.join(dir, "package.json")).exists())) { log.info("installing", { providerID, }) BunProc.run(["add", `@ai-sdk/${providerID}@alpha`], { cwd: Global.Path.cache, }) } const mod = await import(path.join(dir)) const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!] const loaded = fn(s.providers[providerID]?.options) s.sdk.set(providerID, loaded) return loaded as SDK } export async function getModel(providerID: string, modelID: string) { const key = `${providerID}/${modelID}` const s = await state() if (s.models.has(key)) return s.models.get(key)! log.info("loading", { providerID, modelID, }) const provider = s.providers[providerID] if (!provider) throw new ModelNotFoundError(modelID) const info = provider.info.models[modelID] if (!info) throw new ModelNotFoundError(modelID) const sdk = await getSDK(providerID) if (!sdk) throw new ModelNotFoundError(modelID) try { const language = sdk.languageModel(modelID) log.info("found", { providerID, modelID }) s.models.set(key, { info, language, }) return { info, language, } } catch (e) { if (e instanceof NoSuchModelError) throw new ModelNotFoundError(modelID) throw e } } const priority = ["claude-sonnet-4", "gemini-2.5-pro-preview", "codex-mini"] export function sort(models: Model[]) { return sortBy( models, [(model) => priority.indexOf(model.id), "desc"], [(model) => (model.id.includes("latest") ? 0 : 1), "asc"], [(model) => model.id, "desc"], ) } export async function defaultModel() { const [provider] = await active().then((val) => Object.values(val)) 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, } } const TOOLS = [ BashTool, EditTool, WebFetchTool, GlobTool, GrepTool, ListTool, LspDiagnosticTool, LspHoverTool, PatchTool, ReadTool, EditTool, // MultiEditTool, WriteTool, TodoWriteTool, TodoReadTool, ] const TOOL_MAPPING: Record = { anthropic: TOOLS.filter((t) => t.id !== "opencode.patch"), openai: TOOLS, google: TOOLS, } export async function tools(providerID: string) { const cfg = await Config.get() if (cfg.tool?.provider?.[providerID]) return cfg.tool.provider[providerID].map( (id) => TOOLS.find((t) => t.id === id)!, ) return TOOL_MAPPING[providerID] ?? TOOLS } class ModelNotFoundError extends Error { constructor(public readonly model: string) { super() } } }