Files
sausage/host/js/models.ts
T

59 lines
3.8 KiB
TypeScript

/** Configured providers with models.dev metadata and pi-ai API adapters. */
import {createModels, createProvider} from '@earendil-works/pi-ai/models';
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]?.models?.[id];
const metadata = {context_window:known?.limit?.context, max_tokens:known?.limit?.output,
reasoning:known?.reasoning, cost:known?.cost, ...override};
const cost = {...known?.cost, ...override.cost};
const missing = ['context_window','max_tokens','reasoning'].filter(key=>metadata[key] === undefined);
for (const key of ['input','output','cache_read','cache_write']) if (cost[key] === 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 {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?.modalities?.input?.filter((type:string)=>['text','image'].includes(type)) ?? ['text'],
cost:{input:cost.input, output:cost.output, cacheRead:cost.cache_read, cacheWrite:cost.cache_write}};
}
export async function modelCatalog() {
let updated;
try {
const response = await fetch('https://models.dev/api.json',{signal:AbortSignal.timeout(5000)});
if (response.ok) updated = await response.json();
} catch { /* Cached metadata and the packaged snapshot also work offline. */ }
return Deno.core.ops.op_model_catalog(updated ?? null);
}
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;
}