57 lines
3.6 KiB
TypeScript
57 lines
3.6 KiB
TypeScript
/** 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<any>, catalog:any) {
|
|
const models:any = createModels();
|
|
let installed = new Set<string>();
|
|
models.refreshConfig = async () => {
|
|
const settings = await readProviders();
|
|
const next = new Set<string>(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;
|
|
}
|