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%
6.0 KB · 140 lines typescript
Raw Blame History
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