SPB Git
5.4 KB · 73 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      const pool2 = eligible.filter(m => !/chat-latest$/.test(m.id)); // ChatGPT-mirror alias: manual use only41      if (heavy) {42        // strongest tier under a price cap (blended ≤ $12/M): quality, then higher list price, then latency43        const capped = pool2.filter(m => price(m) <= 12);44        return (capped.length ? capped : pool2).sort((a, b) => q(b) - q(a) || price(b) - price(a) || lat(a) - lat(b));45      }46      // light tasks: full-score models at ≤ $4/M blended, fastest first; fall back to any eligible by quality then price47      const light = pool2.filter(m => q(m) >= 100 && price(m) <= 4).sort((a, b) => lat(a) - lat(b));48      return light.length ? light : pool2.sort((a, b) => q(b) - q(a) || price(a) - price(b) || lat(a) - lat(b));49    }50  }51}5253export function route(mode: RoutingMode, models: AIModel[], requested: string | undefined, text: string, opts: Parameters<typeof classifyTask>[1] = {}): RoutingDecision {54  if (mode === "manual") {55    if (!requested) throw new Error("manual routing requires a model");56    return { mode, requested, selected: requested, reason: "Modèle choisi explicitement.", candidates: [requested] };57  }58  const task = classifyTask(text, opts);59  let ranked = rankFor(mode, models, task);60  if (opts.hasImages) ranked = ranked.filter(m => m.capabilities.vision === true);61  if (opts.wantsTools) ranked = ranked.filter(m => m.capabilities.tools === true);62  if (!ranked.length) ranked = rankFor("quality", models).filter(m => (!opts.hasImages || m.capabilities.vision === true) && (!opts.wantsTools || m.capabilities.tools === true));63  const sel = ranked[0];64  if (!sel) throw new Error("No available model satisfies the request");65  const why: Record<string, string> = {66    fast: `Mode rapide : latence médiane la plus basse (${sel.benchmark?.p50LatencyMs} ms) parmi les modèles ≥ 89 % au benchmark.`,67    economy: `Mode économique : prix mixte le plus bas (${blendedPrice(sel)?.toFixed(2)} $/M) parmi les modèles ≥ 89 %.`,68    quality: `Mode qualité : score ${sel.benchmark?.qualityScore} %, palier le plus élevé du fournisseur.`,69    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).`,70  };71  return { mode, requested, selected: sel.id, reason: why[mode], task, candidates: ranked.slice(0, 5).map(m => m.id) };72}73
Files · main
Menu

Theme