diff options
| author | Adam Malczewski <[email protected]> | 2026-06-10 10:06:27 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-10 10:06:27 +0900 |
| commit | f8bf715abc8a89ec0c6370b40403c509b1ce2870 (patch) | |
| tree | 915600a766e042a8491ac57423542cde1dda1eb6 /src/core/metrics/format.ts | |
| parent | ccfd2f4157c1cbbb3d8aeceee94d9e963a82ab03 (diff) | |
| download | dispatch-web-f8bf715abc8a89ec0c6370b40403c509b1ce2870.tar.gz dispatch-web-f8bf715abc8a89ec0c6370b40403c509b1ce2870.zip | |
feat(metrics): per-turn + per-step token/timing metrics bubbles
Consume [email protected] / [email protected] metrics: usage.stepId,
step-complete (ttft/decode/genTotal), done.durationMs/usage, and the
durable GET /conversations/:id/metrics endpoint.
- core/metrics: pure live-fold + durable-merge reducer; decode-rate TPS;
head-aligned, stable placement; progressive per-step rows (each shown as
its step ends) with the turn-total row gated on the done event.
- features/chat: store folds metric events + hydrates durable TurnMetrics;
ChatView renders inline step bubbles + a turn-total bubble.
- app: MetricsSync HTTP effect (tolerates 404) injected into chat stores.
- scripts/live-probe: drives the metrics path; live-verified 17/17 vs bin/up.
- docs: regenerate .dispatch wire/transport mirrors to 0.4.0; glossary terms
(turn/step metrics, TTFT, decode time, TPS, metrics bubble); trim handoff.
Diffstat (limited to 'src/core/metrics/format.ts')
| -rw-r--r-- | src/core/metrics/format.ts | 69 |
1 files changed, 69 insertions, 0 deletions
diff --git a/src/core/metrics/format.ts b/src/core/metrics/format.ts new file mode 100644 index 0000000..3a4078c --- /dev/null +++ b/src/core/metrics/format.ts @@ -0,0 +1,69 @@ +import type { StepMetrics, TurnMetrics, Usage } from "@dispatch/wire"; +import type { StepMetricsView, TurnMetricsView } from "./types"; + +function formatTokens(n: number): string { + return n.toLocaleString("en-US"); +} + +function formatDuration(ms: number | undefined): string | null { + if (ms === undefined || ms <= 0) return null; + if (ms < 1000) return `${Math.round(ms)}ms`; + return `${(ms / 1000).toFixed(1)}s`; +} + +function formatTps(tps: number | null): string | null { + if (tps === null) return null; + if (tps < 10) return `${tps.toFixed(1)} tok/s`; + return `${Math.round(tps)} tok/s`; +} + +/** Compute tokens-per-second. Returns null when elapsed time is absent or zero. */ +export function computeTps(outputTokens: number, elapsedMs: number | undefined): number | null { + if (elapsedMs === undefined || elapsedMs <= 0) return null; + return outputTokens / (elapsedMs / 1000); +} + +function totalTokens(u: Usage): number { + return u.inputTokens + u.outputTokens; +} + +function formatBreakdown(u: Usage): string { + let s = `${formatTokens(u.inputTokens)} in / ${formatTokens(u.outputTokens)} out`; + if (u.cacheReadTokens !== undefined && u.cacheReadTokens > 0) { + s += ` / ${formatTokens(u.cacheReadTokens)} cache`; + } + return s; +} + +/** Build a formatted view of a single step's metrics. */ +export function viewStepMetrics(step: StepMetrics, index: number): StepMetricsView { + const total = totalTokens(step.usage); + const tps = computeTps(step.usage.outputTokens, step.decodeMs ?? step.genTotalMs); + return { + label: `step ${index + 1}`, + tokensLabel: `${formatTokens(total)} tok`, + tps: formatTps(tps), + ttft: formatDuration(step.ttftMs), + decode: formatDuration(step.decodeMs), + genTotal: formatDuration(step.genTotalMs), + }; +} + +/** Build a formatted view of a turn's aggregate metrics. */ +export function viewTurnMetrics(turn: TurnMetrics): TurnMetricsView { + const total = totalTokens(turn.usage); + let totalGenMs: number | undefined; + for (const step of turn.steps) { + const stepMs = step.decodeMs ?? step.genTotalMs; + if (stepMs !== undefined) { + totalGenMs = (totalGenMs ?? 0) + stepMs; + } + } + const tps = computeTps(turn.usage.outputTokens, totalGenMs); + return { + tokensLabel: `${formatTokens(total)} tok`, + breakdown: formatBreakdown(turn.usage), + tps: formatTps(tps), + duration: formatDuration(turn.durationMs), + }; +} |
