mirror of
https://github.com/teamchong/pxpipe.git
synced 2026-07-22 02:02:51 +02:00
This commit is contained in:
+28
-74
@@ -150,21 +150,16 @@ export interface RecentRow {
|
||||
*/
|
||||
interface SessionTotals {
|
||||
sessionId: string;
|
||||
firstSeen: number; // unix seconds
|
||||
lastSeen: number; // unix seconds
|
||||
requests: number;
|
||||
// Dollar-weighted accumulators. baseline/actual are the SAME math as the
|
||||
// global `Totals.allBaselineEquivalentWeighted` / `allActualInputWeighted`
|
||||
// + `allOutputWeighted`, but scoped to a single session.
|
||||
// global `Totals.allBaselineEquivalentWeighted` / `allActualInputWeighted`,
|
||||
// but scoped to a single session. Output is excluded because the proxy
|
||||
// doesn't touch it.
|
||||
baselineInputWeighted: number;
|
||||
actualInputWeighted: number;
|
||||
outputWeighted: number;
|
||||
// Per-bucket char attribution from ev.info.bucketChars.
|
||||
bucketChars: { [bucket: string]: number };
|
||||
// Passthrough reason histogram from ev.info.passthroughReasons.
|
||||
passthroughReasons: Record<string, number>;
|
||||
passthroughRequests: number; // count of requests where !ev.info.compressed
|
||||
compressedRequests: number;
|
||||
// Honest denominator for the headline: count of requests that contributed
|
||||
// to `baselineInputWeighted` (events that carried a baseline measurement,
|
||||
// matching the global `Totals.baselineMeasuredCount`).
|
||||
baselineMeasuredCount: number;
|
||||
}
|
||||
|
||||
interface Totals {
|
||||
@@ -512,59 +507,30 @@ export class DashboardState {
|
||||
if (!s) {
|
||||
s = {
|
||||
sessionId: sid,
|
||||
firstSeen: Date.now() / 1000,
|
||||
lastSeen: Date.now() / 1000,
|
||||
requests: 0,
|
||||
baselineInputWeighted: 0,
|
||||
actualInputWeighted: 0,
|
||||
outputWeighted: 0,
|
||||
bucketChars: {},
|
||||
passthroughReasons: {},
|
||||
passthroughRequests: 0,
|
||||
compressedRequests: 0,
|
||||
baselineMeasuredCount: 0,
|
||||
};
|
||||
this.sessions.set(sid, s);
|
||||
// Cap memory — drop the oldest session by lastSeen when over budget.
|
||||
// Cap memory — drop the first (oldest by insertion order) session
|
||||
// when over budget. We no longer track lastSeen privately on the
|
||||
// class — insertion order is a fine proxy because `currentSessionId`
|
||||
// (set above on every update) is what the serve path uses to pick
|
||||
// the most-recent session, not a scan of `this.sessions`.
|
||||
if (this.sessions.size > DashboardState.SESSION_CAP) {
|
||||
const oldest = [...this.sessions.values()].sort(
|
||||
(a, b) => a.lastSeen - b.lastSeen,
|
||||
)[0];
|
||||
if (oldest) this.sessions.delete(oldest.sessionId);
|
||||
const firstKey = this.sessions.keys().next().value;
|
||||
if (firstKey !== undefined) this.sessions.delete(firstKey);
|
||||
}
|
||||
}
|
||||
s.lastSeen = Date.now() / 1000;
|
||||
s.requests += 1;
|
||||
// Reuse the same haveUsage / haveBaseline guards + the
|
||||
// baselineInputEff / actualInputEff / outputEquiv locals computed
|
||||
// earlier in update() so the lifetime totals block (above) and the
|
||||
// per-session block (here) read the same values. Re-deriving them
|
||||
// here would duplicate the cache-aware-baseline math and invite drift.
|
||||
// baselineInputEff / actualInputEff locals computed earlier in
|
||||
// update() so the lifetime totals block (above) and the per-session
|
||||
// block (here) read the same values. Re-deriving them here would
|
||||
// duplicate the cache-aware-baseline math and invite drift.
|
||||
if (haveBaseline && haveUsage) {
|
||||
s.baselineInputWeighted += baselineInputEff;
|
||||
s.actualInputWeighted += actualInputEff;
|
||||
s.outputWeighted += outputEquiv;
|
||||
}
|
||||
if (compressed) s.compressedRequests += 1;
|
||||
else s.passthroughRequests += 1;
|
||||
// Per-bucket char attribution from `info.bucketChars` (flat
|
||||
// `Partial<Record<BucketName, number>>` — see src/core/tracker.ts).
|
||||
// Matches the shape the Svelte panel iterates over.
|
||||
const bc = info?.bucketChars;
|
||||
if (bc && typeof bc === 'object') {
|
||||
for (const [key, val] of Object.entries(bc) as [string, unknown][]) {
|
||||
if (typeof val === 'number' && Number.isFinite(val)) {
|
||||
s.bucketChars[key] = (s.bucketChars[key] ?? 0) + val;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Passthrough reason histogram from `info.passthroughReasons`.
|
||||
const pr = info?.passthroughReasons;
|
||||
if (pr && typeof pr === 'object') {
|
||||
for (const [reason, count] of Object.entries(pr as Record<string, unknown>)) {
|
||||
if (typeof count === 'number' && Number.isFinite(count) && count > 0) {
|
||||
s.passthroughReasons[reason] = (s.passthroughReasons[reason] ?? 0) + count;
|
||||
}
|
||||
}
|
||||
s.baselineMeasuredCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -695,28 +661,16 @@ export class DashboardState {
|
||||
if (!s) {
|
||||
return jsonResponse({ sessionId: null, message: 'no active session yet' });
|
||||
}
|
||||
// Dollar-weighted savings ratio. Same math as the global `saved_pct`
|
||||
// (compressed AND passthrough requests — passthrough has baseline=actual,
|
||||
// so it correctly drags the ratio down when we leak). Honest denominator:
|
||||
// includes both compressed and uncompressed flows that the proxy saw.
|
||||
const baselineUsd = (s.baselineInputWeighted * ASSUMED_INPUT_USD_PER_MTOK) / 1e6;
|
||||
const actualUsd = (s.actualInputWeighted * ASSUMED_INPUT_USD_PER_MTOK) / 1e6;
|
||||
const savedUsd = baselineUsd - actualUsd;
|
||||
const savedPct = baselineUsd > 0 ? (savedUsd / baselineUsd) * 100 : 0;
|
||||
// Minimal headline payload: the same dollar-weighted baseline/actual
|
||||
// input accumulators the global Totals block exposes, plus the honest
|
||||
// denominator (`baselineMeasuredCount` — only requests that carried a
|
||||
// baseline measurement). The Svelte panel does the savings math itself
|
||||
// so we don't round-trip dollar values through the wire.
|
||||
return jsonResponse({
|
||||
sessionId: s.sessionId,
|
||||
firstSeen: s.firstSeen,
|
||||
lastSeen: s.lastSeen,
|
||||
uptimeSec: Math.max(0, s.lastSeen - s.firstSeen),
|
||||
requests: s.requests,
|
||||
compressedRequests: s.compressedRequests,
|
||||
passthroughRequests: s.passthroughRequests,
|
||||
baselineUsd: round4(baselineUsd),
|
||||
actualUsd: round4(actualUsd),
|
||||
savedUsd: round4(savedUsd),
|
||||
savedPct: round1(savedPct),
|
||||
bucketChars: s.bucketChars,
|
||||
passthroughReasons: s.passthroughReasons,
|
||||
baselineInputWeighted: s.baselineInputWeighted,
|
||||
actualInputWeighted: s.actualInputWeighted,
|
||||
baselineMeasuredCount: s.baselineMeasuredCount,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,263 +1,66 @@
|
||||
<script lang="ts">
|
||||
// Top-of-dashboard headline. Dollar-weighted savings ratio scoped to the
|
||||
// most-recently-active session, with per-bucket breakdown of where the
|
||||
// savings came from and a list of passthrough reasons for requests we
|
||||
// didn't compress. Polls 2s via the `currentSession` store.
|
||||
// One-line headline for the current session: dollar-weighted savings ratio.
|
||||
//
|
||||
// Math (no cherry-pick):
|
||||
// saved_$ = Σ baseline_$ − Σ actual_$ (over requests where we measured baseline)
|
||||
// saved_% = saved_$ / Σ baseline_$
|
||||
//
|
||||
// `baseline_$` is the cache-aware bill Anthropic would have charged for the
|
||||
// uncompressed body with the SAME cache_control markers Claude Code sent —
|
||||
// measured by the counterfactual baseline probe on a sampled subset of
|
||||
// requests. Caching savings stay credited to Claude Code; what remains is
|
||||
// proxy-attributable savings only.
|
||||
//
|
||||
// Unmeasured requests (probe skipped, passthroughs) don't enter either sum.
|
||||
// We only report what we measured.
|
||||
import { currentSession } from '../stores/index.js';
|
||||
|
||||
$: data = $currentSession.data;
|
||||
$: err = $currentSession.error;
|
||||
$: hasSession = data && data.sessionId != null;
|
||||
|
||||
$: baselineUsd = data?.baselineInputWeighted ?? 0;
|
||||
$: actualUsd = data?.actualInputWeighted ?? 0;
|
||||
$: savedUsd = Math.max(0, baselineUsd - actualUsd);
|
||||
$: savedPct = baselineUsd > 0 ? (savedUsd / baselineUsd) * 100 : 0;
|
||||
$: measuredReqs = data?.baselineMeasuredCount ?? 0;
|
||||
|
||||
function fmtUsd(n: number): string {
|
||||
return '$' + n.toFixed(2);
|
||||
}
|
||||
function fmtPct(n: number): string {
|
||||
return n.toFixed(1) + '%';
|
||||
}
|
||||
function fmtDuration(sec: number): string {
|
||||
if (sec < 60) return Math.round(sec) + 's';
|
||||
const m = Math.floor(sec / 60);
|
||||
if (m < 60) return m + 'm';
|
||||
const h = Math.floor(m / 60);
|
||||
return h + 'h ' + (m % 60) + 'm';
|
||||
}
|
||||
function fmtChars(n: number): string {
|
||||
if (n < 1000) return String(n);
|
||||
if (n < 1_000_000) return (n / 1000).toFixed(1) + 'k';
|
||||
return (n / 1_000_000).toFixed(1) + 'M';
|
||||
}
|
||||
|
||||
$: bucketTotal = data?.bucketChars
|
||||
? Object.values(data.bucketChars).reduce((a, b) => a + b, 0)
|
||||
: 0;
|
||||
$: bucketEntries = data?.bucketChars
|
||||
? (Object.entries(data.bucketChars) as [string, number][])
|
||||
.filter(([, v]) => v > 0)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
: [];
|
||||
$: passthroughEntries = data?.passthroughReasons
|
||||
? (Object.entries(data.passthroughReasons) as [string, number][])
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
: [];
|
||||
|
||||
// Static key→label / key→color maps. The Record type matches the spec's
|
||||
// `bucketChars` shape so any new bucket added in tracker.ts will show up
|
||||
// in legend order. Hex codes chosen to match the legacy panel palette.
|
||||
const BUCKET_LABELS: Record<string, string> = {
|
||||
static_slab: 'slab',
|
||||
reminder: 'reminder',
|
||||
tool_result: 'tool_result',
|
||||
history: 'history',
|
||||
billing: 'billing',
|
||||
dynamic: 'dynamic',
|
||||
};
|
||||
const BUCKET_COLORS: Record<string, string> = {
|
||||
static_slab: '#b9be0',
|
||||
reminder: '#85c5f6',
|
||||
tool_result: '#10b981',
|
||||
history: '#f59e0b',
|
||||
billing: '#8e7681',
|
||||
dynamic: '#ef4444',
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="panel session-summary">
|
||||
{#if err}
|
||||
<div class="error">error: {err}</div>
|
||||
{:else if !data}
|
||||
<div class="loading">loading…</div>
|
||||
{:else if !hasSession}
|
||||
<h2>This session</h2>
|
||||
<div class="empty">{data.message ?? 'no active session yet'}</div>
|
||||
{:else}
|
||||
<div class="header">
|
||||
<h2>This session</h2>
|
||||
<div class="sub">
|
||||
started {fmtDuration(data.uptimeSec ?? 0)} ago · {data.requests} requests
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="headline">
|
||||
<div class="big">
|
||||
Saved <span class="pct">{fmtPct(data.savedPct ?? 0)}</span>
|
||||
</div>
|
||||
<div class="usd">
|
||||
{fmtUsd(data.savedUsd ?? 0)} of {fmtUsd(data.baselineUsd ?? 0)} baseline
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bar">
|
||||
<div class="bar-fill" style="width: {Math.max(0, Math.min(100, data.savedPct ?? 0))}%"></div>
|
||||
</div>
|
||||
|
||||
{#if bucketEntries.length > 0}
|
||||
<div class="section">
|
||||
<div class="section-label">Where it came from</div>
|
||||
<div class="bucket-bar">
|
||||
{#each bucketEntries as [key, val]}
|
||||
{@const w = bucketTotal > 0 ? (val / bucketTotal) * 100 : 0}
|
||||
<div
|
||||
class="bucket-seg"
|
||||
style="width: {w}%; background: {BUCKET_COLORS[key] ?? '#8e7681'}"
|
||||
title="{BUCKET_LABELS[key] ?? key}: {fmtChars(val)} chars ({w.toFixed(1)}%)"
|
||||
></div>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="bucket-legend">
|
||||
{#each bucketEntries as [key, val]}
|
||||
{@const w = bucketTotal > 0 ? (val / bucketTotal) * 100 : 0}
|
||||
<span class="legend-item">
|
||||
<span class="swatch" style="background: {BUCKET_COLORS[key] ?? '#8e7681'}"></span>
|
||||
{BUCKET_LABELS[key] ?? key} {w.toFixed(0)}%
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if passthroughEntries.length > 0}
|
||||
<div class="section">
|
||||
<div class="section-label">
|
||||
Where it didn't · {data.passthroughRequests ?? 0} of {data.requests} requests passed through
|
||||
</div>
|
||||
<ul class="passthrough-list">
|
||||
{#each passthroughEntries as [reason, count]}
|
||||
<li><span class="reason">{reason}</span> <span class="count">{count}</span></li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{:else if (data.passthroughRequests ?? 0) === 0}
|
||||
<div class="section">
|
||||
<div class="section-label-good">✓ Every request compressed</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{#if err}
|
||||
<div class="line muted">session: {err}</div>
|
||||
{:else if measuredReqs > 0}
|
||||
<div class="line">
|
||||
<span class="label">THIS SESSION</span>
|
||||
— saved <span class="num">{fmtUsd(savedUsd)}</span>
|
||||
(<span class="num">{savedPct.toFixed(1)}%</span>)
|
||||
· <span class="muted">{measuredReqs} requests</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.session-summary {
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
.line {
|
||||
font-size: 14px;
|
||||
color: #c9d1d9;
|
||||
margin-bottom: 12px;
|
||||
padding: 8px 12px;
|
||||
background: #161b22;
|
||||
border: 1px solid #30363d;
|
||||
border-radius: 6px;
|
||||
}
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
.label {
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: #8e7681;
|
||||
color: #8b949e;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.sub {
|
||||
font-size: 12px;
|
||||
color: #8e7681;
|
||||
}
|
||||
.headline {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.big {
|
||||
font-size: 18px;
|
||||
.num {
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: #3fb950;
|
||||
font-weight: 600;
|
||||
color: #c9d1d9;
|
||||
}
|
||||
.pct {
|
||||
color: #3fb950;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.usd {
|
||||
font-size: 12px;
|
||||
color: #8e7681;
|
||||
margin-top: 2px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.bar {
|
||||
margin-top: 8px;
|
||||
height: 6px;
|
||||
background: #21262d;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.bar-fill {
|
||||
height: 100%;
|
||||
background: #3fb950;
|
||||
transition: width 1s ease;
|
||||
}
|
||||
.section {
|
||||
margin-top: 16px;
|
||||
}
|
||||
.section-label {
|
||||
font-size: 11px;
|
||||
color: #8e7681;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.section-label-good {
|
||||
color: #3fb950;
|
||||
text-transform: none;
|
||||
letter-spacing: 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
.bucket-bar {
|
||||
display: flex;
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background: #21262d;
|
||||
}
|
||||
.bucket-seg {
|
||||
height: 100%;
|
||||
transition: width 1s ease;
|
||||
}
|
||||
.bucket-legend {
|
||||
margin-top: 6px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
font-size: 11px;
|
||||
color: #c9d1d9;
|
||||
}
|
||||
.legend-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.swatch {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.passthrough-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
font-size: 12px;
|
||||
color: #c9d1d9;
|
||||
}
|
||||
.passthrough-list li {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 2px 0;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.reason {
|
||||
color: #8e7681;
|
||||
}
|
||||
.count {
|
||||
color: #8e7681;
|
||||
}
|
||||
.loading, .empty, .error {
|
||||
color: #8e7681;
|
||||
font-size: 12px;
|
||||
}
|
||||
.error {
|
||||
color: #f85149;
|
||||
.muted {
|
||||
color: #6e7681;
|
||||
}
|
||||
</style>
|
||||
|
||||
+6
-20
@@ -153,27 +153,13 @@ export interface CompressionToggleResponse {
|
||||
compression_enabled: boolean;
|
||||
}
|
||||
|
||||
/** /api/current-session.json payload — per-session aggregates for the most-recently-active Claude Code session. */
|
||||
/** /api/current-session.json payload — per-session aggregates for the most-recently-active Claude Code session.
|
||||
* Pruned to only the fields the minimal SessionSummary.svelte headline reads:
|
||||
* dollar-weighted baseline/actual input + the measured-request count. */
|
||||
export interface CurrentSessionPayload {
|
||||
sessionId: string | null;
|
||||
message?: string;
|
||||
firstSeen?: number;
|
||||
lastSeen?: number;
|
||||
uptimeSec?: number;
|
||||
requests?: number;
|
||||
compressedRequests?: number;
|
||||
passthroughRequests?: number;
|
||||
baselineUsd?: number;
|
||||
actualUsd?: number;
|
||||
savedUsd?: number;
|
||||
savedPct?: number;
|
||||
bucketChars?: {
|
||||
static_slab: number;
|
||||
reminder: number;
|
||||
tool_result: number;
|
||||
history: number;
|
||||
billing: number;
|
||||
dynamic: number;
|
||||
};
|
||||
passthroughReasons?: Record<string, number>;
|
||||
baselineInputWeighted?: number;
|
||||
actualInputWeighted?: number;
|
||||
baselineMeasuredCount?: number;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user