import type { ModelCapabilities, PolyModel } from "@/lib/ai/core/types"; import { estimateCost } from "@/lib/ai/core/pricing"; /** * Savings opportunities — pure math over aggregated usage per model + registry pricing. * * For every model that actually cost money in the range, we look for a *cheaper sibling on the same * provider* that could have handled the same traffic: it must not be deprecated, must offer every * capability the expensive model has among {vision, tools, structuredOutput, reasoning, files}, and * must fit the average prompt in its context window. We then re-price the exact same token volume * (input, cached input, output) with the sibling's list prices. * * Which sibling? The most capable one that still saves at least `minRatio` (default 40 %) — i.e. the * next tier down (Opus → Sonnet, GPT-5.5 → GPT-5.4 mini), not blindly the cheapest nano model. * Same-family siblings are preferred when the registry exposes a family. */ export interface ModelUsageAgg { modelKey: string; provider: string; requests: number; inputTokens: number; outputTokens: number; cachedTokens: number; reasoningTokens: number; costUsd: number; } export interface SavingsOpportunity { fromModelKey: string; fromLabel: string; toModelKey: string; toLabel: string; provider: string; requests: number; /** Cost of the real traffic re-priced with the expensive model's list prices (apples-to-apples). */ currentCostUsd: number; /** Same traffic priced with the sibling. */ alternativeCostUsd: number; savingsUsd: number; savingsPct: number; /** Human sentence for the card. */ headline: string; /** Why the sibling qualifies. */ rationale: string; } const MATCHED_CAPS: (keyof ModelCapabilities)[] = ["vision", "tools", "structuredOutput", "reasoning", "files"]; const FAST_TIER_RE = /flash|mini|nano|haiku|fast|lite|instant|turbo|small|ministral|sonnet|medium|8b|9b|gemma|gpt-oss-20b/i; export function coversCapabilities(candidate: PolyModel, source: PolyModel): boolean { return MATCHED_CAPS.every((c) => !source.capabilities[c] || candidate.capabilities[c]); } function hasPricing(m: PolyModel): boolean { return Boolean(m.pricing && (m.pricing.inputPerMillion !== undefined || m.pricing.outputPerMillion !== undefined) && ((m.pricing.inputPerMillion ?? 0) > 0 || (m.pricing.outputPerMillion ?? 0) > 0)); } function priceTraffic(row: ModelUsageAgg, m: PolyModel): number { const cost = estimateCost({ inputTokens: row.inputTokens, outputTokens: row.outputTokens, cachedInputTokens: row.cachedTokens, reasoningTokens: row.reasoningTokens }, m.pricing); return cost.known ? cost.totalUsd : 0; } export interface SavingsOptions { /** Minimum relative saving for a sibling to count (0.4 = 40 %). */ minRatio?: number; /** Ignore opportunities below this absolute amount. */ minUsd?: number; top?: number; } export function computeSavings(rows: ModelUsageAgg[], registry: Map, opts: SavingsOptions = {}): SavingsOpportunity[] { const minRatio = opts.minRatio ?? 0.4; const minUsd = opts.minUsd ?? 0.01; const top = opts.top ?? 3; const byProvider = new Map(); for (const m of registry.values()) { if (m.status === "deprecated" || !hasPricing(m)) continue; const arr = byProvider.get(m.provider) ?? []; arr.push(m); byProvider.set(m.provider, arr); } const out: SavingsOpportunity[] = []; for (const row of rows) { if (row.costUsd <= 0 || row.requests <= 0) continue; const source = registry.get(row.modelKey); if (!source || !hasPricing(source)) continue; const current = priceTraffic(row, source); if (current <= 0) continue; const avgContext = row.requests ? row.inputTokens / row.requests : 0; const pool = (byProvider.get(source.provider) ?? []).filter((c) => c.key !== source.key && coversCapabilities(c, source) && (!c.limits?.contextTokens || c.limits.contextTokens >= avgContext * 1.1)); const sameFamily = source.family ? pool.filter((c) => c.family === source.family) : []; const candidates = (sameFamily.length ? sameFamily : pool) .map((c) => ({ model: c, cost: priceTraffic(row, c) })) .filter((c) => c.cost > 0 && c.cost <= current * (1 - minRatio)) .sort((a, b) => b.cost - a.cost); // most expensive qualifying sibling = closest tier const pick = candidates[0]; if (!pick) continue; const savings = current - pick.cost; if (savings < minUsd) continue; const pct = savings / current; const fromLabel = source.displayName; const toLabel = pick.model.displayName; out.push({ fromModelKey: source.key, fromLabel, toModelKey: pick.model.key, toLabel, provider: source.provider, requests: row.requests, currentCostUsd: round(current), alternativeCostUsd: round(pick.cost), savingsUsd: round(savings), savingsPct: Math.round(pct * 1000) / 10, headline: `Switching eligible tasks from ${fromLabel} to ${toLabel} could have saved ~${fmtUsd(savings)}`, 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"}).`, }); } return out.sort((a, b) => b.savingsUsd - a.savingsUsd).slice(0, top); } function describeCaps(m: PolyModel): string { const names = MATCHED_CAPS.filter((c) => m.capabilities[c]).map((c) => (c === "structuredOutput" ? "structured output" : c)); return names.length ? `the same capabilities (${names.join(", ")})` : "the same text capabilities"; } function round(n: number): number { return Math.round(n * 10_000) / 10_000; } function fmtUsd(n: number): string { return n < 0.01 ? `$${n.toFixed(4)}` : `$${n.toFixed(2)}`; } function fmtTokens(n: number): string { if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`; if (n >= 1_000) return `${(n / 1_000).toFixed(0)}K`; return String(n); }