// Generates docs/screenshots/free-tier-budget-card.svg from the per-model catalog. // Run: node scripts/research/gen-budget-card-svg.mjs import fs from "node:fs"; const txt = fs.readFileSync("open-sse/config/freeModelCatalog.data.ts", "utf8"); const recs = [ ...txt.matchAll( /\{ provider: "([^"]+)", modelId: "([^"]+)", displayName: "([^"]+)", monthlyTokens: (\d+), creditTokens: (\d+), freeType: "([^"]+)", poolKey: (null|"[^"]+"), tos: "([^"]+)" \}/g ), ].map((m) => ({ provider: m[1], modelId: m[2], displayName: m[3], monthlyTokens: +m[4], creditTokens: +m[5], freeType: m[6], poolKey: m[7] === "null" ? null : m[7].slice(1, -1), tos: m[8], })); const fmt = (n) => n >= 1e9 ? (n / 1e9).toFixed(2) + "B" : n >= 1e6 ? Math.round(n / 1e6) + "M" : Math.round(n / 1e3) + "K"; const poolMap = new Map(); for (const r of recs) { if (!["recurring-daily", "recurring-monthly", "keyless"].includes(r.freeType)) continue; const k = r.poolKey || `${r.provider}:${r.modelId}`; const cur = poolMap.get(k); if (!cur || r.monthlyTokens > cur.monthlyTokens) poolMap.set(k, r); } const pools = [...poolMap.values()].filter((r) => r.monthlyTokens > 0).sort((a, b) => b.monthlyTokens - a.monthlyTokens); const steady = pools.reduce((s, r) => s + r.monthlyTokens, 0); const otMap = new Map(); for (const r of recs) { if (r.freeType !== "one-time-initial" || r.creditTokens <= 0) continue; const k = r.poolKey || r.provider; otMap.set(k, { provider: r.provider, v: Math.max(otMap.get(k)?.v || 0, r.creditTokens) }); } const oneTime = [...otMap.values()].sort((a, b) => b.v - a.v); const oneTimeSum = oneTime.reduce((s, r) => s + r.v, 0); const firstMonth = steady + oneTimeSum; const avoidProviders = [...new Set(recs.filter((r) => r.tos === "avoid").map((r) => r.provider))].length; const uncappedProviders = [...new Set(recs.filter((r) => r.freeType === "recurring-uncapped").map((r) => r.provider))]; const GRID = pools.slice(0, 28); const STRIP = oneTime.slice(0, 9); const PAL = ["#6c5ce7","#00b894","#0984e3","#e17055","#fdcb6e","#e84393","#00cec9","#d63031","#a29bfe","#55efc4","#74b9ff","#ffeaa7","#fab1a0","#81ecec"]; const color = (i) => PAL[i % PAL.length]; const cleanName = (r) => (r.displayName || r.provider).replace(/\s*\(.*$/, "").replace(/ —.*$/, "").slice(0, 24); // bar segments (min width so every pool shows) const BAR_X = 32, BAR_W = 836, MIN = 7; const extra = BAR_W - MIN * GRID.length; let bx = BAR_X; const segs = GRID.map((r, i) => { const w = MIN + (r.monthlyTokens / steady) * extra; const s = { x: bx, w, c: color(i) }; bx += w; return s; }); const B = []; // body elements // title B.push(`Monthly free-token budget`); B.push(`${pools.length} free pools · ${recs.length} models · one endpoint`); // stats const stat = (sx, label, val, vc) => { B.push(`${label}`); B.push(`${val}`); }; stat(32, "Steady / month", `~${fmt(steady)}`, "#e6edf3"); stat(330, "First month (+ signup credits)", `~${fmt(firstMonth)}`, "#3fb950"); stat(700, "ToS-flagged (you decide)", `${avoidProviders} providers`, "#d29922"); // bar B.push(``); B.push(``); for (const s of segs) B.push(``); B.push(``); B.push(`Each segment = one free pool · widths floored so every provider shows · honest numbers in the grid.`); // model grid 4 cols const COLS = 4, COLW = 213, GX = 32, GY = 200, RH = 30; GRID.forEach((r, i) => { const col = i % COLS, row = (i / COLS) | 0; const cx = GX + col * COLW, cy = GY + row * RH; B.push(``); B.push(`${cleanName(r)} ${fmt(r.monthlyTokens)}`); }); let y = GY + Math.ceil(GRID.length / COLS) * RH + 6; // first-month strip (wrapping) B.push(``); y += 26; B.push(`+ First month: one-time signup credits (~${fmt(oneTimeSum)})`); y += 24; let sxp = 32; for (const r of STRIP) { const label = `${r.provider} ${fmt(r.v)}`; const w = 16 + label.length * 6.7; if (sxp + w > 862) { sxp = 32; y += 30; } B.push(``); B.push(`${label}`); sxp += w + 8; } y += 26; // ToS note (softened) B.push(``); B.push(`Pool-deduped, honest counting — no inflated rate-limit ceilings. Some terms suggest personal-use only; we flag them so you decide.`); B.push(`+ ${uncappedProviders.length} permanently-free, no-cap providers (e.g. ${uncappedProviders.slice(0, 3).join(", ")}) · OpenRouter $10 → +24M/mo.`); y += 34; const H = y + 24; // card content bottom const CANVAS = H + 16; const out = []; out.push(``); out.push(``); out.push(``); out.push(`OmniRoute · /dashboard/free-tiers · preview mockup`); out.push(...B); out.push(``); fs.writeFileSync("docs/screenshots/free-tier-budget-card.svg", out.join("\n") + "\n"); console.log(`SVG: ${GRID.length} models, ${STRIP.length} first-month chips, canvas ${CANVAS}px. steady=${fmt(steady)} firstMonth=${fmt(firstMonth)} oneTime=${fmt(oneTimeSum)}`);