/** Configured providers retain pi-ai catalog metadata and API compatibility flags. */ import {createModels, createProvider} from '@earendil-works/pi-ai/models'; import {getBuiltinModel} from '@earendil-works/pi-ai/providers/all'; import {anthropicMessagesApi} from '@earendil-works/pi-ai/api/anthropic-messages.lazy'; import {openAIResponsesApi} from '@earendil-works/pi-ai/api/openai-responses.lazy'; import {openAICompletionsApi} from '@earendil-works/pi-ai/api/openai-completions.lazy'; export const CONFIG = '/etc/sausage/config.yaml'; const adapters = {'anthropic-messages': anthropicMessagesApi, 'openai-responses': openAIResponsesApi, 'openai-chat-completions': openAICompletionsApi}; export function resolveModel(provider: string, settings: any, entry: any, catalog: any) { const override = typeof entry === 'string' ? {id:entry} : entry; const id = override.id; const known = catalog(settings.catalog ?? provider,id); const metadata = {context_window:known?.contextWindow, max_tokens:known?.maxTokens, reasoning:known?.reasoning, ...override}; const cost = {...known?.cost}; const costFields = {input:'input',output:'output',cache_read:'cacheRead',cache_write:'cacheWrite'}; for (const [key, field] of Object.entries(costFields)) if (override.cost?.[key] !== undefined) cost[field] = override.cost[key]; const missing = ['context_window','max_tokens','reasoning'].filter(key=>metadata[key] === undefined); for (const [key, field] of Object.entries(costFields)) if (cost[field] === undefined) missing.push(`cost.${key}`); if (missing.length) throw new Error(`${CONFIG}: ${provider}/${id} lacks ${missing.join(', ')}`); const extra = Object.keys(override).filter(key=>!['id','context_window','max_tokens','reasoning','cost'].includes(key)); const extraCost = Object.keys(override.cost ?? {}).filter(key=>!['input','output','cache_read','cache_write'].includes(key)); if (extra.length || extraCost.length) throw new Error(`${CONFIG}: invalid overrides for ${provider}/${id}: ${[...extra,...extraCost].join(', ')}`); return {...known, type:'chat', id, name:known?.name ?? id, provider, api:settings.type === 'openai-chat-completions' ? 'openai-completions' : settings.type, baseUrl:settings.base_url, contextWindow:metadata.context_window, maxTokens:metadata.max_tokens, reasoning:metadata.reasoning, input:known?.input ?? ['text'], cost}; } export function modelCatalog() { return getBuiltinModel; } export async function configuredModels(readProviders:()=>Promise, catalog:any) { const models:any = createModels(); let installed = new Set(); models.refreshConfig = async () => { const settings = await readProviders(); const next = new Set(Object.keys(settings)); for (const name of installed) if (!next.has(name)) models.deleteProvider(name); for (const [name, value] of Object.entries(settings) as [string,any][]) { const adapter = adapters[value.type]; if (!adapter) throw new Error(`${CONFIG}: unknown API type for provider ${name}: ${value.type}`); const entries = (value.models ?? []).map((entry:any)=>resolveModel(name,value,entry,catalog)); models.setProvider(createProvider({id:name,name,baseUrl:value.base_url,models:entries,api:adapter(), auth:{apiKey:{name:`${name} API key`,resolve:async()=>{ const current = (await readProviders())[name]; if (!current?.api_key || !current.base_url) throw new Error(`${CONFIG}: set providers.${name}.api_key and base_url before a model request`); return {auth:{apiKey:current.api_key,baseUrl:current.base_url},source:CONFIG}; }}}})); } installed = next; }; await models.refreshConfig(); return models; }