import type { AIModel } from "./types"; import { blendedPrice } from "./cost"; export type RoutingMode = "manual" | "fast" | "quality" | "economy" | "auto"; export type TaskClass = "code" | "math" | "reasoning" | "vision" | "longctx" | "french" | "json" | "tools" | "general"; export interface RoutingDecision { mode: RoutingMode; requested?: string; selected: string; reason: string; task?: TaskClass; candidates: string[] } /** Cheap heuristic task classifier (no model call). */ export function classifyTask(text: string, opts: { hasImages?: boolean; approxTokens?: number; wantsTools?: boolean } = {}): TaskClass { if (opts.hasImages) return "vision"; if ((opts.approxTokens ?? 0) > 20_000) return "longctx"; const t = text.toLowerCase(); if (/```|\b(function|class|def |import |const |let |var |#include|sql|regex|typescript|python|javascript|rust|bash|script|bug|stack ?trace|compile|refactor|unit test)\b/.test(t)) return "code"; if (/\b(prove|theorem|integral|derivative|equation|solve for|probability|matrix|eigen|calcul|démontre|intégrale|dérivée|équation|résous)\b|\d+\s*[\+\-\*\/\^]\s*\d+/.test(t)) return "math"; if (/\b(json|schema|structured|table of|csv)\b/.test(t)) return "json"; if (/\b(why|explain|analyse|analyze|compare|strategy|plan|évalue|pourquoi|explique|analyse|stratégie|réfléchis)\b/.test(t) && t.length > 200) return "reasoning"; if (/[àâçéèêëîïôûùüÿœ]/.test(t) || /\b(le|la|les|des|une|est|pour|avec|dans)\b/.test(t)) return "french"; return "general"; } const q = (m: AIModel) => m.benchmark?.qualityScore ?? -1; const lat = (m: AIModel) => m.benchmark?.p50LatencyMs ?? 1e9; const price = (m: AIModel) => blendedPrice(m) ?? 1e9; const chatOnly = (ms: AIModel[]) => ms.filter(m => m.available && !m.hidden && ["chat", "search"].includes(m.kind) && m.benchmark?.qualityScore != null); export function rankFor(mode: Exclude, models: AIModel[], task: TaskClass = "general"): AIModel[] { const pool = chatOnly(models); const passes = (m: AIModel, t: string) => m.benchmark?.perTest?.[t] === "pass"; switch (mode) { case "fast": return pool.filter(m => q(m) >= 89).sort((a, b) => lat(a) - lat(b)); case "economy": return pool.filter(m => q(m) >= 89).sort((a, b) => price(a) - price(b) || q(b) - q(a)); case "quality": return pool.sort((a, b) => q(b) - q(a) || (blendedPrice(b) ?? -1) - (blendedPrice(a) ?? -1) || lat(a) - lat(b)); case "auto": { const testFor: Record = { code: "code", math: "math", reasoning: "reasoning", vision: "vision", longctx: "longctx", french: "french", json: "json", tools: "tools", general: null }; const t = testFor[task]; const eligible = t ? pool.filter(m => passes(m, t)) : pool; // balance: quality first, then price-per-quality, then latency; prefer mid-tier (exclude the very top price tier unless task is reasoning/math/code) const heavy = task === "reasoning" || task === "math" || task === "code" || task === "longctx"; const pool2 = eligible.filter(m => !/chat-latest$/.test(m.id)); // ChatGPT-mirror alias: manual use only if (heavy) { // strongest tier under a price cap (blended ≤ $12/M): quality, then higher list price, then latency const capped = pool2.filter(m => price(m) <= 12); return (capped.length ? capped : pool2).sort((a, b) => q(b) - q(a) || price(b) - price(a) || lat(a) - lat(b)); } // light tasks: full-score models at ≤ $4/M blended, fastest first; fall back to any eligible by quality then price const light = pool2.filter(m => q(m) >= 100 && price(m) <= 4).sort((a, b) => lat(a) - lat(b)); return light.length ? light : pool2.sort((a, b) => q(b) - q(a) || price(a) - price(b) || lat(a) - lat(b)); } } } export function route(mode: RoutingMode, models: AIModel[], requested: string | undefined, text: string, opts: Parameters[1] = {}): RoutingDecision { if (mode === "manual") { if (!requested) throw new Error("manual routing requires a model"); return { mode, requested, selected: requested, reason: "Modèle choisi explicitement.", candidates: [requested] }; } const task = classifyTask(text, opts); let ranked = rankFor(mode, models, task); if (opts.hasImages) ranked = ranked.filter(m => m.capabilities.vision === true); if (opts.wantsTools) ranked = ranked.filter(m => m.capabilities.tools === true); if (!ranked.length) ranked = rankFor("quality", models).filter(m => (!opts.hasImages || m.capabilities.vision === true) && (!opts.wantsTools || m.capabilities.tools === true)); const sel = ranked[0]; if (!sel) throw new Error("No available model satisfies the request"); const why: Record = { fast: `Mode rapide : latence médiane la plus basse (${sel.benchmark?.p50LatencyMs} ms) parmi les modèles ≥ 89 % au benchmark.`, economy: `Mode économique : prix mixte le plus bas (${blendedPrice(sel)?.toFixed(2)} $/M) parmi les modèles ≥ 89 %.`, quality: `Mode qualité : score ${sel.benchmark?.qualityScore} %, palier le plus élevé du fournisseur.`, auto: `Mode automatique : tâche détectée « ${task} », meilleur compromis qualité/prix/latence (${sel.benchmark?.qualityScore} %, ${blendedPrice(sel)?.toFixed(2)} $/M, ${sel.benchmark?.p50LatencyMs} ms).`, }; return { mode, requested, selected: sel.id, reason: why[mode], task, candidates: ranked.slice(0, 5).map(m => m.id) }; }