import type { ModelStatus, ProviderId } from "@/lib/ai/core/types"; /** * Compact, public-safe model summary for the marketing site (`GET /api/public/models`). * Nothing user-specific, nothing secret: just the registry facts a visitor can see anyway * on /models. */ export interface PublicModelSummary { key: string; displayName: string; provider: ProviderId; contextTokens: number | null; inputPerMillion: number | null; outputPerMillion: number | null; status: ModelStatus; /** ISO date the registry first saw the model. */ firstSeenAt: string; } export interface CurationInput { key: string; displayName: string; provider: ProviderId; contextTokens?: number | null; inputPerMillion?: number | null; outputPerMillion?: number | null; status: ModelStatus; sortWeight: number; firstSeenAt: Date; hidden?: boolean; } export const PUBLIC_MODELS_MAX = 40; /** A model is "new" for the strip when the registry first saw it within this window. */ export const NEW_WINDOW_DAYS = 30; /** * Curated ordering for the live strip: newest releases first, then flagships (registry * `sortWeight`), interleaved per provider so nine providers show up in the first screen * instead of one provider's whole catalog. Pure and deterministic — unit tested. */ export function curatePublicModels(rows: CurationInput[], now: Date, max = PUBLIC_MODELS_MAX): PublicModelSummary[] { const cutoff = now.getTime() - NEW_WINDOW_DAYS * 86_400_000; const eligible = rows.filter((r) => !r.hidden && (r.status === "active" || r.status === "preview")); const ranked = [...eligible].sort((a, b) => { const aNew = a.firstSeenAt.getTime() >= cutoff ? 1 : 0; const bNew = b.firstSeenAt.getTime() >= cutoff ? 1 : 0; if (aNew !== bNew) return bNew - aNew; if (a.sortWeight !== b.sortWeight) return b.sortWeight - a.sortWeight; const t = b.firstSeenAt.getTime() - a.firstSeenAt.getTime(); if (t !== 0) return t; return a.displayName.localeCompare(b.displayName); }); // Round-robin across providers, preserving each provider's own ranking. const queues = new Map(); for (const r of ranked) { const q = queues.get(r.provider) ?? []; q.push(r); queues.set(r.provider, q); } const out: CurationInput[] = []; while (out.length < max && queues.size) { for (const [provider, q] of [...queues.entries()]) { const next = q.shift(); if (next) out.push(next); if (!q.length) queues.delete(provider); if (out.length >= max) break; } } return out.map((r) => ({ key: r.key, displayName: r.displayName, provider: r.provider, contextTokens: r.contextTokens ?? null, inputPerMillion: r.inputPerMillion ?? null, outputPerMillion: r.outputPerMillion ?? null, status: r.status, firstSeenAt: r.firstSeenAt.toISOString(), })); } /** `1M`, `400K`, `128K` — compact context window. */ export function formatContext(tokens: number | null | undefined): string { if (!tokens) return "—"; if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(tokens % 1_000_000 === 0 ? 0 : 1)}M`; if (tokens >= 1_000) return `${Math.round(tokens / 1_000)}K`; return String(tokens); } /** `$1.25 / $10` — per-million input / output price, compact. */ export function formatPricePair(input: number | null | undefined, output: number | null | undefined): string | null { if (input == null && output == null) return null; const f = (n: number | null | undefined) => (n == null ? "—" : n === 0 ? "free" : Number.isInteger(n) ? `$${n}` : `$${n.toFixed(2)}`); return `${f(input)} / ${f(output)}`; }