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%
9.2 KB · 207 lines typescript
Raw Blame History
1import type { PolyModel } from "@/lib/ai/core/types";23/**4 * Model badges & lifecycle — pure functions over `PolyModel` (client- and server-safe).5 *6 * Everything is derived from real registry data: pricing quantiles across the models the7 * caller passes in, capability flags, limits, lifecycle metadata (`shutdownDate`, `firstSeenAt`)8 * and a few documented id heuristics (coding / open-weights / fast tiers). Nothing is invented.9 */10export const LONG_CONTEXT_TOKENS = 400_000;11export const NEW_MODEL_DAYS = 30;12export const RETIRING_DAYS = 90;13const DAY = 86_400_000;1415export const FAST_RE = /flash|mini|nano|haiku|fast|lite|luna|non-reasoning|instant|turbo|ministral|small|\b[89]b\b|gemma|gpt-oss-20b/i;16export const FRONTIER_RE = /opus|fable|mythos|gpt-5\.5|gpt-5\.6|gpt-6|\bpro\b|grok-4(\.\d+)?(?!-fast)|large|\bk3\b|deepseek-v4-pro|magistral-medium|sonnet-5/i;17export const CODING_RE = /codestral|coder|devstral|codex|\bcode\b|code-|k2\.7/i;18export const OPEN_WEIGHTS_RE = /llama|qwen|gemma|gpt-oss|mixtral|mistral-small|mistral-nemo|ministral|devstral|magistral-small|deepseek|kimi|\bk2\b|k2\.|\bk3\b|glm|command-|phi-|nemotron|olmo|falcon|granite|hermes|dbrx|jamba|minimax/i;1920export type SpeedTier = "fast" | "standard" | "frontier";2122export interface BadgeContext {23  now: number;24  /** Blended $/1M (input + output) at or below which a model counts as CHEAP. */25  cheapThreshold: number;26  /** Output $/1M at or below which a model qualifies for FAST by price alone. */27  fastOutputThreshold: number;28  /** Models seen in the last 30 days; false when the registry itself is younger than that (everything would be "new"). */29  newViaFirstSeen: boolean;30  count: number;31}3233export function quantile(sorted: number[], p: number): number {34  if (!sorted.length) return NaN;35  const idx = (sorted.length - 1) * p;36  const lo = Math.floor(idx);37  const hi = Math.ceil(idx);38  return sorted[lo] + (sorted[hi] - sorted[lo]) * (idx - lo);39}4041export function blendedPrice(m: PolyModel): number | null {42  const p = m.pricing;43  if (!p) return null;44  const i = p.inputPerMillion;45  const o = p.outputPerMillion;46  if (typeof i !== "number" && typeof o !== "number") return null;47  return (i ?? o ?? 0) + (o ?? i ?? 0);48}4950function meta<T = unknown>(m: PolyModel, key: string): T | undefined {51  return m.metadata?.[key] as T | undefined;52}5354function dateMs(v: unknown): number | null {55  if (typeof v !== "string" || !v) return null;56  const t = new Date(v).getTime();57  return Number.isFinite(t) ? t : null;58}5960export function firstSeenMs(m: PolyModel): number | null {61  return dateMs(meta(m, "firstSeenAt"));62}6364/** Provider-reported creation / release date when the provider exposes one. Never inferred. */65export function releaseDateMs(m: PolyModel): number | null {66  return dateMs(meta(m, "releaseDate")) ?? dateMs(meta(m, "createdAt"));67}6869export function buildBadgeContext(models: PolyModel[], now: number): BadgeContext {70  const blended = models.map(blendedPrice).filter((v): v is number => typeof v === "number" && v > 0).sort((a, b) => a - b);71  const outputs = models.map((m) => m.pricing?.outputPerMillion).filter((v): v is number => typeof v === "number" && v > 0).sort((a, b) => a - b);72  const cheapThreshold = blended.length >= 4 ? quantile(blended, 0.25) : 1.5;73  const fastOutputThreshold = outputs.length >= 4 ? Math.min(2, quantile(outputs, 0.35)) : 2;74  const recent = models.filter((m) => {75    const t = firstSeenMs(m);76    return t !== null && now - t <= NEW_MODEL_DAYS * DAY;77  }).length;78  const newViaFirstSeen = models.length === 0 || recent / models.length <= 0.5;79  return { now, cheapThreshold, fastOutputThreshold, newViaFirstSeen, count: models.length };80}8182function ctxOf(all: PolyModel[] | BadgeContext, now?: number): BadgeContext {83  return Array.isArray(all) ? buildBadgeContext(all, now ?? Date.now()) : all;84}8586export function isFastModel(m: PolyModel, ctx?: BadgeContext): boolean {87  const out = m.pricing?.outputPerMillion;88  const threshold = ctx?.fastOutputThreshold ?? 2;89  return FAST_RE.test(`${m.id} ${m.displayName}`) || (typeof out === "number" && out <= threshold);90}9192export function isFrontierModel(m: PolyModel): boolean {93  return FRONTIER_RE.test(`${m.id} ${m.displayName}`);94}9596export function speedTier(m: PolyModel, ctx?: BadgeContext): SpeedTier {97  if (isFastModel(m, ctx)) return "fast";98  if (isFrontierModel(m)) return "frontier";99  return "standard";100}101102export function isCodingModel(m: PolyModel): boolean {103  return meta<boolean>(m, "coding") === true || CODING_RE.test(`${m.id} ${m.displayName}`);104}105106export function isOpenWeightsModel(m: PolyModel): boolean {107  const flag = meta<boolean>(m, "openWeights") ?? meta<boolean>(m, "openSource");108  if (typeof flag === "boolean") return flag;109  return OPEN_WEIGHTS_RE.test(`${m.id} ${m.displayName} ${meta<string>(m, "vendor") ?? ""}`);110}111112export function isCheapModel(m: PolyModel, ctx: BadgeContext): boolean {113  const b = blendedPrice(m);114  return b !== null && b <= ctx.cheapThreshold;115}116117export function isLongContextModel(m: PolyModel): boolean {118  return (m.limits?.contextTokens ?? 0) >= LONG_CONTEXT_TOKENS;119}120121export function isNewModel(m: PolyModel, ctx: BadgeContext): boolean {122  const limit = NEW_MODEL_DAYS * DAY;123  if (ctx.newViaFirstSeen) {124    const seen = firstSeenMs(m);125    if (seen !== null && ctx.now - seen <= limit && ctx.now - seen >= 0) return true;126  }127  const released = releaseDateMs(m);128  return released !== null && ctx.now - released <= limit && ctx.now - released >= 0;129}130131export type ModelBadgeKind = "new" | "reasoning" | "vision" | "coding" | "fast" | "cheap" | "long-context";132133export interface ModelBadge {134  kind: ModelBadgeKind;135  label: string;136  title: string;137}138139const BADGE_LABEL: Record<ModelBadgeKind, { label: string; title: string }> = {140  new: { label: "NEW", title: `Added in the last ${NEW_MODEL_DAYS} days` },141  reasoning: { label: "REASONING", title: "Extended reasoning / thinking" },142  vision: { label: "VISION", title: "Understands images" },143  coding: { label: "CODING", title: "Tuned or documented for code" },144  fast: { label: "FAST", title: "Fast / low-latency tier" },145  cheap: { label: "CHEAP", title: "Bottom quartile of blended price across the registry" },146  "long-context": { label: "LONG CONTEXT", title: `${LONG_CONTEXT_TOKENS / 1000}K tokens or more` },147};148149/** Badges in display priority. Pass the full model list (or a prebuilt `BadgeContext`) so quantiles are real. */150export function deriveBadges(model: PolyModel, all: PolyModel[] | BadgeContext, now?: number): ModelBadge[] {151  const ctx = ctxOf(all, now);152  const out: ModelBadge[] = [];153  const push = (kind: ModelBadgeKind) => out.push({ kind, ...BADGE_LABEL[kind] });154  if (isNewModel(model, ctx)) push("new");155  if (model.capabilities.reasoning) push("reasoning");156  if (model.capabilities.vision) push("vision");157  if (isCodingModel(model)) push("coding");158  if (isFastModel(model, ctx)) push("fast");159  if (isCheapModel(model, ctx)) push("cheap");160  if (isLongContextModel(model)) push("long-context");161  return out;162}163164export type LifecycleStatus = "new" | "active" | "preview" | "deprecated" | "retiring" | "unavailable" | "unknown";165export type LifecycleTone = "success" | "info" | "warning" | "danger" | "default" | "accent" | "outline";166167export interface Lifecycle {168  status: LifecycleStatus;169  label: string;170  tone: LifecycleTone;171  detail?: string;172}173174export function shutdownDateOf(m: PolyModel): string | null {175  const v = meta(m, "shutdownDate") ?? meta(m, "expirationDate");176  return typeof v === "string" && v ? v : null;177}178179export function retiresSoon(shutdownDate: string | null | undefined, now: number, days = RETIRING_DAYS): boolean {180  const t = dateMs(shutdownDate);181  return t !== null && t - now < days * DAY;182}183184/**185 * One lifecycle badge per model, by priority: Retiring > Deprecated > Unavailable (no key) > Preview > New > Active.186 * `connected` is optional — omit it on public pages where the viewer has no keys.187 */188export function lifecycleStatus(m: PolyModel, opts: { connected?: boolean; ctx?: BadgeContext; now?: number } = {}): Lifecycle {189  const now = opts.ctx?.now ?? opts.now ?? Date.now();190  const shutdown = shutdownDateOf(m);191  if (shutdown && retiresSoon(shutdown, now) && m.status !== "deprecated") {192    const past = (dateMs(shutdown) ?? 0) < now;193    return { status: past ? "deprecated" : "retiring", label: past ? "Deprecated" : "Retiring", tone: past ? "danger" : "warning", detail: `${past ? "Shut down on" : "Shutdown scheduled for"} ${shutdown}` };194  }195  if (m.status === "deprecated") return { status: "deprecated", label: "Deprecated", tone: "danger", detail: shutdown ? `Shutdown ${shutdown}` : undefined };196  if (opts.connected === false) return { status: "unavailable", label: "Unavailable", tone: "outline", detail: "Connect a key for this provider to use it" };197  if (m.status === "preview") return { status: "preview", label: "Preview", tone: "info" };198  if (opts.ctx && isNewModel(m, opts.ctx)) return { status: "new", label: "New", tone: "accent" };199  if (m.status === "unknown") return { status: "unknown", label: "Unverified", tone: "outline", detail: "Listed by the provider but not yet documented" };200  return { status: "active", label: "Active", tone: "success" };201}202203export function sortWeightOf(m: PolyModel): number {204  const w = meta<number>(m, "sortWeight");205  return typeof w === "number" ? w : 0;206}207