59 lines
3.8 KiB
TypeScript
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;
|
|
}
|