// Shared LLM model runtime + selection for every model-backed feature (URL prefill, roast // evaluation). One ModelRuntime per process; which model actually runs is decided here: // the admin-chosen setting (app_settings.llm_model, passed in as `preferred`) wins, then the // LLM_MODEL / PREFILL_MODEL env overrides, then the first available configured model. import { ModelRuntime } from "@earendil-works/pi-coding-agent"; let modelRuntimePromise = null; export async function getModelRuntime() { if (!modelRuntimePromise) modelRuntimePromise = ModelRuntime.create(); return modelRuntimePromise; } export const modelKey = (model) => `${model.provider}:${model.id}`; /** Every configured model, as stable "provider:id" keys for the admin picker. */ export async function listAvailableModels() { const runtime = await getModelRuntime(); return (await runtime.getAvailable()).map((model) => ({ key: modelKey(model), name: model.name ?? model.id, provider: model.provider, })); } /** @param {string|null} preferred "provider:id" from the admin setting, or empty for auto. */ export async function pickModel(preferred = null) { const runtime = await getModelRuntime(); for (const candidate of [preferred, process.env.LLM_MODEL, process.env.PREFILL_MODEL]) { if (!candidate) continue; const sep = candidate.indexOf(":"); if (sep === -1) continue; const model = runtime.getModel(candidate.slice(0, sep), candidate.slice(sep + 1)); if (model) return model; } return (await runtime.getAvailable())[0]; } /** Consistent error for "the server has no usable model" across features. */ export function noModelError() { const err = new Error("No LLM model is configured on the server."); err.code = "no_model"; return err; }