TypeScript 97.4%
SQL 1%
JavaScript 0.9%
CSS 0.6%
1import type { ModelCapabilities, PolyModel } from "@/lib/ai/core/types";2import { estimateCost } from "@/lib/ai/core/pricing";34/**5 * Savings opportunities — pure math over aggregated usage per model + registry pricing.6 *7 * For every model that actually cost money in the range, we look for a *cheaper sibling on the same8 * provider* that could have handled the same traffic: it must not be deprecated, must offer every9 * capability the expensive model has among {vision, tools, structuredOutput, reasoning, files}, and10 * must fit the average prompt in its context window. We then re-price the exact same token volume11 * (input, cached input, output) with the sibling's list prices.12 *13 * Which sibling? The most capable one that still saves at least `minRatio` (default 40 %) — i.e. the14 * next tier down (Opus → Sonnet, GPT-5.5 → GPT-5.4 mini), not blindly the cheapest nano model.15 * Same-family siblings are preferred when the registry exposes a family.16 */1718export interface ModelUsageAgg {19 modelKey: string;20 provider: string;21 requests: number;22 inputTokens: number;23 outputTokens: number;24 cachedTokens: number;25 reasoningTokens: number;26 costUsd: number;27}2829export interface SavingsOpportunity {30 fromModelKey: string;31 fromLabel: string;32 toModelKey: string;33 toLabel: string;34 provider: string;35 requests: number;36 /** Cost of the real traffic re-priced with the expensive model's list prices (apples-to-apples). */37 currentCostUsd: number;38 /** Same traffic priced with the sibling. */39 alternativeCostUsd: number;40 savingsUsd: number;41 savingsPct: number;42 /** Human sentence for the card. */43 headline: string;44 /** Why the sibling qualifies. */45 rationale: string;46}4748const MATCHED_CAPS: (keyof ModelCapabilities)[] = ["vision", "tools", "structuredOutput", "reasoning", "files"];4950const FAST_TIER_RE = /flash|mini|nano|haiku|fast|lite|instant|turbo|small|ministral|sonnet|medium|8b|9b|gemma|gpt-oss-20b/i;5152export function coversCapabilities(candidate: PolyModel, source: PolyModel): boolean {53 return MATCHED_CAPS.every((c) => !source.capabilities[c] || candidate.capabilities[c]);54}5556function hasPricing(m: PolyModel): boolean {57 return Boolean(m.pricing && (m.pricing.inputPerMillion !== undefined || m.pricing.outputPerMillion !== undefined) && ((m.pricing.inputPerMillion ?? 0) > 0 || (m.pricing.outputPerMillion ?? 0) > 0));58}5960function priceTraffic(row: ModelUsageAgg, m: PolyModel): number {61 const cost = estimateCost({ inputTokens: row.inputTokens, outputTokens: row.outputTokens, cachedInputTokens: row.cachedTokens, reasoningTokens: row.reasoningTokens }, m.pricing);62 return cost.known ? cost.totalUsd : 0;63}6465export interface SavingsOptions {66 /** Minimum relative saving for a sibling to count (0.4 = 40 %). */67 minRatio?: number;68 /** Ignore opportunities below this absolute amount. */69 minUsd?: number;70 top?: number;71}7273export function computeSavings(rows: ModelUsageAgg[], registry: Map<string, PolyModel>, opts: SavingsOptions = {}): SavingsOpportunity[] {74 const minRatio = opts.minRatio ?? 0.4;75 const minUsd = opts.minUsd ?? 0.01;76 const top = opts.top ?? 3;77 const byProvider = new Map<string, PolyModel[]>();78 for (const m of registry.values()) {79 if (m.status === "deprecated" || !hasPricing(m)) continue;80 const arr = byProvider.get(m.provider) ?? [];81 arr.push(m);82 byProvider.set(m.provider, arr);83 }8485 const out: SavingsOpportunity[] = [];86 for (const row of rows) {87 if (row.costUsd <= 0 || row.requests <= 0) continue;88 const source = registry.get(row.modelKey);89 if (!source || !hasPricing(source)) continue;90 const current = priceTraffic(row, source);91 if (current <= 0) continue;92 const avgContext = row.requests ? row.inputTokens / row.requests : 0;93 const pool = (byProvider.get(source.provider) ?? []).filter((c) => c.key !== source.key && coversCapabilities(c, source) && (!c.limits?.contextTokens || c.limits.contextTokens >= avgContext * 1.1));94 const sameFamily = source.family ? pool.filter((c) => c.family === source.family) : [];95 const candidates = (sameFamily.length ? sameFamily : pool)96 .map((c) => ({ model: c, cost: priceTraffic(row, c) }))97 .filter((c) => c.cost > 0 && c.cost <= current * (1 - minRatio))98 .sort((a, b) => b.cost - a.cost); // most expensive qualifying sibling = closest tier99 const pick = candidates[0];100 if (!pick) continue;101 const savings = current - pick.cost;102 if (savings < minUsd) continue;103 const pct = savings / current;104 const fromLabel = source.displayName;105 const toLabel = pick.model.displayName;106 out.push({107 fromModelKey: source.key,108 fromLabel,109 toModelKey: pick.model.key,110 toLabel,111 provider: source.provider,112 requests: row.requests,113 currentCostUsd: round(current),114 alternativeCostUsd: round(pick.cost),115 savingsUsd: round(savings),116 savingsPct: Math.round(pct * 1000) / 10,117 headline: `Switching eligible tasks from ${fromLabel} to ${toLabel} could have saved ~${fmtUsd(savings)}`,118 rationale: `${toLabel} is on the same provider, ${FAST_TIER_RE.test(pick.model.id) ? "a faster tier " : ""}with ${describeCaps(source)} and ${Math.round(pct * 100)} % lower list prices for this traffic (${fmtTokens(row.inputTokens)} in / ${fmtTokens(row.outputTokens)} out across ${row.requests} request${row.requests === 1 ? "" : "s"}).`,119 });120 }121 return out.sort((a, b) => b.savingsUsd - a.savingsUsd).slice(0, top);122}123124function describeCaps(m: PolyModel): string {125 const names = MATCHED_CAPS.filter((c) => m.capabilities[c]).map((c) => (c === "structuredOutput" ? "structured output" : c));126 return names.length ? `the same capabilities (${names.join(", ")})` : "the same text capabilities";127}128129function round(n: number): number {130 return Math.round(n * 10_000) / 10_000;131}132function fmtUsd(n: number): string {133 return n < 0.01 ? `$${n.toFixed(4)}` : `$${n.toFixed(2)}`;134}135function fmtTokens(n: number): string {136 if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;137 if (n >= 1_000) return `${(n / 1_000).toFixed(0)}K`;138 return String(n);139}140