TypeScript 97.4%
SQL 1%
JavaScript 0.9%
CSS 0.6%
1import type { PolyModel } from "@/lib/client/types";2import { estimateCost, estimateTextTokens } from "@/lib/client/tokens";34/**5 * Live per-response metrics for the Arena. While a model streams we estimate output tokens from the6 * character count and price them with `estimateCost`; once the `done` frame arrives the exact usage7 * and cost recorded by the server replace the estimate (`exact: true`).8 */9export type ArenaStatus = "idle" | "waiting" | "thinking" | "streaming" | "done" | "error" | "stopped";1011export interface MetricsSource {12 status: ArenaStatus;13 text: string;14 reasoning?: string;15 startedAt: number;16 firstTokenAt?: number;17 response: {18 ttftMs?: number | null;19 latencyMs?: number | null;20 costUsd?: number | null;21 usage?: { inputTokens?: number; outputTokens?: number; reasoningTokens?: number; cachedInputTokens?: number } | null;22 } | null;23}2425export interface LiveMetrics {26 status: ArenaStatus;27 /** Wall-clock time since the request started (or total latency once final). */28 elapsedMs: number;29 ttftMs: number | null;30 inputTokens: number | null;31 outputTokens: number | null;32 reasoningTokens: number | null;33 cachedTokens: number | null;34 tokensPerSecond: number | null;35 costUsd: number | null;36 /** True when tokens/cost come from the provider, false while estimated client-side. */37 exact: boolean;38}3940export function isFinalStatus(s: ArenaStatus): boolean {41 return s === "done" || s === "error" || s === "stopped";42}4344/**45 * @param inputTokensEstimate client estimate of the prompt (+system prompt, attachments) in tokens46 * @param now current timestamp — pass it in so renders stay pure (ticked by a timer component)47 */48export function liveMetrics(src: MetricsSource, model: PolyModel | null | undefined, inputTokensEstimate: number, now: number): LiveMetrics {49 const final = isFinalStatus(src.status);50 const r = src.response;51 if (final && r && (r.usage || typeof r.costUsd === "number" || typeof r.latencyMs === "number")) {52 const u = r.usage ?? null;53 const out = u?.outputTokens ?? null;54 const gen = typeof r.latencyMs === "number" ? Math.max(1, r.latencyMs - (r.ttftMs ?? 0)) : null;55 return {56 status: src.status,57 elapsedMs: r.latencyMs ?? Math.max(0, now - src.startedAt),58 ttftMs: r.ttftMs ?? null,59 inputTokens: u?.inputTokens ?? null,60 outputTokens: out,61 reasoningTokens: u?.reasoningTokens ?? null,62 cachedTokens: u?.cachedInputTokens ?? null,63 tokensPerSecond: out && gen ? Math.round((out / gen) * 1000) : null,64 costUsd: typeof r.costUsd === "number" ? r.costUsd : null,65 exact: true,66 };67 }68 const outputTokens = src.text ? estimateTextTokens(src.text) + (src.reasoning ? estimateTextTokens(src.reasoning) : 0) : null;69 const ttftMs = src.firstTokenAt ? Math.max(0, src.firstTokenAt - src.startedAt) : null;70 const genMs = src.firstTokenAt ? Math.max(1, now - src.firstTokenAt) : null;71 const tokensPerSecond = outputTokens && genMs && genMs > 400 ? Math.round((outputTokens / genMs) * 1000) : null;72 const cost = outputTokens !== null || src.status !== "idle" ? estimateCost(model, inputTokensEstimate, outputTokens ?? 0).usd : null;73 return {74 status: src.status,75 elapsedMs: Math.max(0, now - src.startedAt),76 ttftMs,77 inputTokens: inputTokensEstimate || null,78 outputTokens,79 reasoningTokens: null,80 cachedTokens: null,81 tokensPerSecond,82 costUsd: cost,83 exact: false,84 };85}8687/** Signed delta formatting helper: "−$0.0031", "+120 ms", "−340 tok". */88export function formatDelta(value: number | null, kind: "usd" | "ms" | "tokens"): string {89 if (value === null || !Number.isFinite(value)) return "—";90 const sign = value < 0 ? "−" : value > 0 ? "+" : "±";91 const abs = Math.abs(value);92 if (kind === "usd") return `${sign}$${abs < 0.01 ? abs.toFixed(abs < 0.001 ? 5 : 4) : abs.toFixed(2)}`;93 if (kind === "ms") return `${sign}${abs < 1000 ? `${Math.round(abs)} ms` : `${(abs / 1000).toFixed(abs < 10_000 ? 2 : 1)} s`}`;94 return `${sign}${abs >= 10_000 ? `${(abs / 1000).toFixed(1)}K` : Math.round(abs)} tok`;95}96