From 37f7268509d051cfa0fcc25030885aed76b48ac1 Mon Sep 17 00:00:00 2001 From: c4ch3c4d3 <23181631+c4ch3c4d3@users.noreply.github.com> Date: Fri, 14 Aug 2026 12:47:53 -0600 Subject: [PATCH] feat: add usage stats parsing and fetching from /api/metrics/stats --- src/lib/stats.ts | 33 ++++++++++++++++++++++++++++ tests/stats.test.ts | 52 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 src/lib/stats.ts create mode 100644 tests/stats.test.ts diff --git a/src/lib/stats.ts b/src/lib/stats.ts new file mode 100644 index 0000000..5793edf --- /dev/null +++ b/src/lib/stats.ts @@ -0,0 +1,33 @@ +import { type LlamaSwapConfig } from "./util"; + +export interface UsageStats { + totalRequests: number; + totalInputTokens: number; + totalOutputTokens: number; + genP95: number; +} + +export function parseStats(json: unknown): UsageStats | null { + if (!json || typeof json !== "object") return null; + const obj = json as Record; + const genHist = (obj.gen_histogram ?? {}) as Record; + const num = (v: unknown): number => (typeof v === "number" && Number.isFinite(v) ? v : 0); + return { + totalRequests: num(obj.total_requests), + totalInputTokens: num(obj.total_input_tokens), + totalOutputTokens: num(obj.total_output_tokens), + genP95: num(genHist.p95), + }; +} + +export async function fetchStats(cfg: LlamaSwapConfig, modelId: string): Promise { + const query = modelId === "all" ? "" : `?model=${encodeURIComponent(modelId)}`; + const headers: Record = {}; + if (cfg.apiKey) headers["Authorization"] = `Bearer ${cfg.apiKey}`; + const res = await fetch(`${cfg.baseUrl}/api/metrics/stats${query}`, { + headers, + signal: AbortSignal.timeout(5000), + }); + if (!res.ok) return null; + return parseStats(await res.json()); +} diff --git a/tests/stats.test.ts b/tests/stats.test.ts new file mode 100644 index 0000000..bf35418 --- /dev/null +++ b/tests/stats.test.ts @@ -0,0 +1,52 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; +import { fetchStats, parseStats, type UsageStats } from "../src/lib/stats"; + +test("parseStats extracts totals and gen p95", () => { + const stats = parseStats({ + total_requests: 1985, + total_input_tokens: 312327705, + total_output_tokens: 932710, + total_cache_tokens: 308144640, + gen_histogram: { p50: 341, p95: 378.86, p99: 392 }, + }); + assert.deepEqual(stats, { totalRequests: 1985, totalInputTokens: 312327705, totalOutputTokens: 932710, genP95: 378.86 }); +}); + +test("parseStats defaults missing fields and rejects malformed input", () => { + assert.deepEqual(parseStats({}), { totalRequests: 0, totalInputTokens: 0, totalOutputTokens: 0, genP95: 0 }); + assert.equal(parseStats({ total_requests: "nope" })!.totalRequests, 0); + assert.equal(parseStats(null), null); + assert.equal(parseStats("x"), null); +}); + +test("fetchStats calls /api/metrics/stats with a model query", async () => { + const calls: string[] = []; + const orig = globalThis.fetch; + globalThis.fetch = (async (url: RequestInfo | URL) => { + calls.push(String(url)); + return { ok: true, json: async () => ({ total_requests: 7 }) } as unknown as Response; + }) as typeof fetch; + try { + await fetchStats({ baseUrl: "http://x" }, "DeepSeek-V4-Flash-0731"); + assert.equal(calls[0], "http://x/api/metrics/stats?model=DeepSeek-V4-Flash-0731"); + } finally { + globalThis.fetch = orig; + } +}); + +test("fetchStats omits the model query for 'all' and returns null on HTTP error", async () => { + const calls: string[] = []; + const orig = globalThis.fetch; + globalThis.fetch = (async (url: RequestInfo | URL) => { + calls.push(String(url)); + return { ok: false, status: 500 } as unknown as Response; + }) as typeof fetch; + try { + const stats = await fetchStats({ baseUrl: "http://x" }, "all"); + assert.equal(stats, null); + assert.equal(calls[0], "http://x/api/metrics/stats"); + } finally { + globalThis.fetch = orig; + } +});