Created
May 9, 2026 21:01
-
-
Save sshh12/d968c2c14da9ba554c7820c4a530ccc5 to your computer and use it in GitHub Desktop.
Cache Ping Cost Analyzer
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Claude Cache Ping Analyzer</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;600&display=swap" rel="stylesheet"> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script> | |
| <style> | |
| *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } | |
| :root { | |
| --bg: #080808; | |
| --s1: #111111; | |
| --s2: #191919; | |
| --border: #242424; | |
| --text: #f0f0f0; | |
| --muted: #909090; | |
| --faint: #555; | |
| --violet: #7c6aff; | |
| --red: #f05050; | |
| --amber: #e8a030; | |
| --green: #3ecf74; | |
| --blue: #60a5fa; | |
| --font-sans: 'IBM Plex Sans', system-ui, sans-serif; | |
| --font-mono: 'IBM Plex Mono', monospace; | |
| } | |
| html { font-size: 15px; } | |
| body { | |
| background: var(--bg); | |
| color: var(--text); | |
| font-family: var(--font-sans); | |
| min-height: 100vh; | |
| padding: 48px 24px 72px; | |
| line-height: 1.5; | |
| -webkit-font-smoothing: antialiased; | |
| } | |
| /* ── Header ─────────────────────────────────────────────── */ | |
| header { | |
| max-width: 960px; | |
| margin: 0 auto 52px; | |
| } | |
| .header-eyebrow { | |
| font-family: var(--font-sans); | |
| font-size: 0.75rem; | |
| font-weight: 500; | |
| color: var(--muted); | |
| letter-spacing: 0.04em; | |
| text-transform: uppercase; | |
| margin-bottom: 12px; | |
| } | |
| header h1 { | |
| font-family: var(--font-sans); | |
| font-size: clamp(1.6rem, 3.5vw, 2.4rem); | |
| font-weight: 700; | |
| color: var(--text); | |
| letter-spacing: -0.02em; | |
| line-height: 1.15; | |
| margin-bottom: 12px; | |
| } | |
| header h1 em { | |
| font-style: normal; | |
| color: var(--violet); | |
| } | |
| header p { | |
| color: var(--muted); | |
| font-size: 0.9rem; | |
| max-width: 480px; | |
| line-height: 1.65; | |
| } | |
| /* ── Layout ─────────────────────────────────────────────── */ | |
| .layout { | |
| display: grid; | |
| grid-template-columns: 224px 1fr; | |
| gap: 24px; | |
| max-width: 1120px; | |
| margin: 0 auto; | |
| align-items: start; | |
| } | |
| @media (max-width: 760px) { | |
| .layout { grid-template-columns: 1fr; } | |
| .sidebar { position: static !important; } | |
| } | |
| /* ── Sidebar / Controls ──────────────────────────────────── */ | |
| .sidebar { | |
| position: sticky; | |
| top: 32px; | |
| } | |
| .ctrl-section { | |
| margin-bottom: 32px; | |
| } | |
| .ctrl-section-label { | |
| font-family: var(--font-sans); | |
| font-size: 0.7rem; | |
| font-weight: 600; | |
| letter-spacing: 0.07em; | |
| text-transform: uppercase; | |
| color: var(--muted); | |
| margin-bottom: 16px; | |
| } | |
| .ctrl { | |
| margin-bottom: 20px; | |
| } | |
| .ctrl:last-child { margin-bottom: 0; } | |
| .ctrl-label { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: baseline; | |
| font-size: 0.82rem; | |
| color: var(--muted); | |
| margin-bottom: 8px; | |
| } | |
| .ctrl-val { | |
| font-family: var(--font-mono); | |
| font-size: 0.78rem; | |
| color: var(--violet); | |
| font-weight: 600; | |
| } | |
| input[type="range"] { | |
| -webkit-appearance: none; | |
| appearance: none; | |
| width: 100%; | |
| height: 2px; | |
| background: var(--border); | |
| border-radius: 2px; | |
| outline: none; | |
| cursor: pointer; | |
| } | |
| input[type="range"]::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| width: 13px; | |
| height: 13px; | |
| border-radius: 50%; | |
| background: var(--violet); | |
| cursor: pointer; | |
| transition: transform 0.15s; | |
| } | |
| input[type="range"]::-webkit-slider-thumb:hover { transform: scale(1.25); } | |
| input[type="range"]::-moz-range-thumb { | |
| width: 13px; height: 13px; | |
| border-radius: 50%; | |
| background: var(--violet); | |
| border: none; | |
| cursor: pointer; | |
| } | |
| select { | |
| width: 100%; | |
| background: transparent; | |
| border: 1px solid var(--border); | |
| border-radius: 6px; | |
| color: var(--text); | |
| padding: 7px 10px; | |
| font-size: 0.8rem; | |
| font-family: var(--font-sans); | |
| outline: none; | |
| cursor: pointer; | |
| -webkit-appearance: none; | |
| appearance: none; | |
| background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%23555' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E"); | |
| background-repeat: no-repeat; | |
| background-position: right 10px center; | |
| padding-right: 28px; | |
| } | |
| select:focus { border-color: var(--violet); } | |
| option { background: #1a1a1a; } | |
| /* ── Main column ─────────────────────────────────────────── */ | |
| .main { display: flex; flex-direction: column; gap: 20px; } | |
| /* ── Strategy cards ───────────────────────────────────────── */ | |
| .cards { | |
| display: grid; | |
| grid-template-columns: repeat(4, 1fr); | |
| gap: 10px; | |
| } | |
| @media (max-width: 700px) { .cards { grid-template-columns: repeat(2, 1fr); } } | |
| @media (max-width: 420px) { .cards { grid-template-columns: 1fr; } } | |
| .card { | |
| background: var(--s1); | |
| border: 1px solid var(--border); | |
| border-radius: 10px; | |
| padding: 18px 18px 16px; | |
| position: relative; | |
| overflow: hidden; | |
| transition: border-color 0.25s; | |
| } | |
| .card::before { | |
| content: ''; | |
| position: absolute; | |
| inset: 0 auto 0 0; | |
| width: 3px; | |
| border-radius: 10px 0 0 10px; | |
| background: transparent; | |
| transition: background 0.25s; | |
| } | |
| .card.cheapest { border-color: var(--border); } | |
| .card.cheapest::before { background: var(--green); } | |
| .card.most-expensive::before { background: var(--red); opacity: 0.4; } | |
| .card-meta { | |
| font-family: var(--font-sans); | |
| font-size: 0.68rem; | |
| font-weight: 600; | |
| color: var(--faint); | |
| letter-spacing: 0.06em; | |
| text-transform: uppercase; | |
| margin-bottom: 4px; | |
| } | |
| .card-name { | |
| font-family: var(--font-sans); | |
| font-size: 0.9rem; | |
| font-weight: 600; | |
| color: var(--text); | |
| margin-bottom: 16px; | |
| } | |
| .card-cost { | |
| font-family: var(--font-mono); | |
| font-size: 1.9rem; | |
| font-weight: 700; | |
| letter-spacing: -0.04em; | |
| line-height: 1; | |
| margin-bottom: 5px; | |
| color: var(--text); | |
| transition: color 0.25s; | |
| } | |
| .card.cheapest .card-cost { color: var(--green); } | |
| .card.most-expensive .card-cost { color: var(--red); opacity: 0.8; } | |
| .card-vs { | |
| font-size: 0.78rem; | |
| color: var(--muted); | |
| margin-bottom: 14px; | |
| min-height: 1em; | |
| } | |
| .card-vs .save { color: var(--green); } | |
| .card-vs .loss { color: var(--red); } | |
| .card-detail { | |
| font-size: 0.78rem; | |
| color: var(--muted); | |
| line-height: 1.55; | |
| padding-top: 12px; | |
| border-top: 1px solid var(--border); | |
| } | |
| /* ── Chart panels ──────────────────────────────────────────── */ | |
| .panel { | |
| background: var(--s1); | |
| border: 1px solid var(--border); | |
| border-radius: 10px; | |
| padding: 22px 22px 20px; | |
| } | |
| .panel-header { | |
| display: flex; | |
| align-items: baseline; | |
| gap: 12px; | |
| margin-bottom: 20px; | |
| } | |
| .panel-title { | |
| font-family: var(--font-sans); | |
| font-size: 0.88rem; | |
| font-weight: 600; | |
| color: var(--text); | |
| } | |
| .panel-sub { | |
| font-size: 0.78rem; | |
| color: var(--muted); | |
| } | |
| .chart-wrap { position: relative; height: 240px; } | |
| /* ── Footer ─────────────────────────────────────────────────── */ | |
| .footnote { | |
| text-align: center; | |
| font-family: var(--font-sans); | |
| font-size: 0.72rem; | |
| color: var(--faint); | |
| margin-top: 32px; | |
| max-width: 1120px; | |
| margin-inline: auto; | |
| line-height: 1.8; | |
| } | |
| .footnote a { color: var(--violet); text-decoration: none; } | |
| .footnote a:hover { text-decoration: underline; } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <div class="header-eyebrow">Claude API · Prompt Caching</div> | |
| <h1>Cache Ping<br><em>Cost Analyzer</em></h1> | |
| <p>Claude caches repeated context (system prompts, docs, codebases) at 10% of normal input cost — but the cache expires after 5 min idle, forcing a 1.25× rewrite on the next call. For infrequent workflows, a cheap keep-alive ping resets the clock at that same 10% read rate. This tool shows when that's actually worth it.</p> | |
| </header> | |
| <div class="layout"> | |
| <!-- Sidebar --> | |
| <div class="sidebar"> | |
| <div class="ctrl-section"> | |
| <div class="ctrl-section-label">Presets</div> | |
| <div class="ctrl"> | |
| <select id="sel-preset"> | |
| <option value="">— custom —</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div class="ctrl-section"> | |
| <div class="ctrl-section-label">Model</div> | |
| <div class="ctrl"> | |
| <select id="sel-model"> | |
| <option value="opus">Opus 4.7</option> | |
| <option value="sonnet">Sonnet 4.6</option> | |
| <option value="haiku">Haiku 4.5</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div class="ctrl-section"> | |
| <div class="ctrl-section-label">Context</div> | |
| <div class="ctrl"> | |
| <div class="ctrl-label"> | |
| <span>Context tokens</span> | |
| <span class="ctrl-val" id="lbl-ctx">500K</span> | |
| </div> | |
| <input type="range" id="sl-ctx" min="10000" max="800000" step="10000" value="500000"> | |
| </div> | |
| <div class="ctrl"> | |
| <div class="ctrl-label"> | |
| <span>Output tokens / event</span> | |
| <span class="ctrl-val" id="lbl-output">100</span> | |
| </div> | |
| <input type="range" id="sl-output" min="0" max="4000" step="100" value="100"> | |
| </div> | |
| </div> | |
| <div class="ctrl-section"> | |
| <div class="ctrl-section-label">Timing</div> | |
| <div class="ctrl"> | |
| <div class="ctrl-label"> | |
| <span>Event interval</span> | |
| <span class="ctrl-val"><span id="lbl-interval">8</span> min</span> | |
| </div> | |
| <input type="range" id="sl-interval" min="1" max="120" step="1" value="8"> | |
| </div> | |
| <div class="ctrl"> | |
| <div class="ctrl-label"> | |
| <span>Ping interval</span> | |
| <span class="ctrl-val"><span id="lbl-ping">4.5</span> min</span> | |
| </div> | |
| <input type="range" id="sl-ping" min="1" max="60" step="1" value="4"> | |
| </div> | |
| <div class="ctrl"> | |
| <div class="ctrl-label"><span>Duration</span></div> | |
| <select id="sel-duration"> | |
| <option value="1">1 hour</option> | |
| <option value="4">4 hours</option> | |
| <option value="8">8 hours</option> | |
| <option value="24">24 hours</option> | |
| <option value="168">7 days</option> | |
| <option value="720">30 days</option> | |
| <option value="8760">1 year</option> | |
| </select> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Main --> | |
| <div class="main"> | |
| <div class="cards"> | |
| <div class="card" id="card-a"> | |
| <div class="card-meta">Strategy A · 5-min TTL</div> | |
| <div class="card-name">Default</div> | |
| <div class="card-cost" id="cost-a">—</div> | |
| <div class="card-vs" id="vs-a"></div> | |
| <div class="card-detail" id="detail-a">Cache busts every event. Full rewrite each time.</div> | |
| </div> | |
| <div class="card" id="card-b"> | |
| <div class="card-meta">Strategy B · 1-hour TTL</div> | |
| <div class="card-name">Extended TTL</div> | |
| <div class="card-cost" id="cost-b">—</div> | |
| <div class="card-vs" id="vs-b"></div> | |
| <div class="card-detail" id="detail-b">Write at 2× rate; TTL extends 1 hour per read.</div> | |
| </div> | |
| <div class="card" id="card-c"> | |
| <div class="card-meta">Strategy C · 5-min TTL</div> | |
| <div class="card-name">Keep-Alive Pings</div> | |
| <div class="card-cost" id="cost-c">—</div> | |
| <div class="card-vs" id="vs-c"></div> | |
| <div class="card-detail" id="detail-c">Write at 1.25× rate; ping every 4.5 min to reset TTL.</div> | |
| </div> | |
| <div class="card" id="card-d"> | |
| <div class="card-meta">Strategy D · 1-hour TTL</div> | |
| <div class="card-name">Extended + Pings</div> | |
| <div class="card-cost" id="cost-d">—</div> | |
| <div class="card-vs" id="vs-d"></div> | |
| <div class="card-detail" id="detail-d">Write at 2× rate; ping at your interval to reset the 1-hour TTL.</div> | |
| </div> | |
| </div> | |
| <div class="panel"> | |
| <div class="panel-header"> | |
| <span class="panel-title">Cumulative Cost</span> | |
| <span class="panel-sub">over time</span> | |
| </div> | |
| <div class="chart-wrap"><canvas id="chart-time"></canvas></div> | |
| </div> | |
| <div class="panel"> | |
| <div class="panel-header"> | |
| <span class="panel-title">Break-Even Curve</span> | |
| <span class="panel-sub">cost vs event interval, 1–120 min</span> | |
| </div> | |
| <div class="chart-wrap"><canvas id="chart-interval"></canvas></div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="footnote"> | |
| Opus 4.7 — $5/M input · $6.25/M 5m-write · $10/M 1h-write · $0.50/M read · $25/M output / | |
| Sonnet 4.6 — $3 · $3.75 · $6 · $0.30 · $15 / | |
| Haiku 4.5 — $1 · $1.25 · $2 · $0.10 · $5<br> | |
| Source: <a href="https://platform.claude.com/docs/en/build-with-claude/prompt-caching">Anthropic prompt caching docs</a> · May 2026 | |
| </div> | |
| <script> | |
| const MODELS = { | |
| opus: { label: 'Claude Opus 4.7', w5m: 6.25, w1h: 10, read: 0.50, out: 25 }, | |
| sonnet: { label: 'Claude Sonnet 4.6', w5m: 3.75, w1h: 6, read: 0.30, out: 15 }, | |
| haiku: { label: 'Claude Haiku 4.5', w5m: 1.25, w1h: 2, read: 0.10, out: 5 }, | |
| }; | |
| function simulate(p, ctxM, eventInterval, pingInterval, durationHours, outputM) { | |
| const totalMin = durationHours * 60; | |
| const eventTimes = []; | |
| for (let t = eventInterval; t <= totalMin + 1e-9; t += eventInterval) | |
| eventTimes.push(parseFloat(t.toFixed(4))); | |
| // Strategy A | |
| const tlA = [{ t: 0, c: 0 }]; | |
| let cA = 0; | |
| if (eventInterval <= 5) { | |
| cA += ctxM * p.w5m; tlA[0].c = cA; | |
| for (const t of eventTimes) { cA += ctxM * p.read + outputM * p.out; tlA.push({ t, c: cA }); } | |
| } else { | |
| for (const t of eventTimes) { cA += ctxM * p.w5m + ctxM * p.read + outputM * p.out; tlA.push({ t, c: cA }); } | |
| } | |
| // Strategy B | |
| const tlB = [{ t: 0, c: 0 }]; | |
| let cB = 0; | |
| if (eventInterval <= 60) { | |
| cB += ctxM * p.w1h; tlB[0].c = cB; | |
| for (const t of eventTimes) { cB += ctxM * p.read + outputM * p.out; tlB.push({ t, c: cB }); } | |
| } else { | |
| for (const t of eventTimes) { cB += ctxM * p.w1h + ctxM * p.read + outputM * p.out; tlB.push({ t, c: cB }); } | |
| } | |
| // Strategy C: 5-min TTL + keep-alive pings | |
| // If pingInterval >= 5, pings arrive after the cache has already expired — each | |
| // access becomes a cache miss (write + read) just like Strategy A. | |
| const TTL_C = 5; | |
| const tlC = [{ t: 0, c: ctxM * p.w5m }]; | |
| let cC = ctxM * p.w5m; | |
| let lastReadC = 0, numPings = 0; | |
| for (const t of eventTimes) { | |
| while (lastReadC + pingInterval < t - 1e-9) { | |
| lastReadC += pingInterval; numPings++; | |
| if (pingInterval >= TTL_C) cC += ctxM * p.w5m; // cache expired before ping | |
| cC += ctxM * p.read; | |
| tlC.push({ t: lastReadC, c: cC }); | |
| } | |
| if (t - lastReadC >= TTL_C) cC += ctxM * p.w5m; // cache expired before event | |
| cC += ctxM * p.read + outputM * p.out; | |
| lastReadC = t; tlC.push({ t, c: cC }); | |
| } | |
| // Strategy D: 1-hour TTL + keep-alive pings (uses same pingInterval as C) | |
| // If pingInterval >= 60, pings arrive after the 1-hour TTL expires. | |
| const TTL_D = 60; | |
| const tlD = [{ t: 0, c: ctxM * p.w1h }]; | |
| let cD = ctxM * p.w1h; | |
| let lastReadD = 0, numPingsD = 0; | |
| for (const t of eventTimes) { | |
| while (lastReadD + pingInterval < t - 1e-9) { | |
| lastReadD += pingInterval; numPingsD++; | |
| if (pingInterval >= TTL_D) cD += ctxM * p.w1h; // cache expired before ping | |
| cD += ctxM * p.read; | |
| tlD.push({ t: lastReadD, c: cD }); | |
| } | |
| if (t - lastReadD >= TTL_D) cD += ctxM * p.w1h; // cache expired before event | |
| cD += ctxM * p.read + outputM * p.out; | |
| lastReadD = t; tlD.push({ t, c: cD }); | |
| } | |
| return { costA: cA, costB: cB, costC: cC, costD: cD, tlA, tlB, tlC, tlD, numEvents: eventTimes.length, numPings, numPingsD }; | |
| } | |
| function fmt(v) { | |
| if (v === 0) return '$0.000'; | |
| if (v < 0.0001) return '$' + v.toExponential(2); | |
| if (v < 0.01) return '$' + v.toFixed(5); | |
| if (v < 1) return '$' + v.toFixed(4); | |
| if (v < 100) return '$' + v.toFixed(3); | |
| return '$' + v.toFixed(2); | |
| } | |
| function fmtK(v) { | |
| if (v >= 1e6) return (v/1e6).toFixed(1) + 'M'; | |
| if (v >= 1e3) return Math.round(v/1e3) + 'K'; | |
| return v; | |
| } | |
| const CHART_DEFAULTS = (xLabel, yLabel) => ({ | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| animation: { duration: 180 }, | |
| plugins: { | |
| legend: { | |
| labels: { | |
| color: '#909090', | |
| font: { size: 11, family: "'IBM Plex Sans', system-ui, sans-serif" }, | |
| boxWidth: 12, padding: 18, | |
| usePointStyle: true, pointStyle: 'line', | |
| } | |
| }, | |
| tooltip: { | |
| backgroundColor: '#111', | |
| borderColor: '#2a2a2a', | |
| borderWidth: 1, | |
| titleColor: '#f0f0f0', | |
| bodyColor: '#909090', | |
| titleFont: { family: "'IBM Plex Sans', system-ui, sans-serif", size: 11 }, | |
| bodyFont: { family: "'IBM Plex Mono', monospace", size: 11 }, | |
| padding: 10, | |
| callbacks: { label: ctx => ` ${ctx.dataset.label}: ${fmt(ctx.parsed.y)}` } | |
| } | |
| }, | |
| scales: { | |
| x: { | |
| ticks: { color: '#666', font: { size: 10, family: "'IBM Plex Sans', sans-serif" }, maxTicksLimit: 8 }, | |
| grid: { color: '#161616' }, | |
| border: { color: '#1e1e1e' }, | |
| title: { display: true, text: xLabel, color: '#666', font: { size: 11, family: "'IBM Plex Sans', sans-serif" } }, | |
| }, | |
| y: { | |
| ticks: { | |
| color: '#666', font: { size: 10, family: "'IBM Plex Mono', monospace" }, | |
| callback: v => fmt(v), maxTicksLimit: 6, | |
| }, | |
| grid: { color: '#161616' }, | |
| border: { color: '#1e1e1e' }, | |
| title: { display: true, text: yLabel, color: '#666', font: { size: 11, family: "'IBM Plex Sans', sans-serif" } }, | |
| } | |
| } | |
| }); | |
| let chartTime, chartInterval; | |
| function initCharts() { | |
| chartTime = new Chart(document.getElementById('chart-time'), { | |
| type: 'line', data: { datasets: [] }, | |
| options: CHART_DEFAULTS('Time (minutes)', 'Cumulative Cost ($)'), | |
| }); | |
| chartInterval = new Chart(document.getElementById('chart-interval'), { | |
| type: 'line', data: { datasets: [] }, | |
| options: CHART_DEFAULTS('Event Interval (minutes)', 'Total Cost ($)'), | |
| }); | |
| } | |
| function tlToPoints(tl, totalMin) { | |
| const pts = []; | |
| for (let i = 0; i <= 300; i++) { | |
| const t = (i / 300) * totalMin; | |
| let cost = 0; | |
| for (let j = tl.length - 1; j >= 0; j--) { | |
| if (tl[j].t <= t + 1e-9) { cost = tl[j].c; break; } | |
| } | |
| pts.push({ x: parseFloat(t.toFixed(2)), y: cost }); | |
| } | |
| return pts; | |
| } | |
| function update() { | |
| const p = MODELS[document.getElementById('sel-model').value]; | |
| const ctxTok = parseInt(document.getElementById('sl-ctx').value); | |
| const eventInt = parseFloat(document.getElementById('sl-interval').value); | |
| const pingInt = parseFloat(document.getElementById('sl-ping').value); | |
| const durH = parseFloat(document.getElementById('sel-duration').value); | |
| const outTok = parseInt(document.getElementById('sl-output').value); | |
| const ctxM = ctxTok / 1e6, outM = outTok / 1e6, totalMin = durH * 60; | |
| const { costA, costB, costC, costD, tlA, tlB, tlC, tlD, numEvents, numPings, numPingsD } = | |
| simulate(p, ctxM, eventInt, pingInt, durH, outM); | |
| const costs = [costA, costB, costC, costD]; | |
| const minC = Math.min(...costs), maxC = Math.max(...costs); | |
| ['a','b','c','d'].forEach((s, i) => { | |
| document.getElementById(`cost-${s}`).textContent = fmt(costs[i]); | |
| const card = document.getElementById(`card-${s}`); | |
| card.classList.toggle('cheapest', costs[i] === minC); | |
| card.classList.toggle('most-expensive', costs[i] === maxC && costs[i] !== minC); | |
| }); | |
| const savB = costA > 0 ? (costA - costB) / costA * 100 : 0; | |
| const savC = costA > 0 ? (costA - costC) / costA * 100 : 0; | |
| const savD = costA > 0 ? (costA - costD) / costA * 100 : 0; | |
| const vsHtml = (sav) => { | |
| if (Math.abs(sav) < 0.5) return ''; | |
| return sav > 0 | |
| ? `<span class="save">${sav.toFixed(0)}% less than Default</span>` | |
| : `<span class="loss">${(-sav).toFixed(0)}% more than Default</span>`; | |
| }; | |
| document.getElementById('vs-a').innerHTML = ''; | |
| document.getElementById('vs-b').innerHTML = vsHtml(savB); | |
| document.getElementById('vs-c').innerHTML = vsHtml(savC); | |
| document.getElementById('vs-d').innerHTML = vsHtml(savD); | |
| const wpe = fmt(ctxM * p.w5m), rpa = fmt(ctxM * p.read); | |
| document.getElementById('detail-a').textContent = eventInt <= 5 | |
| ? `${numEvents} events — cache never busts (≤5 min TTL). Reads only.` | |
| : `${numEvents} events × write ${wpe} + read ${rpa}.`; | |
| document.getElementById('detail-b').textContent = eventInt <= 60 | |
| ? `1 write at 2× + ${numEvents} reads over ${durH}h.` | |
| : `${numEvents} writes at 2× — cache busts (>60 min TTL).`; | |
| document.getElementById('detail-c').textContent = | |
| `1 write · ${numPings} pings · ${numEvents} reads.`; | |
| document.getElementById('detail-d').textContent = | |
| `1 write at 2× · ${numPingsD} pings (${pingInt} min) · ${numEvents} reads.`; | |
| // Time chart | |
| chartTime.data.datasets = [ | |
| { label: 'A · Default', data: tlToPoints(tlA, totalMin), borderColor: '#f05050', backgroundColor: 'rgba(240,80,80,0.04)', fill: true, tension: 0, pointRadius: 0, borderWidth: 1.5, stepped: 'before' }, | |
| { label: 'B · Extended TTL', data: tlToPoints(tlB, totalMin), borderColor: '#e8a030', backgroundColor: 'rgba(232,160,48,0.04)', fill: true, tension: 0, pointRadius: 0, borderWidth: 1.5, stepped: 'before' }, | |
| { label: 'C · Keep-Alive Pings', data: tlToPoints(tlC, totalMin), borderColor: '#3ecf74', backgroundColor: 'rgba(62,207,116,0.04)', fill: true, tension: 0, pointRadius: 0, borderWidth: 1.5, stepped: 'before' }, | |
| { label: 'D · Extended + Pings', data: tlToPoints(tlD, totalMin), borderColor: '#60a5fa', backgroundColor: 'rgba(96,165,250,0.04)', fill: true, tension: 0, pointRadius: 0, borderWidth: 1.5, stepped: 'before' }, | |
| ]; | |
| chartTime.options.scales.x.type = 'linear'; | |
| chartTime.update(); | |
| // Interval sweep | |
| const ivLabels = [], swA = [], swB = [], swC = [], swD = []; | |
| for (let iv = 1; iv <= 120; iv++) { | |
| const r = simulate(p, ctxM, iv, pingInt, durH, outM); | |
| ivLabels.push(iv); swA.push(r.costA); swB.push(r.costB); swC.push(r.costC); swD.push(r.costD); | |
| } | |
| chartInterval.data.labels = ivLabels; | |
| chartInterval.data.datasets = [ | |
| { label: 'A · Default', data: swA, borderColor: '#f05050', backgroundColor: 'transparent', tension: 0.3, pointRadius: 0, borderWidth: 1.5 }, | |
| { label: 'B · Extended TTL', data: swB, borderColor: '#e8a030', backgroundColor: 'transparent', tension: 0.3, pointRadius: 0, borderWidth: 1.5 }, | |
| { label: 'C · Keep-Alive Pings', data: swC, borderColor: '#3ecf74', backgroundColor: 'transparent', tension: 0.3, pointRadius: 0, borderWidth: 1.5 }, | |
| { label: 'D · Extended + Pings', data: swD, borderColor: '#60a5fa', backgroundColor: 'transparent', tension: 0.3, pointRadius: 0, borderWidth: 1.5 }, | |
| ]; | |
| chartInterval.update(); | |
| } | |
| // ── Presets ─────────────────────────────────────────────────────────────────── | |
| const PRESETS = [ | |
| { | |
| name: 'PR Review Bot', | |
| sub: '300K ctx · PRs every 30 min · 8h day', | |
| model: 'opus', ctx: 300000, eventInterval: 30, pingInterval: 4, duration: 8, output: 500, | |
| }, | |
| { | |
| name: 'Support Desk Agent', | |
| sub: '100K knowledge base · ticket every 6 min', | |
| model: 'sonnet', ctx: 100000, eventInterval: 6, pingInterval: 4, duration: 8, output: 250, | |
| }, | |
| { | |
| name: 'Legal Doc Q&A', | |
| sub: '75K doc · analyst query every 12 min', | |
| model: 'opus', ctx: 75000, eventInterval: 12, pingInterval: 4, duration: 4, output: 400, | |
| }, | |
| { | |
| name: 'Log / Alert Monitor', | |
| sub: '50K runbook · alert every 2 min · 24h', | |
| model: 'haiku', ctx: 50000, eventInterval: 2, pingInterval: 1.5, duration: 24, output: 100, | |
| }, | |
| { | |
| name: 'Research Agent', | |
| sub: '400K papers + notes · step every 8 min', | |
| model: 'opus', ctx: 400000, eventInterval: 8, pingInterval: 4, duration: 4, output: 800, | |
| }, | |
| { | |
| name: 'CI/CD Watchdog', | |
| sub: '40K build config · build every 15 min', | |
| model: 'haiku', ctx: 40000, eventInterval: 15, pingInterval: 4, duration: 8, output: 200, | |
| }, | |
| { | |
| name: 'Slack Bot', | |
| sub: '20K system prompt · message every 3 min', | |
| model: 'haiku', ctx: 20000, eventInterval: 3, pingInterval: 2.0, duration: 8, output: 150, | |
| }, | |
| { | |
| name: 'Overnight Pipeline', | |
| sub: '80K pipeline defs · job every 20 min · 7d', | |
| model: 'sonnet', ctx: 80000, eventInterval: 20, pingInterval: 4, duration: 168, output: 300, | |
| }, | |
| ]; | |
| // Populate preset select | |
| const selPreset = document.getElementById('sel-preset'); | |
| PRESETS.forEach((preset, idx) => { | |
| const opt = document.createElement('option'); | |
| opt.value = idx; | |
| opt.textContent = preset.name; | |
| selPreset.appendChild(opt); | |
| }); | |
| function applyPreset(preset) { | |
| document.getElementById('sel-model').value = preset.model; | |
| document.getElementById('sl-ctx').value = preset.ctx; | |
| document.getElementById('lbl-ctx').textContent = fmtK(preset.ctx); | |
| document.getElementById('sl-interval').value = preset.eventInterval; | |
| document.getElementById('lbl-interval').textContent = preset.eventInterval; | |
| document.getElementById('sl-ping').value = preset.pingInterval; | |
| document.getElementById('lbl-ping').textContent = preset.pingInterval; | |
| document.getElementById('sel-duration').value = preset.duration; | |
| document.getElementById('sl-output').value = preset.output; | |
| document.getElementById('lbl-output').textContent = fmtK(preset.output); | |
| update(); | |
| } | |
| function clearPresetSelect() { selPreset.value = ''; } | |
| selPreset.addEventListener('change', function() { | |
| if (this.value !== '') applyPreset(PRESETS[parseInt(this.value)]); | |
| }); | |
| document.getElementById('sl-ctx').addEventListener('input', function() { document.getElementById('lbl-ctx').textContent = fmtK(this.value); clearPresetSelect(); update(); }); | |
| document.getElementById('sl-interval').addEventListener('input', function() { document.getElementById('lbl-interval').textContent = this.value; clearPresetSelect(); update(); }); | |
| document.getElementById('sl-ping').addEventListener('input', function() { document.getElementById('lbl-ping').textContent = this.value; clearPresetSelect(); update(); }); | |
| document.getElementById('sl-output').addEventListener('input', function() { document.getElementById('lbl-output').textContent = fmtK(this.value); clearPresetSelect(); update(); }); | |
| document.getElementById('sel-model').addEventListener('change', () => { clearPresetSelect(); update(); }); | |
| document.getElementById('sel-duration').addEventListener('change', () => { clearPresetSelect(); update(); }); | |
| initCharts(); | |
| update(); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment