SPB Git forge

spb/polyllm

Public
15commits 1branches 0releases
2.2 MBsize
maindefault branch
13 days agolast push
TypeScript 97.4% SQL 1% JavaScript 0.9% CSS 0.6%
4.4 KB · 83 lines typescript
Raw Blame History
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