import { Data, Effect } from "effect"; import { fuzzyMatch } from "@earendil-works/pi-tui"; export interface ModelSummary { readonly provider: string; readonly id: string; readonly name: string | undefined; } export class ModelNotFoundError extends Data.TaggedError("ModelNotFoundError")<{ readonly query: string; /** Top fuzzy-ranked near-misses, capped at MAX_SUGGESTIONS — not the whole catalog. */ readonly suggestions: ReadonlyArray; }> {} export class ModelAmbiguousError extends Data.TaggedError("ModelAmbiguousError")<{ readonly query: string; /** The tied/strong matches. */ readonly candidates: ReadonlyArray; }> {} const MAX_SUGGESTIONS = 8; function providerAndId(model: ModelSummary): string { return `${model.provider}/${model.id}`; } function haystack(model: ModelSummary): string { return `${providerAndId(model)} ${model.name ?? ""}`.toLowerCase(); } function findExactMatches(query: string, available: ReadonlyArray): ModelSummary[] { const normalized = query.trim().toLowerCase(); return available.filter( (model) => providerAndId(model).toLowerCase() === normalized || model.id.toLowerCase() === normalized, ); } /** * Resolves a free-text model reference against the available models. * Exact `provider/id` (or bare `id`) matches win outright. Otherwise the query is * fuzzy-matched against every model's `provider/id` and display name; the best-scoring * match wins unless multiple models tie for best, in which case resolution is ambiguous. */ export function resolveModel( query: string, available: ReadonlyArray, ): Effect.Effect { return Effect.gen(function* () { const exact = findExactMatches(query, available); if (exact.length === 1) return exact[0]!; if (exact.length > 1) { return yield* Effect.fail(new ModelAmbiguousError({ query, candidates: exact })); } const scored = available .map((model) => ({ model, ...fuzzyMatch(query, haystack(model)) })) .sort((a, b) => a.score - b.score); const matching = scored.filter((entry) => entry.matches); if (matching.length === 0) { const suggestions = scored.slice(0, MAX_SUGGESTIONS).map((entry) => entry.model); return yield* Effect.fail(new ModelNotFoundError({ query, suggestions })); } const bestScore = matching[0]!.score; const winners = matching.filter((entry) => entry.score === bestScore).map((entry) => entry.model); if (winners.length === 1) return winners[0]!; return yield* Effect.fail(new ModelAmbiguousError({ query, candidates: winners })); }); } function formatList(models: ReadonlyArray): string { if (models.length === 0) return "(no models available)"; return models.map((model) => `- ${providerAndId(model)}${model.name ? ` (${model.name})` : ""}`).join("\n"); } export function formatModelNotFound(error: ModelNotFoundError): string { return `No model matches "${error.query}". Closest available models:\n${formatList(error.suggestions)}\n\nRetry switch_model with an exact provider/id from this list.`; } export function formatModelAmbiguous(error: ModelAmbiguousError): string { return `"${error.query}" matches multiple models:\n${formatList(error.candidates)}\n\nRetry switch_model with an exact provider/id from this list.`; }