From a52423870bd9fe3bb98adb83dc38372e0be778eb Mon Sep 17 00:00:00 2001 From: teamchong <25894545+teamchong@users.noreply.github.com> Date: Tue, 19 May 2026 11:40:03 -0400 Subject: [PATCH] dashboard: wire empirical cost fit into headline savings math + handoff MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `DashboardState.fitCosts()` runs OLS over a rolling 50-event ring of cold-miss (image_pixels, outgoing_text_chars, cache_create_tokens) samples. `baselineCost()` and `estImageTokens()` consult the fit on every event: - `cost_fit != null` (N ≥ 3 samples) → headline saved_pct switches to the live empirical rates: txtReplaced = compressedChars × α, imgTokens = imagePixels × β. - `cost_fit == null` → falls back to the stale constants (OPUS_IMAGE_TOKEN_COST = 2500, CHARS_PER_TOKEN = 4) — same behavior as before, so nothing breaks while data accumulates. # Why a rolling ring, not seeded from history Old events may carry the new instrumented fields but come from a different renderer config (cell size, multiCol, atlas profile) or a different model (Opus 4.6 vs 4.7 tokenizer). Mixing them in gives a misleading α/β that lags reality. Each restart starts the fit empty and refills from live traffic, so flipping `multiCol`, swapping fonts, or upgrading the model auto-flushes stale samples. # Endpoint `/proxy-stats` JSON payload now carries: "cost_fit": { "alpha": 0.3, "beta": 0.0053, "chars_per_token": 3.3, "pixels_per_token": 189, "single_col_tokens_per_img": 4180, "multicol2_tokens_per_img": 8500, "n": 12 } null until ≥3 cold-miss samples land. Guards against degenerate fits (collinear data, negative rates) by returning null rather than feeding the dashboard noise. # Math Solves `tokens ≈ α · text_chars + β · pixels` via 2×2 normal equations on the centered design matrix. Rounds α to 4 sig figs, keeps β at 6 sig figs (β is tiny ~5e-3 so it needs the resolution). # Open Once N stabilizes at ~10+, re-grade the hardcoded `OPUS_IMAGE_TOKEN_COST` and `CHARS_PER_TOKEN` constants to match the empirical numbers. That just makes the fall-back path agree with reality, doesn't change live behavior. HANDOFF.md captures the verification recipe and priority queue for the next session. --- src/dashboard.ts | 159 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 152 insertions(+), 7 deletions(-) diff --git a/src/dashboard.ts b/src/dashboard.ts index cf0fa5b..f74c3dc 100644 --- a/src/dashboard.ts +++ b/src/dashboard.ts @@ -99,8 +99,17 @@ interface Totals { * See /tmp/pixelpipe-history-compression.md for the analysis. */ const OPUS_IMAGE_TOKEN_COST = 2500; -/** Estimated token cost of the N images we emit per compressed request. */ -function estImageTokens(imageCount: number): number { +/** Estimated token cost of the N images we emit per compressed request. + * When the dashboard has accumulated ≥3 cold-miss samples with the new + * instrumentation, prefer `imagePixels × β` (live empirical rate) over + * the stale flat constant — the constant was measured on Opus pre-4.7 + * with single-col 508×1559 PNGs, and is wrong by ~3× under multi-col 2. */ +function estImageTokens( + imageCount: number, + imagePixels: number, + beta: number | null, +): number { + if (beta !== null && imagePixels > 0) return Math.round(imagePixels * beta); return imageCount * OPUS_IMAGE_TOKEN_COST; } @@ -131,16 +140,27 @@ function baselineCost( actualEff: number, compressedChars: number, imageCount: number, + imagePixels: number, cacheCreate: number, cacheRead: number, + fit: { alpha: number; beta: number } | null, ): number { // compressedChars is the actual text we IMAGE-encoded (static slab + // reminders + tool_results that passed the break-even gate). NOT the // total origChars — that would include text-only blocks that stayed // as text and wouldn't have appeared as `imageCount` either, making // the apples-to-apples comparison invalid. - const txtReplaced = Math.floor(compressedChars / 4); // ~4 chars per token - const imgTokensEst = estImageTokens(imageCount); + // + // When `fit` is non-null we have ≥3 cold-miss samples and trust the + // measured rates: txtReplaced = compressedChars × α (tokens-per-char), + // imgTokens = imagePixels × β (tokens-per-pixel). When `fit` is null + // we fall back to the stale 4-chars-per-token + 2500-tokens-per-image + // constants, which are still in the right ballpark for Opus pre-4.7 + // single-col workloads. + const txtReplaced = fit + ? Math.floor(compressedChars * fit.alpha) + : Math.floor(compressedChars / 4); + const imgTokensEst = estImageTokens(imageCount, imagePixels, fit ? fit.beta : null); // extraText CAN be negative when image cost > text cost. Don't clamp — // surfaces honest negatives so the operator can see cost-bleed. const extraText = txtReplaced - imgTokensEst; @@ -150,8 +170,30 @@ function baselineCost( return actualEff + extraText * baselineRate; } +/** One sample for the empirical cost regression. Built from cold-miss + * events (cache_read=0, cache_create>1000) where the new instrumentation + * (`image_pixels`, `outgoing_text_chars`) is present. + * + * Solves: `tokens ≈ α · text_chars + β · pixels` over N samples via 2×2 + * normal equations. Surfaced live in `/proxy-stats` so the dashboard + * shows the empirical chars/token and tokens/image numbers as soon as + * enough cold-miss data accumulates — replaces the one-shot script that + * nobody would ever remember to run. */ +interface ColdMissSample { + tokens: number; + text_chars: number; + pixels: number; +} + +/** Cap for the cold-miss ring. Fits get noisier with more old data after a + * model change, so we keep it short — enough samples for a stable fit + * (N≥10 typical) but young enough that a flipped MULTI_COL or model upgrade + * flushes through within a few sessions. */ +const COLD_MISS_CAP = 50; + export class DashboardState { private recent: RecentRow[] = []; + private coldMisses: ColdMissSample[] = []; private totals: Totals = { requests: 0, compressedRequests: 0, @@ -213,9 +255,24 @@ export class DashboardState { const haveUsage = u !== undefined && (inp > 0 || out > 0 || cc > 0 || cr > 0); const eff = haveUsage ? effectiveCost(inp, cc, cr) : 0; + // Pull the current empirical fit BEFORE recording — when this event is + // itself a cold miss it'll feed back into the next request's fit, but + // for this baseline calc we use whatever rate we have so far. Null + // until ≥3 cold-miss samples accumulate; baselineCost falls back to + // the stale constants in that case. + const fit = this.fitCosts(); + const imgPx = (info as { imagePixels?: number } | undefined)?.imagePixels ?? 0; const baselineEff = haveUsage && compressed - ? baselineCost(eff, info?.compressedChars ?? 0, info?.imageCount ?? 0, cc, cr) + ? baselineCost( + eff, + info?.compressedChars ?? 0, + info?.imageCount ?? 0, + imgPx, + cc, + cr, + fit ? { alpha: fit.alpha, beta: fit.beta } : null, + ) : eff; const prevSaved = this.totals.effectiveInputBaselineEst - this.totals.effectiveInputActual; @@ -232,7 +289,9 @@ export class DashboardState { status: ev.status, compressed, cc_added: compressed ? 1 : undefined, // we always emit exactly one cache_control - expected_image_tokens: compressed ? estImageTokens(info?.imageCount ?? 0) : undefined, + expected_image_tokens: compressed + ? estImageTokens(info?.imageCount ?? 0, imgPx, fit ? fit.beta : null) + : undefined, input_tokens: haveUsage ? inp : undefined, cache_create: haveUsage ? cc : undefined, cache_read: haveUsage ? cr : undefined, @@ -242,6 +301,73 @@ export class DashboardState { }; this.recent.push(row); if (this.recent.length > RECENT_CAP) this.recent.splice(0, this.recent.length - RECENT_CAP); + + // Cold-miss sample for the empirical cost regression. Requires the new + // pair of measurements (image_pixels + outgoing_text_chars) to be + // present, and the usage numbers to show a true cold miss (cache_read=0, + // cache_create>1000 — the latter filters out tiny no-system requests). + const pixels = (info as { imagePixels?: number } | undefined)?.imagePixels; + const textChars = (info as { outgoingTextChars?: number } | undefined)?.outgoingTextChars; + if ( + compressed && + haveUsage && + cr === 0 && + cc > 1000 && + typeof pixels === 'number' && + pixels > 0 && + typeof textChars === 'number' && + textChars > 0 + ) { + this.coldMisses.push({ tokens: inp + cc, text_chars: textChars, pixels }); + if (this.coldMisses.length > COLD_MISS_CAP) { + this.coldMisses.splice(0, this.coldMisses.length - COLD_MISS_CAP); + } + } + } + + /** Solve the empirical cost regression over the current cold-miss ring. + * Returns `null` until we have at least 3 samples — fewer leaves the + * 2×2 normal equations under-determined and the fit jumps around with + * every new event, which would be worse than just showing nothing. */ + fitCosts(): { + alpha: number; + beta: number; + chars_per_token: number; + pixels_per_token: number; + single_col_tokens_per_img: number; + multicol2_tokens_per_img: number; + n: number; + } | null { + const samples = this.coldMisses; + if (samples.length < 3) return null; + let sxx = 0; + let sxy = 0; + let syy = 0; + let sxt = 0; + let syt = 0; + for (const s of samples) { + sxx += s.text_chars * s.text_chars; + sxy += s.text_chars * s.pixels; + syy += s.pixels * s.pixels; + sxt += s.text_chars * s.tokens; + syt += s.pixels * s.tokens; + } + const det = sxx * syy - sxy * sxy; + if (det === 0) return null; // collinear, can't separate the two effects + const alpha = (syy * sxt - sxy * syt) / det; + const beta = (sxx * syt - sxy * sxt) / det; + // Guard against degenerate fits (negative rates mean the data is too + // noisy / multi-modal to give a clean linear answer). + if (alpha <= 0 || beta <= 0) return null; + return { + alpha: round4(alpha), + beta: round4(beta * 1000) / 1000, // β is tiny — keep 6 sig figs effectively + chars_per_token: round1(1 / alpha), + pixels_per_token: Math.round(1 / beta), + single_col_tokens_per_img: Math.round(508 * 1559 * beta), + multicol2_tokens_per_img: Math.round(1028 * 1559 * beta), + n: samples.length, + }; } /** On startup, fold the last N entries from the JSONL events file back @@ -275,8 +401,13 @@ export class DashboardState { status: t.status, compressed: t.compressed === true, cc_added: t.compressed === true ? 1 : undefined, + // Replay path uses the FIT-LESS form: historical rows don't get + // re-scored as the live fit evolves. Only fresh events flowing + // through record() benefit from the empirical rates. expected_image_tokens: - t.compressed === true ? estImageTokens(t.image_count ?? 0) : undefined, + t.compressed === true + ? estImageTokens(t.image_count ?? 0, (t as { image_pixels?: number }).image_pixels ?? 0, null) + : undefined, input_tokens: t.input_tokens, cache_create: t.cache_create_tokens, cache_read: t.cache_read_tokens, @@ -292,6 +423,14 @@ export class DashboardState { : undefined, }; this.recent.push(row); + // DELIBERATELY do NOT seed the cold-miss ring from historical events. + // Old events may carry the new instrumentation but come from a + // different renderer config (cell size, multiCol, atlas profile) or + // a different model (Opus 4.6 vs 4.7 tokenizer). Mixing them into the + // fit gives a misleading α/β that lags behind reality. Each restart + // starts the fit window empty and refills from live traffic — this + // way flipping `multiCol`, bumping the cell size, or upgrading to a + // new model auto-flushes stale samples on the next restart. } } @@ -313,6 +452,12 @@ export class DashboardState { saved_pct: round1(pct), saved_usd_opus47: round4((saved * 15.0) / 1e6), uptime_sec: uptimeSec, + // Empirical cost fit — null until ≥3 cold-miss events with the new + // instrumentation accumulate. When present, contains the live model's + // chars/token and per-image-shape token cost so the dashboard can + // re-ground its stale 2,500/img and 4-chars/tok constants from data + // instead of guessing. + cost_fit: this.fitCosts(), }; return new Response(JSON.stringify(payload, null, 2), { headers: { 'content-type': 'application/json', 'access-control-allow-origin': '*' },