import type { PolyModel } from "@/lib/ai/core/types"; /** * Model badges & lifecycle — pure functions over `PolyModel` (client- and server-safe). * * Everything is derived from real registry data: pricing quantiles across the models the * caller passes in, capability flags, limits, lifecycle metadata (`shutdownDate`, `firstSeenAt`) * and a few documented id heuristics (coding / open-weights / fast tiers). Nothing is invented. */ export const LONG_CONTEXT_TOKENS = 400_000; export const NEW_MODEL_DAYS = 30; export const RETIRING_DAYS = 90; const DAY = 86_400_000; export 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; export 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; export const CODING_RE = /codestral|coder|devstral|codex|\bcode\b|code-|k2\.7/i; export 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; export type SpeedTier = "fast" | "standard" | "frontier"; export interface BadgeContext { now: number; /** Blended $/1M (input + output) at or below which a model counts as CHEAP. */ cheapThreshold: number; /** Output $/1M at or below which a model qualifies for FAST by price alone. */ fastOutputThreshold: number; /** Models seen in the last 30 days; false when the registry itself is younger than that (everything would be "new"). */ newViaFirstSeen: boolean; count: number; } export function quantile(sorted: number[], p: number): number { if (!sorted.length) return NaN; const idx = (sorted.length - 1) * p; const lo = Math.floor(idx); const hi = Math.ceil(idx); return sorted[lo] + (sorted[hi] - sorted[lo]) * (idx - lo); } export function blendedPrice(m: PolyModel): number | null { const p = m.pricing; if (!p) return null; const i = p.inputPerMillion; const o = p.outputPerMillion; if (typeof i !== "number" && typeof o !== "number") return null; return (i ?? o ?? 0) + (o ?? i ?? 0); } function meta(m: PolyModel, key: string): T | undefined { return m.metadata?.[key] as T | undefined; } function dateMs(v: unknown): number | null { if (typeof v !== "string" || !v) return null; const t = new Date(v).getTime(); return Number.isFinite(t) ? t : null; } export function firstSeenMs(m: PolyModel): number | null { return dateMs(meta(m, "firstSeenAt")); } /** Provider-reported creation / release date when the provider exposes one. Never inferred. */ export function releaseDateMs(m: PolyModel): number | null { return dateMs(meta(m, "releaseDate")) ?? dateMs(meta(m, "createdAt")); } export function buildBadgeContext(models: PolyModel[], now: number): BadgeContext { const blended = models.map(blendedPrice).filter((v): v is number => typeof v === "number" && v > 0).sort((a, b) => a - b); const outputs = models.map((m) => m.pricing?.outputPerMillion).filter((v): v is number => typeof v === "number" && v > 0).sort((a, b) => a - b); const cheapThreshold = blended.length >= 4 ? quantile(blended, 0.25) : 1.5; const fastOutputThreshold = outputs.length >= 4 ? Math.min(2, quantile(outputs, 0.35)) : 2; const recent = models.filter((m) => { const t = firstSeenMs(m); return t !== null && now - t <= NEW_MODEL_DAYS * DAY; }).length; const newViaFirstSeen = models.length === 0 || recent / models.length <= 0.5; return { now, cheapThreshold, fastOutputThreshold, newViaFirstSeen, count: models.length }; } function ctxOf(all: PolyModel[] | BadgeContext, now?: number): BadgeContext { return Array.isArray(all) ? buildBadgeContext(all, now ?? Date.now()) : all; } export function isFastModel(m: PolyModel, ctx?: BadgeContext): boolean { const out = m.pricing?.outputPerMillion; const threshold = ctx?.fastOutputThreshold ?? 2; return FAST_RE.test(`${m.id} ${m.displayName}`) || (typeof out === "number" && out <= threshold); } export function isFrontierModel(m: PolyModel): boolean { return FRONTIER_RE.test(`${m.id} ${m.displayName}`); } export function speedTier(m: PolyModel, ctx?: BadgeContext): SpeedTier { if (isFastModel(m, ctx)) return "fast"; if (isFrontierModel(m)) return "frontier"; return "standard"; } export function isCodingModel(m: PolyModel): boolean { return meta(m, "coding") === true || CODING_RE.test(`${m.id} ${m.displayName}`); } export function isOpenWeightsModel(m: PolyModel): boolean { const flag = meta(m, "openWeights") ?? meta(m, "openSource"); if (typeof flag === "boolean") return flag; return OPEN_WEIGHTS_RE.test(`${m.id} ${m.displayName} ${meta(m, "vendor") ?? ""}`); } export function isCheapModel(m: PolyModel, ctx: BadgeContext): boolean { const b = blendedPrice(m); return b !== null && b <= ctx.cheapThreshold; } export function isLongContextModel(m: PolyModel): boolean { return (m.limits?.contextTokens ?? 0) >= LONG_CONTEXT_TOKENS; } export function isNewModel(m: PolyModel, ctx: BadgeContext): boolean { const limit = NEW_MODEL_DAYS * DAY; if (ctx.newViaFirstSeen) { const seen = firstSeenMs(m); if (seen !== null && ctx.now - seen <= limit && ctx.now - seen >= 0) return true; } const released = releaseDateMs(m); return released !== null && ctx.now - released <= limit && ctx.now - released >= 0; } export type ModelBadgeKind = "new" | "reasoning" | "vision" | "coding" | "fast" | "cheap" | "long-context"; export interface ModelBadge { kind: ModelBadgeKind; label: string; title: string; } const BADGE_LABEL: Record = { new: { label: "NEW", title: `Added in the last ${NEW_MODEL_DAYS} days` }, reasoning: { label: "REASONING", title: "Extended reasoning / thinking" }, vision: { label: "VISION", title: "Understands images" }, coding: { label: "CODING", title: "Tuned or documented for code" }, fast: { label: "FAST", title: "Fast / low-latency tier" }, cheap: { label: "CHEAP", title: "Bottom quartile of blended price across the registry" }, "long-context": { label: "LONG CONTEXT", title: `${LONG_CONTEXT_TOKENS / 1000}K tokens or more` }, }; /** Badges in display priority. Pass the full model list (or a prebuilt `BadgeContext`) so quantiles are real. */ export function deriveBadges(model: PolyModel, all: PolyModel[] | BadgeContext, now?: number): ModelBadge[] { const ctx = ctxOf(all, now); const out: ModelBadge[] = []; const push = (kind: ModelBadgeKind) => out.push({ kind, ...BADGE_LABEL[kind] }); if (isNewModel(model, ctx)) push("new"); if (model.capabilities.reasoning) push("reasoning"); if (model.capabilities.vision) push("vision"); if (isCodingModel(model)) push("coding"); if (isFastModel(model, ctx)) push("fast"); if (isCheapModel(model, ctx)) push("cheap"); if (isLongContextModel(model)) push("long-context"); return out; } export type LifecycleStatus = "new" | "active" | "preview" | "deprecated" | "retiring" | "unavailable" | "unknown"; export type LifecycleTone = "success" | "info" | "warning" | "danger" | "default" | "accent" | "outline"; export interface Lifecycle { status: LifecycleStatus; label: string; tone: LifecycleTone; detail?: string; } export function shutdownDateOf(m: PolyModel): string | null { const v = meta(m, "shutdownDate") ?? meta(m, "expirationDate"); return typeof v === "string" && v ? v : null; } export function retiresSoon(shutdownDate: string | null | undefined, now: number, days = RETIRING_DAYS): boolean { const t = dateMs(shutdownDate); return t !== null && t - now < days * DAY; } /** * One lifecycle badge per model, by priority: Retiring > Deprecated > Unavailable (no key) > Preview > New > Active. * `connected` is optional — omit it on public pages where the viewer has no keys. */ export function lifecycleStatus(m: PolyModel, opts: { connected?: boolean; ctx?: BadgeContext; now?: number } = {}): Lifecycle { const now = opts.ctx?.now ?? opts.now ?? Date.now(); const shutdown = shutdownDateOf(m); if (shutdown && retiresSoon(shutdown, now) && m.status !== "deprecated") { const past = (dateMs(shutdown) ?? 0) < now; return { status: past ? "deprecated" : "retiring", label: past ? "Deprecated" : "Retiring", tone: past ? "danger" : "warning", detail: `${past ? "Shut down on" : "Shutdown scheduled for"} ${shutdown}` }; } if (m.status === "deprecated") return { status: "deprecated", label: "Deprecated", tone: "danger", detail: shutdown ? `Shutdown ${shutdown}` : undefined }; if (opts.connected === false) return { status: "unavailable", label: "Unavailable", tone: "outline", detail: "Connect a key for this provider to use it" }; if (m.status === "preview") return { status: "preview", label: "Preview", tone: "info" }; if (opts.ctx && isNewModel(m, opts.ctx)) return { status: "new", label: "New", tone: "accent" }; if (m.status === "unknown") return { status: "unknown", label: "Unverified", tone: "outline", detail: "Listed by the provider but not yet documented" }; return { status: "active", label: "Active", tone: "success" }; } export function sortWeightOf(m: PolyModel): number { const w = meta(m, "sortWeight"); return typeof w === "number" ? w : 0; }