SPB Git
4.8 KB · 65 lines typescript
1import type { AIModel } from "./types";2import { blendedPrice } from "./cost";34export type RoutingMode = "manual" | "fast" | "quality" | "economy" | "auto";5export type TaskClass = "code" | "math" | "reasoning" | "vision" | "longctx" | "french" | "json" | "tools" | "general";67export interface RoutingDecision { mode: RoutingMode; requested?: string; selected: string; reason: string; task?: TaskClass; candidates: string[] }89/** Cheap heuristic task classifier (no model call). */10export function classifyTask(text: string, opts: { hasImages?: boolean; approxTokens?: number; wantsTools?: boolean } = {}): TaskClass {11  if (opts.hasImages) return "vision";12  if ((opts.approxTokens ?? 0) > 20_000) return "longctx";13  const t = text.toLowerCase();14  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";15  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";16  if (/\b(json|schema|structured|table of|csv)\b/.test(t)) return "json";17  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";18  if (/[àâçéèêëîïôûùüÿœ]/.test(t) || /\b(le|la|les|des|une|est|pour|avec|dans)\b/.test(t)) return "french";19  return "general";20}2122const q = (m: AIModel) => m.benchmark?.qualityScore ?? -1;23const lat = (m: AIModel) => m.benchmark?.p50LatencyMs ?? 1e9;24const price = (m: AIModel) => blendedPrice(m) ?? 1e9;25const chatOnly = (ms: AIModel[]) => ms.filter(m => m.available && !m.hidden && ["chat", "search"].includes(m.kind) && m.benchmark?.qualityScore != null);2627export function rankFor(mode: Exclude<RoutingMode, "manual">, models: AIModel[], task: TaskClass = "general"): AIModel[] {28  const pool = chatOnly(models);29  const passes = (m: AIModel, t: string) => m.benchmark?.perTest?.[t] === "pass";30  switch (mode) {31    case "fast": return pool.filter(m => q(m) >= 89).sort((a, b) => lat(a) - lat(b));32    case "economy": return pool.filter(m => q(m) >= 89).sort((a, b) => price(a) - price(b) || q(b) - q(a));33    case "quality": return pool.sort((a, b) => q(b) - q(a) || (blendedPrice(b) ?? -1) - (blendedPrice(a) ?? -1) || lat(a) - lat(b));34    case "auto": {35      const testFor: Record<TaskClass, string | null> = { code: "code", math: "math", reasoning: "reasoning", vision: "vision", longctx: "longctx", french: "french", json: "json", tools: "tools", general: null };36      const t = testFor[task];37      const eligible = t ? pool.filter(m => passes(m, t)) : pool;38      // balance: quality first, then price-per-quality, then latency; prefer mid-tier (exclude the very top price tier unless task is reasoning/math/code)39      const heavy = task === "reasoning" || task === "math" || task === "code" || task === "longctx";40      return eligible.sort((a, b) => q(b) - q(a) || (heavy ? price(b) - price(a) : price(a) - price(b)) || lat(a) - lat(b)).filter(m => heavy || price(m) <= 12);41    }42  }43}4445export function route(mode: RoutingMode, models: AIModel[], requested: string | undefined, text: string, opts: Parameters<typeof classifyTask>[1] = {}): RoutingDecision {46  if (mode === "manual") {47    if (!requested) throw new Error("manual routing requires a model");48    return { mode, requested, selected: requested, reason: "Modèle choisi explicitement.", candidates: [requested] };49  }50  const task = classifyTask(text, opts);51  let ranked = rankFor(mode, models, task);52  if (opts.hasImages) ranked = ranked.filter(m => m.capabilities.vision === true);53  if (opts.wantsTools) ranked = ranked.filter(m => m.capabilities.tools === true);54  if (!ranked.length) ranked = rankFor("quality", models).filter(m => (!opts.hasImages || m.capabilities.vision === true) && (!opts.wantsTools || m.capabilities.tools === true));55  const sel = ranked[0];56  if (!sel) throw new Error("No available model satisfies the request");57  const why: Record<string, string> = {58    fast: `Mode rapide : latence médiane la plus basse (${sel.benchmark?.p50LatencyMs} ms) parmi les modèles ≥ 89 % au benchmark.`,59    economy: `Mode économique : prix mixte le plus bas (${blendedPrice(sel)?.toFixed(2)} $/M) parmi les modèles ≥ 89 %.`,60    quality: `Mode qualité : score ${sel.benchmark?.qualityScore} %, palier le plus élevé du fournisseur.`,61    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).`,62  };63  return { mode, requested, selected: sel.id, reason: why[mode], task, candidates: ranked.slice(0, 5).map(m => m.id) };64}65
Files · 7d61c36e2fa5e0d19df2d38831a92de25c4fc209
Menu

Theme