TypeScript 97.4%
SQL 1%
JavaScript 0.9%
CSS 0.6%
1import type { PolyModel } from "@/lib/ai/core/types";2import { analyzePrompt, routeModels, type RouterMode } from "@/lib/client/router";3import { blendedPrice, firstSeenMs, isCodingModel, isFastModel, isFrontierModel, isNewModel, isOpenWeightsModel, releaseDateMs, sortWeightOf, type BadgeContext } from "./badges";45/** Sections of the model picker (in display order). */6export type SectionKey = "favorites" | "recent" | "recommended" | "fast" | "reasoning" | "cheapest" | "context" | "vision" | "coding" | "open-weights" | "new";78export interface SectionMeta {9 key: SectionKey;10 label: string;11 hint: string;12}1314export const SECTION_META: SectionMeta[] = [15 { key: "favorites", label: "Favorites", hint: "Starred by you" },16 { key: "recent", label: "Recently used", hint: "Your last picks" },17 { key: "recommended", label: "Recommended", hint: "Smart Router picks for this prompt" },18 { key: "fast", label: "Fast", hint: "Low-latency tiers" },19 { key: "reasoning", label: "Best reasoning", hint: "Extended thinking" },20 { key: "cheapest", label: "Cheapest", hint: "Lowest blended price" },21 { key: "context", label: "Largest context", hint: "Biggest windows" },22 { key: "vision", label: "Vision", hint: "Understand images" },23 { key: "coding", label: "Coding", hint: "Tuned for code" },24 { key: "open-weights", label: "Open source", hint: "Open-weights models" },25 { key: "new", label: "New models", hint: "Added in the last 30 days" },26];2728export interface Section extends SectionMeta {29 /** First `perSection` items. */30 items: PolyModel[];31 /** Every match (for "Show all"). */32 all: PolyModel[];33 /** Total before `perSection` truncation. */34 total: number;35}3637export interface SectionOptions {38 favorites: Set<string>;39 recents: string[];40 ctx: BadgeContext;41 mode?: RouterMode;42 /** Current composer draft — drives "Recommended". */43 draft?: string;44 perSection?: number;45}4647const GENERIC_PROMPT = "Help me think through a question and write a clear, well-structured answer.";4849const byWeight = (a: PolyModel, b: PolyModel) => sortWeightOf(b) - sortWeightOf(a) || a.displayName.localeCompare(b.displayName);5051export function recommendedModels(models: PolyModel[], opts: { favorites?: Set<string>; mode?: RouterMode; draft?: string; limit?: number }) {52 const analysis = analyzePrompt(opts.draft?.trim() || GENERIC_PROMPT);53 const route = routeModels(models, analysis, opts.mode ?? "balanced", { favorites: opts.favorites });54 const list = [route.recommended, ...route.alternatives].filter((c): c is NonNullable<typeof c> => Boolean(c));55 return { route, models: list.slice(0, opts.limit ?? 3).map((c) => c.model) };56}5758/** Build every non-empty section. `models` must already be filtered to what the user can pick. */59export function buildSelectorSections(models: PolyModel[], opts: SectionOptions): Section[] {60 const per = opts.perSection ?? 5;61 const live = models.filter((m) => m.status !== "deprecated");62 const byKey = new Map(models.map((m) => [m.key, m]));6364 const lists: Record<SectionKey, PolyModel[]> = {65 favorites: models.filter((m) => opts.favorites.has(m.key)).sort(byWeight),66 recent: opts.recents.map((k) => byKey.get(k)).filter((m): m is PolyModel => Boolean(m)),67 recommended: recommendedModels(live, { favorites: opts.favorites, mode: opts.mode, draft: opts.draft, limit: 3 }).models,68 fast: live.filter((m) => isFastModel(m, opts.ctx)).sort((a, b) => (a.pricing?.outputPerMillion ?? 99) - (b.pricing?.outputPerMillion ?? 99) || byWeight(a, b)),69 reasoning: live.filter((m) => m.capabilities.reasoning).sort((a, b) => Number(isFrontierModel(b)) - Number(isFrontierModel(a)) || byWeight(a, b)),70 cheapest: live.filter((m) => blendedPrice(m) !== null).sort((a, b) => (blendedPrice(a) ?? 0) - (blendedPrice(b) ?? 0) || byWeight(a, b)),71 context: live.filter((m) => (m.limits?.contextTokens ?? 0) > 0).sort((a, b) => (b.limits?.contextTokens ?? 0) - (a.limits?.contextTokens ?? 0) || byWeight(a, b)),72 vision: live.filter((m) => m.capabilities.vision).sort(byWeight),73 coding: live.filter(isCodingModel).sort(byWeight),74 "open-weights": live.filter(isOpenWeightsModel).sort(byWeight),75 new: live.filter((m) => isNewModel(m, opts.ctx)).sort((a, b) => (firstSeenMs(b) ?? releaseDateMs(b) ?? 0) - (firstSeenMs(a) ?? releaseDateMs(a) ?? 0)),76 };7778 return SECTION_META.map((meta) => {79 const all = lists[meta.key];80 return { ...meta, items: all.slice(0, per), all, total: all.length };81 }).filter((s) => s.total > 0);82}83