diff --git a/src/lib/render.ts b/src/lib/render.ts
index 7139787..bf3d30b 100644
--- a/src/lib/render.ts
+++ b/src/lib/render.ts
@@ -1,5 +1,6 @@
import { type GpuMetricKind } from "./metrics-parser";
import { type ModelRuntimeState } from "./inflight-tracker";
+import { type UsageStats } from "./stats";
import { escapeXml, shorten } from "./util";
export function svgDataUrl(svg: string): string {
@@ -188,3 +189,69 @@ function frame(bg: string, parts: string[]): string {
${parts.join("\n ")}
`;
}
+
+export function formatCompact(n: number): string {
+ if (!Number.isFinite(n)) return "--";
+ if (n < 1000) return `${Math.round(n)}`;
+ if (n < 1_000_000) return `${(n / 1000).toFixed(1)}k`;
+ return `${(n / 1_000_000).toFixed(1)}M`;
+}
+
+export interface UsageRenderOptions {
+ modelName: string;
+ stats?: UsageStats;
+ primaryStat: "requests" | "input_tokens" | "output_tokens" | "gen_p95";
+ offline: boolean;
+}
+
+export function renderUsage(opts: UsageRenderOptions): string {
+ const name = escapeXml(shorten(opts.modelName === "all" ? "ALL MODELS" : opts.modelName));
+
+ if (opts.offline) {
+ return frame("#10131a", [
+ centerText("!!", 34, 20, "bold", "#e0e0e0"),
+ centerText("OFFLINE", 52, 9, "normal", "#bdbdbd"),
+ centerText(name, 64, 7, "normal", "#ffffff"),
+ ]);
+ }
+
+ const s = opts.stats;
+ const big = s ? formatCompact(primaryValue(s, opts.primaryStat)) : "--";
+ const rows = usageRows(s, opts.primaryStat);
+ const parts: string[] = [
+ centerText(name, 10, 7, "normal", "#ffffff"),
+ centerText(big, 36, 24, "bold", "#ffffff"),
+ ];
+ for (const row of rows) {
+ parts.push(
+ `${row.label}`,
+ `${row.value}`,
+ );
+ }
+ return frame("#10131a", parts);
+}
+
+function primaryValue(s: UsageStats, primary: "requests" | "input_tokens" | "output_tokens" | "gen_p95"): number {
+ switch (primary) {
+ case "requests":
+ return s.totalRequests;
+ case "input_tokens":
+ return s.totalInputTokens;
+ case "output_tokens":
+ return s.totalOutputTokens;
+ case "gen_p95":
+ return s.genP95;
+ }
+}
+
+function usageRows(
+ s: UsageStats | undefined,
+ primary: "requests" | "input_tokens" | "output_tokens" | "gen_p95",
+): { label: string; value: string; y: number }[] {
+ const items: { label: string; value: string }[] = [];
+ if (primary !== "requests") items.push({ label: "REQ", value: s ? formatCompact(s.totalRequests) : "--" });
+ if (primary !== "input_tokens") items.push({ label: "IN", value: s ? formatCompact(s.totalInputTokens) : "--" });
+ if (primary !== "output_tokens") items.push({ label: "OUT", value: s ? formatCompact(s.totalOutputTokens) : "--" });
+ if (primary !== "gen_p95") items.push({ label: "P95", value: s ? `${Math.round(s.genP95)} t/s` : "--" });
+ return items.map((it, i) => ({ ...it, y: 52 + i * 7 }));
+}
diff --git a/tests/render.test.ts b/tests/render.test.ts
index 45d2e52..6692d6d 100644
--- a/tests/render.test.ts
+++ b/tests/render.test.ts
@@ -1,6 +1,6 @@
import assert from "node:assert/strict";
import { test } from "node:test";
-import { renderGpuGraph, renderInflight, svgDataUrl } from "../src/lib/render";
+import { renderGpuGraph, renderInflight, renderUsage, svgDataUrl, formatCompact } from "../src/lib/render";
test("svgDataUrl wraps an SVG as a base64 data URL", () => {
const url = svgDataUrl("");
@@ -99,3 +99,37 @@ test("renderGpuGraph: single-point history avoids divide-by-zero", () => {
const svg = renderGpuGraph({ gpuName: "ALL GPUS", metric: "util_percent", value: 50, history: [50], offline: false });
assert.match(svg, /