import type { Model } from "@earendil-works/pi-ai"; import type { ResolvedModelRef } from "./template-conditionals.js"; const PREFERRED_PROVIDERS = ["openai-codex", "anthropic", "github-copilot", "openrouter"]; export interface SelectedModelCandidate { model: Model; alreadyActive: boolean; } export interface RegistryLike { find(provider: string, modelId: string): Model | undefined; getAll(): Model[]; getAvailable(): Model[]; isUsingOAuth(model: Model): boolean; hasConfiguredAuth?: (model: Model) => boolean; getApiKeyAndHeaders?: (model: Model) => Promise<{ ok: boolean; apiKey?: string; headers?: Record; error?: string; }>; } function isSameModel(a: Model, b: Model): boolean { return a.provider === b.provider && a.id === b.id; } function modelSpecMatches(modelSpec: string, model: Model): boolean { const slashIndex = modelSpec.indexOf("/"); if (slashIndex !== -1) { const provider = modelSpec.slice(0, slashIndex); const modelId = modelSpec.slice(slashIndex + 1); return provider === model.provider && modelId === model.id; } return modelSpec === model.id; } function orderMatchesByProviderPreference(models: Model[]): Model[] { const prioritized: Model[] = []; const seen = new Set(); for (const provider of PREFERRED_PROVIDERS) { for (const model of models) { const key = `${model.provider}/${model.id}`; if (model.provider === provider && !seen.has(key)) { prioritized.push(model); seen.add(key); } } } for (const model of models) { const key = `${model.provider}/${model.id}`; if (!seen.has(key)) { prioritized.push(model); seen.add(key); } } return prioritized; } function getModelCandidates(modelSpec: string, registry: Pick): Model[] { const slashIndex = modelSpec.indexOf("/"); if (slashIndex !== -1) { const provider = modelSpec.slice(0, slashIndex); const modelId = modelSpec.slice(slashIndex + 1); if (!provider || !modelId) return []; if (modelId.split("/").some((segment) => segment.length === 0)) return []; const model = registry.find(provider, modelId); return model ? [model] : []; } const allMatches = registry.getAll().filter((model) => model.id === modelSpec); if (allMatches.length <= 1) return allMatches; return orderMatchesByProviderPreference(allMatches); } async function hasUsableAuth(model: Model, registry: RegistryLike): Promise { const availableMatch = registry.getAvailable().some((candidate) => isSameModel(candidate, model)); if (availableMatch) return true; if (!registry.isUsingOAuth(model)) return false; if (registry.hasConfiguredAuth) return registry.hasConfiguredAuth(model); if (registry.getApiKeyAndHeaders) { const auth = await registry.getApiKeyAndHeaders(model); return auth.ok; } return false; } export async function selectModelCandidate( modelSpecs: string[], currentModel: Model | undefined, registry: RegistryLike, ): Promise { if (currentModel && modelSpecs.some((spec) => modelSpecMatches(spec, currentModel))) { return { model: currentModel, alreadyActive: true }; } for (const spec of modelSpecs) { for (const model of getModelCandidates(spec, registry)) { if (await hasUsableAuth(model, registry)) { return { model, alreadyActive: false }; } } } return undefined; } export function getResolvedModelRef(model: Model): ResolvedModelRef { return { provider: model.provider, id: model.id }; }