diff --git a/src/app/api/compression/replay/__tests__/route.test.ts b/src/app/api/compression/replay/__tests__/route.test.ts new file mode 100644 index 000000000000..7dba14e56cd9 --- /dev/null +++ b/src/app/api/compression/replay/__tests__/route.test.ts @@ -0,0 +1,457 @@ +/** + * TDD for GET /api/compression/replay — the recent compression run history + * endpoint that backs the studio's "replay" panel. + * + * It returns compression_analytics rows (newest first), with optional + * provider / model / windowMs query parameters plus the per-engine breakdown + * rows for each run. See src/app/api/compression/replay/route.ts and the + * live event shape in open-sse/services/compression/engineBreakdown.ts. + * + * Auth: requireManagementAuth — not enforced in the unconfigured/first-run + * state (fresh temp DATA_DIR, no INITIAL_PASSWORD), so these exercise the + * 200 happy path. The DB helper (getRecentCompressionAnalyticsRuns) is + * covered separately so this file is focused on the route wiring. + * + * Run: node --import tsx/esm --test src/app/api/compression/replay/__tests__/route.test.ts + */ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +// ─── isolated temp DB ──────────────────────────────────────────────────────── + +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-compression-replay-")); +const ORIGINAL_DATA_DIR = process.env.DATA_DIR; +const ORIGINAL_INITIAL_PASSWORD = process.env.INITIAL_PASSWORD; +process.env.DATA_DIR = TEST_DATA_DIR; +delete process.env.INITIAL_PASSWORD; + +const core = await import("@/lib/db/core.ts"); +core.resetDbInstance(); + +const { + insertCompressionAnalyticsRow, + insertCompressionEngineBreakdown, + getRecentCompressionAnalyticsRuns, +} = await import("@/lib/db/compressionAnalytics.ts"); + +const route = await import("../route.ts"); + +// ─── helpers ───────────────────────────────────────────────────────────────── + +function get(url: string): Request { + return new Request(url, { method: "GET" }); +} + +function resetDb(): void { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); + fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); +} + +async function readJson(res: Response): Promise { + return (await res.json()) as T; +} + +interface ReplayResponseBody { + runs: Array<{ + id: number; + timestamp: string; + mode: string; + engine: string | null; + provider: string | null; + modelHint: string | null; + requestId: string | null; + originalTokens: number; + compressedTokens: number; + tokensSaved: number; + savingsPercent: number; + durationMs: number | null; + validationFallback: boolean; + receiptSource: string | null; + estimatedUsdSaved: number | null; + actualTotalTokens: number | null; + engineBreakdown: Array<{ + engine: string; + originalTokens: number; + compressedTokens: number; + }>; + }>; + count: number; + filters: { provider: string | null; model: string | null; windowMs: number; limit: number }; + windowStart: string; + now: string; +} + +// ─── lifecycle ─────────────────────────────────────────────────────────────── + +test.beforeEach(() => { + resetDb(); +}); + +test.after(() => { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); + if (ORIGINAL_DATA_DIR === undefined) delete process.env.DATA_DIR; + else process.env.DATA_DIR = ORIGINAL_DATA_DIR; + if (ORIGINAL_INITIAL_PASSWORD !== undefined) + process.env.INITIAL_PASSWORD = ORIGINAL_INITIAL_PASSWORD; +}); + +// ─── tests ──────────────────────────────────────────────────────────────────── + +test("GET /replay — empty store returns 200 with zero runs and default filters", async () => { + const res = await route.GET(get("http://localhost/api/compression/replay")); + assert.equal(res.status, 200); + const body = await readJson(res); + assert.equal(body.count, 0); + assert.deepEqual(body.runs, []); + assert.equal(body.filters.provider, null); + assert.equal(body.filters.model, null); + assert.equal(body.filters.windowMs, 24 * 60 * 60 * 1000, "default windowMs is 24h"); + assert.equal(body.filters.limit, 100, "default limit is 100"); + assert.ok(typeof body.windowStart === "string" && body.windowStart.endsWith("Z")); + assert.ok(typeof body.now === "string" && body.now.endsWith("Z")); +}); + +test("GET /replay — returns runs newest first with the expected shape", async () => { + const older = new Date(Date.now() - 60_000).toISOString(); + const newer = new Date(Date.now() - 5_000).toISOString(); + + insertCompressionAnalyticsRow({ + timestamp: older, + provider: "openai", + mode: "stacked", + engine: "headroom", + original_tokens: 1000, + compressed_tokens: 700, + tokens_saved: 300, + duration_ms: 12, + request_id: "gpt-4o-mini::req-older", + validation_fallback: 0, + receipt_source: "provider", + estimated_usd_saved: 0.0001, + actual_total_tokens: 700, + }); + insertCompressionAnalyticsRow({ + timestamp: newer, + provider: "anthropic", + mode: "stacked", + engine: "caveman", + original_tokens: 2000, + compressed_tokens: 1100, + tokens_saved: 900, + duration_ms: 21, + request_id: "claude-3-5-sonnet::req-newer", + validation_fallback: 1, + receipt_source: "provider", + estimated_usd_saved: 0.0042, + actual_total_tokens: 1100, + }); + + const res = await route.GET(get("http://localhost/api/compression/replay")); + assert.equal(res.status, 200); + const body = await readJson(res); + assert.equal(body.count, 2); + assert.equal(body.runs[0].requestId, "claude-3-5-sonnet::req-newer", "newest first"); + assert.equal(body.runs[1].requestId, "gpt-4o-mini::req-older"); + assert.equal(body.runs[0].provider, "anthropic"); + assert.equal(body.runs[0].modelHint, "claude-3-5-sonnet", "modelHint extracted from request_id"); + assert.equal(body.runs[1].modelHint, "gpt-4o-mini"); + assert.equal(body.runs[0].validationFallback, true); + assert.equal(body.runs[1].validationFallback, false); + assert.equal(body.runs[0].savingsPercent, 45); + assert.equal(body.runs[0].actualTotalTokens, 1100); + assert.equal(body.runs[0].estimatedUsdSaved, 0.0042); +}); + +test("GET /replay — attaches per-engine breakdown rows when present", async () => { + const now = new Date().toISOString(); + insertCompressionAnalyticsRow({ + timestamp: now, + provider: "openai", + mode: "stacked", + engine: "stacked", + original_tokens: 2000, + compressed_tokens: 1100, + tokens_saved: 900, + duration_ms: 30, + request_id: "gpt-4o::req-breakdown", + }); + insertCompressionEngineBreakdown([ + { + timestamp: now, + request_id: "gpt-4o::req-breakdown", + engine: "headroom", + original_tokens: 2000, + compressed_tokens: 1500, + tokens_saved: 500, + duration_ms: 10, + }, + { + timestamp: now, + request_id: "gpt-4o::req-breakdown", + engine: "caveman", + original_tokens: 1500, + compressed_tokens: 1100, + tokens_saved: 400, + duration_ms: 15, + }, + ]); + + const res = await route.GET(get("http://localhost/api/compression/replay")); + const body = await readJson(res); + assert.equal(body.count, 1); + const breakdown = body.runs[0].engineBreakdown; + assert.equal(breakdown.length, 2, "two breakdown rows attached"); + assert.deepEqual( + breakdown.map((b) => b.engine), + ["headroom", "caveman"] + ); + assert.equal(breakdown[0].originalTokens, 2000); + assert.equal(breakdown[1].compressedTokens, 1100); +}); + +test("GET /replay — synthesizes a breakdown row when none is stored", async () => { + insertCompressionAnalyticsRow({ + timestamp: new Date().toISOString(), + provider: "anthropic", + mode: "lite", + engine: "lite", + original_tokens: 1000, + compressed_tokens: 800, + tokens_saved: 200, + request_id: "claude::req-solo", + }); + + const res = await route.GET(get("http://localhost/api/compression/replay")); + const body = await readJson(res); + assert.equal(body.runs[0].engineBreakdown.length, 1); + assert.equal(body.runs[0].engineBreakdown[0].engine, "lite"); + assert.equal(body.runs[0].engineBreakdown[0].originalTokens, 1000); + assert.equal(body.runs[0].engineBreakdown[0].compressedTokens, 800); +}); + +test("GET /replay?provider=openai — filters by case-insensitive provider", async () => { + const now = new Date().toISOString(); + insertCompressionAnalyticsRow({ + timestamp: now, + provider: "OpenAI", + mode: "lite", + original_tokens: 100, + compressed_tokens: 60, + tokens_saved: 40, + request_id: "gpt::1", + }); + insertCompressionAnalyticsRow({ + timestamp: now, + provider: "anthropic", + mode: "lite", + original_tokens: 100, + compressed_tokens: 50, + tokens_saved: 50, + request_id: "claude::1", + }); + insertCompressionAnalyticsRow({ + timestamp: now, + provider: null, + mode: "lite", + original_tokens: 100, + compressed_tokens: 70, + tokens_saved: 30, + request_id: "unknown::1", + }); + + const res = await route.GET( + get("http://localhost/api/compression/replay?provider=openai") + ); + const body = await readJson(res); + assert.equal(body.count, 1, "case-insensitive match returns only the OpenAI row"); + assert.equal(body.runs[0].provider, "OpenAI"); + assert.equal(body.filters.provider, "openai"); +}); + +test("GET /replay?model=gpt-4o — request_id LIKE filter matches nested model ids", async () => { + const now = new Date().toISOString(); + insertCompressionAnalyticsRow({ + timestamp: now, + provider: "openai", + mode: "lite", + original_tokens: 100, + compressed_tokens: 60, + tokens_saved: 40, + request_id: "gpt-4o::req-1", + }); + insertCompressionAnalyticsRow({ + timestamp: now, + provider: "openai", + mode: "lite", + original_tokens: 100, + compressed_tokens: 70, + tokens_saved: 30, + request_id: "gpt-4o-mini::req-2", + }); + insertCompressionAnalyticsRow({ + timestamp: now, + provider: "anthropic", + mode: "lite", + original_tokens: 100, + compressed_tokens: 50, + tokens_saved: 50, + request_id: "claude::req-3", + }); + + const res = await route.GET( + get( + "http://localhost/api/compression/replay?model=" + encodeURIComponent("gpt-4o") + ) + ); + const body = await readJson(res); + assert.equal(body.count, 2, "both gpt-4o and gpt-4o-mini request_ids match"); + assert.ok(body.runs.every((r) => r.requestId && r.requestId.includes("gpt-4o"))); + assert.equal(body.filters.model, "gpt-4o"); +}); + +test("GET /replay?windowMs=60000 — only returns rows from the last minute", async () => { + const within = new Date(Date.now() - 30_000).toISOString(); + const outside = new Date(Date.now() - 5 * 60_000).toISOString(); + insertCompressionAnalyticsRow({ + timestamp: within, + mode: "lite", + original_tokens: 100, + compressed_tokens: 60, + tokens_saved: 40, + request_id: "within", + }); + insertCompressionAnalyticsRow({ + timestamp: outside, + mode: "lite", + original_tokens: 100, + compressed_tokens: 50, + tokens_saved: 50, + request_id: "outside", + }); + + const res = await route.GET( + get("http://localhost/api/compression/replay?windowMs=60000") + ); + const body = await readJson(res); + assert.equal(body.count, 1, "only the recent row falls in the 60s window"); + assert.equal(body.runs[0].requestId, "within"); + assert.equal(body.filters.windowMs, 60000); + assert.ok( + new Date(body.windowStart).getTime() <= new Date(within).getTime(), + "windowStart is at or before the included row" + ); +}); + +test("GET /replay?windowMs=garbage — falls back to default 24h window", async () => { + insertCompressionAnalyticsRow({ + timestamp: new Date(Date.now() - 60_000).toISOString(), + mode: "lite", + original_tokens: 100, + compressed_tokens: 60, + tokens_saved: 40, + request_id: "recent", + }); + const res = await route.GET( + get("http://localhost/api/compression/replay?windowMs=not-a-number") + ); + const body = await readJson(res); + assert.equal(body.filters.windowMs, 24 * 60 * 60 * 1000, "non-numeric defaults to 24h"); + assert.equal(body.count, 1); +}); + +test("GET /replay?windowMs=999999999999 — clamps to 30 days", async () => { + const res = await route.GET( + get("http://localhost/api/compression/replay?windowMs=999999999999") + ); + const body = await readJson(res); + assert.equal(body.filters.windowMs, 30 * 24 * 60 * 60 * 1000, "clamped to 30d ceiling"); +}); + +test("GET /replay?limit=1 — caps result count", async () => { + const now = new Date().toISOString(); + for (let i = 0; i < 5; i++) { + insertCompressionAnalyticsRow({ + timestamp: new Date(Date.now() - i * 1000).toISOString(), + provider: "openai", + mode: "lite", + original_tokens: 100, + compressed_tokens: 60, + tokens_saved: 40, + request_id: `r-${i}`, + }); + } + const res = await route.GET(get("http://localhost/api/compression/replay?limit=1")); + const body = await readJson(res); + assert.equal(body.count, 1); + assert.equal(body.runs[0].requestId, "r-0", "newest of the 5"); + assert.equal(body.filters.limit, 1); +}); + +test("GET /replay?limit=99999 — clamps to 1000", async () => { + const res = await route.GET( + get("http://localhost/api/compression/replay?limit=99999") + ); + const body = await readJson(res); + assert.equal(body.filters.limit, 1000, "clamped to 1000 ceiling"); + assert.equal(body.count, 0, "empty store still returns a well-formed body"); +}); + +test("GET /replay — empty query params treated as unset", async () => { + insertCompressionAnalyticsRow({ + timestamp: new Date().toISOString(), + mode: "lite", + original_tokens: 100, + compressed_tokens: 60, + tokens_saved: 40, + request_id: "ok", + }); + const res = await route.GET( + get( + "http://localhost/api/compression/replay?provider=&model=&limit=&windowMs=&extra=junk" + ) + ); + assert.equal(res.status, 200, "empty values are treated as no filter"); + const body = await readJson(res); + assert.equal(body.count, 1); + assert.equal(body.filters.provider, null); + assert.equal(body.filters.model, null); + assert.equal(body.filters.limit, 100, "empty limit defaults to 100"); + assert.equal(body.filters.windowMs, 24 * 60 * 60 * 1000, "empty windowMs defaults to 24h"); +}); + +test("getRecentCompressionAnalyticsRuns — respects provider + model + sinceIso together", async () => { + const now = new Date().toISOString(); + insertCompressionAnalyticsRow({ + timestamp: now, + provider: "openai", + mode: "stacked", + original_tokens: 500, + compressed_tokens: 200, + tokens_saved: 300, + request_id: "gpt-4o::reqA", + }); + insertCompressionAnalyticsRow({ + timestamp: now, + provider: "openai", + mode: "stacked", + original_tokens: 500, + compressed_tokens: 250, + tokens_saved: 250, + request_id: "claude::reqB", + }); + + const onlyGpt = getRecentCompressionAnalyticsRuns({ + provider: "openai", + model: "gpt-4o", + sinceIso: new Date(Date.now() - 60_000).toISOString(), + limit: 50, + }); + assert.equal(onlyGpt.length, 1); + assert.equal(onlyGpt[0].request_id, "gpt-4o::reqA"); + assert.equal(onlyGpt[0].provider, "openai"); +}); diff --git a/src/app/api/compression/replay/route.ts b/src/app/api/compression/replay/route.ts new file mode 100644 index 000000000000..49885953c457 --- /dev/null +++ b/src/app/api/compression/replay/route.ts @@ -0,0 +1,234 @@ +/** + * GET /api/compression/replay + * + * Returns the recent compression run history so the dashboard can render a + * "replay" panel alongside the live `compression.completed` event in + * `open-sse/services/compression/engineBreakdown.ts`. + * + * Query parameters (all optional, case-sensitive except `provider`): + * - `provider` Exact (case-insensitive) match on `provider` column. + * Example: `?provider=openai`. + * - `model` Best-effort filter on the originating model. The + * `compression_analytics` table does not store `model` + * directly, so this matches against `request_id LIKE`. + * Example: `?model=gpt-4o-mini`. + * - `windowMs` How far back to look, in milliseconds from `now`. + * Clamped to [1, 30 days]. Default 24h. + * Example: `?windowMs=300000` (last 5 minutes). + * - `limit` Max rows to return. Clamped to [1, 1000]. Default 100. + * + * Response: + * { + * runs: Array, + * count: number, + * filters: { provider, model, windowMs, limit }, + * windowStart: ISO string, + * now: ISO string + * } + * + * Each run mirrors the relevant columns from `compression_analytics` plus + * the per-engine breakdown rows (if any) for the same `request_id`, so the + * studio can switch from the live event to historical detail without losing + * the engine graph. + * + * Auth: requireManagementAuth. In the unconfigured/first-run state (no + * INITIAL_PASSWORD) this is a no-op. + */ + +import { NextResponse } from "next/server"; +import { requireManagementAuth } from "@/lib/api/requireManagementAuth"; +import { + getRecentCompressionAnalyticsRuns, + getCompressionEngineBreakdownForRequests, +} from "@/lib/db/compressionAnalytics"; +import type { CompressionReplayRow } from "@/lib/db/compressionAnalytics"; +import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error"; +import { ensureEngineBreakdown } from "@omniroute/open-sse/services/compression/engineBreakdown"; +import type { EngineBreakdownEntry } from "@omniroute/open-sse/services/compression/engineBreakdown"; + +/** Max lookback the `windowMs` query param will accept (30 days). */ +const WINDOW_MS_MAX = 30 * 24 * 60 * 60 * 1000; +/** Default lookback if `windowMs` is not supplied (24 hours). */ +const WINDOW_MS_DEFAULT = 24 * 60 * 60 * 1000; +/** Default row cap when `limit` is not supplied. */ +const LIMIT_DEFAULT = 100; +/** Hard ceiling for `limit`. */ +const LIMIT_MAX = 1000; + +function parseWindowMs(raw: string | null): number { + if (raw === null) return WINDOW_MS_DEFAULT; + const n = Number(raw); + if (!Number.isFinite(n) || n <= 0) return WINDOW_MS_DEFAULT; + return Math.min(WINDOW_MS_MAX, Math.floor(n)); +} + +function parseLimit(raw: string | null): number { + if (raw === null) return LIMIT_DEFAULT; + const n = Number(raw); + if (!Number.isFinite(n) || n <= 0) return LIMIT_DEFAULT; + return Math.min(LIMIT_MAX, Math.floor(n)); +} + +function cleanString(value: string | null | undefined): string | null { + if (value === null || value === undefined) return null; + const trimmed = value.trim(); + return trimmed.length === 0 ? null : trimmed; +} + +interface ReplayRunResponse { + id: number; + timestamp: string; + mode: string; + engine: string | null; + provider: string | null; + modelHint: string | null; + comboId: string | null; + compressionComboId: string | null; + requestId: string | null; + originalTokens: number; + compressedTokens: number; + tokensSaved: number; + savingsPercent: number; + durationMs: number | null; + validationFallback: boolean; + receiptSource: string | null; + estimatedUsdSaved: number | null; + actualTotalTokens: number | null; + engineBreakdown: EngineBreakdownEntry[]; +} + +/** + * Compute the model hint from a request_id fragment, if any. + * request_ids frequently embed a model tag (e.g. `claude-3-5-sonnet:::req-…`). + * Returns null if no plausible model token is present, so callers can still + * distinguish "model unknown" from "model=gpt-4o". + */ +function extractModelHint(requestId: string | null | undefined): string | null { + if (!requestId) return null; + const head = requestId.split(":::")[0]; + const beforeUuid = head.split("::")[0]; + if (beforeUuid && beforeUuid.length > 0 && beforeUuid.length <= 128) { + return beforeUuid; + } + return null; +} + +/** + * Read all per-engine breakdown rows for a list of request_ids via the DB helper. + * Returns a Map keyed by request_id mapped to a list of `EngineBreakdownEntry` + * shapes ready to embed in the replay response. + */ +function loadBreakdownForRequests( + requestIds: Array +): Map { + const raw = getCompressionEngineBreakdownForRequests(requestIds); + const out = new Map(); + for (const [requestId, rows] of raw.entries()) { + out.set( + requestId, + rows.map((r) => ({ + engine: r.engine, + originalTokens: r.original_tokens, + compressedTokens: r.compressed_tokens, + tokensSaved: r.tokens_saved, + durationMs: r.duration_ms ?? undefined, + })) + ); + } + return out; +} + +function shapeRun( + row: CompressionReplayRow, + breakdown: EngineBreakdownEntry[] +): ReplayRunResponse { + const originalTokens = Number(row.original_tokens) || 0; + const compressedTokens = Number(row.compressed_tokens) || 0; + const savingsPercent = + originalTokens > 0 + ? Math.round(((originalTokens - compressedTokens) / originalTokens) * 1000) / 10 + : 0; + + return { + id: row.id, + timestamp: row.timestamp, + mode: row.mode, + engine: row.engine ?? null, + provider: row.provider ?? null, + modelHint: extractModelHint(row.request_id), + comboId: row.combo_id ?? row.compression_combo_id ?? null, + compressionComboId: row.compression_combo_id ?? null, + requestId: row.request_id ?? null, + originalTokens, + compressedTokens, + tokensSaved: Number(row.tokens_saved) || 0, + savingsPercent, + durationMs: row.duration_ms ?? null, + validationFallback: Boolean(row.validation_fallback), + receiptSource: row.receipt_source ?? null, + estimatedUsdSaved: + row.estimated_usd_saved === null || row.estimated_usd_saved === undefined + ? null + : Number(row.estimated_usd_saved), + actualTotalTokens: + row.actual_total_tokens === null || row.actual_total_tokens === undefined + ? null + : Number(row.actual_total_tokens), + engineBreakdown: + breakdown.length > 0 + ? breakdown + : ensureEngineBreakdown({ + engine: row.engine ?? undefined, + mode: row.mode, + originalTokens, + compressedTokens, + savingsPercent, + techniquesUsed: [], + }), + }; +} + +export async function GET(req: Request) { + const authError = await requireManagementAuth(req); + if (authError) return authError; + + try { + const url = new URL(req.url); + const provider = cleanString(url.searchParams.get("provider")); + const model = cleanString(url.searchParams.get("model")); + const windowMs = parseWindowMs(url.searchParams.get("windowMs")); + const limit = parseLimit(url.searchParams.get("limit")); + + const now = new Date(); + const sinceIso = new Date(now.getTime() - windowMs).toISOString(); + + const rows = getRecentCompressionAnalyticsRuns({ + limit, + sinceIso, + provider, + model, + }); + + const requestIds = rows.map((r) => r.request_id); + const breakdownByRequestId = loadBreakdownForRequests(requestIds); + + const runs: ReplayRunResponse[] = rows.map((row) => + shapeRun(row, breakdownByRequestId.get(row.request_id ?? "") ?? []) + ); + + return NextResponse.json({ + runs, + count: runs.length, + filters: { provider, model, windowMs, limit }, + windowStart: sinceIso, + now: now.toISOString(), + }); + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err); + console.error("[/api/compression/replay]", msg); + return NextResponse.json( + { error: "Failed to load compression replay history", details: sanitizeErrorMessage(msg) }, + { status: 500 } + ); + } +} diff --git a/src/lib/db/compressionAnalytics.ts b/src/lib/db/compressionAnalytics.ts index ee63e7f2b62d..98eb12b06ca7 100644 --- a/src/lib/db/compressionAnalytics.ts +++ b/src/lib/db/compressionAnalytics.ts @@ -236,6 +236,53 @@ export function insertCompressionEngineBreakdown(rows: CompressionEngineBreakdow insertAll(rows); } +/** + * Idempotent table init for callers that only read `compression_engine_breakdown` + * (not write). Mirrors the side-effect normally triggered by + * `insertCompressionEngineBreakdown` / `getPerEngineAnalytics`. + */ +export function ensureCompressionAnalyticsTables(): void { + const db = getDbInstance(); + ensureCompressionAnalyticsColumns(); + if (breakdownTableEnsuredForDb !== db) { + ensureCompressionEngineBreakdownTable(); + } +} + +/** + * Fetch all per-engine breakdown rows for the supplied request_ids (one row per + * engine inside a stacked compression pipeline). Returns a Map keyed by request_id + * so the caller can pair rows with their parent `compression_analytics` row without + * issuing N+1 queries. Empty input returns an empty map. + */ +export function getCompressionEngineBreakdownForRequests( + requestIds: Array +): Map { + const out = new Map(); + const ids = requestIds.filter((id): id is string => Boolean(id)); + if (ids.length === 0) return out; + ensureCompressionAnalyticsTables(); + const placeholders = ids.map(() => "?").join(","); + const db = getDbInstance(); + const rows = db + .prepare( + `SELECT timestamp, request_id, engine, + original_tokens, compressed_tokens, tokens_saved, duration_ms + FROM compression_engine_breakdown + WHERE request_id IN (${placeholders}) + ORDER BY request_id ASC, id ASC` + ) + .all(...ids) as CompressionEngineBreakdownRow[]; + for (const r of rows) { + const key = r.request_id; + if (!key) continue; + const list = out.get(key) ?? []; + list.push(r); + out.set(key, list); + } + return out; +} + export function attachCompressionUsageReceipt( requestId: string | null | undefined, usage: Record | null | undefined, @@ -595,3 +642,73 @@ export function getLatestCompressionAnalyticsRun(): LatestCompressionAnalyticsRu ) .get() as LatestCompressionAnalyticsRun | undefined; } + +export interface CompressionReplayRow extends LatestCompressionAnalyticsRun { + provider: string | null; + receipt_source: string | null; + estimated_usd_saved: number | null; + actual_total_tokens: number | null; +} + +export interface GetRecentCompressionRunsOptions { + limit?: number; + sinceIso?: string; + provider?: string | null; + model?: string | null; +} + +/** + * Return the most recent compression runs from `compression_analytics`, newest first. + * + * - `limit` clamped to [1, 1000]; default 100. + * - `sinceIso` inclusive lower bound on `timestamp`. Defaults to "all-time". + * - `provider` exact-match on `provider` column (case-insensitive). Ignored when null/empty. + * - `model` best-effort: the `compression_analytics` table does not currently store + * `model` directly, so the filter matches `request_id LIKE '%%'`. + * Pre-existing rows from a given request carry the originating model id + * in their request-id fragment via upstream call sites; passing a model + * that was never observed returns zero rows without erroring. Pass null + * or empty to disable the filter. + * + * Used by GET /api/compression/replay to power the dashboard's "replay history" panel. + */ +export function getRecentCompressionAnalyticsRuns( + opts: GetRecentCompressionRunsOptions = {} +): CompressionReplayRow[] { + const limit = Math.max(1, Math.min(1000, Math.floor(opts.limit ?? 100) || 100)); + const sinceIso = opts.sinceIso ?? null; + const provider = opts.provider?.trim() || null; + const model = opts.model?.trim() || null; + + let where = ""; + const params: unknown[] = []; + if (sinceIso) { + where = "WHERE timestamp >= ?"; + params.push(sinceIso); + } + if (provider) { + if (where) where += " AND LOWER(provider) = LOWER(?)"; + else where = "WHERE LOWER(provider) = LOWER(?)"; + params.push(provider); + } + if (model) { + if (where) where += " AND request_id LIKE ?"; + else where = "WHERE request_id LIKE ?"; + params.push(`%${model}%`); + } + params.push(limit); + + const db = getDbInstance(); + return db + .prepare( + `SELECT id, timestamp, combo_id, compression_combo_id, mode, + original_tokens, compressed_tokens, tokens_saved, duration_ms, + request_id, engine, provider, receipt_source, + estimated_usd_saved, actual_total_tokens, validation_fallback + FROM compression_analytics + ${where} + ORDER BY timestamp DESC, id DESC + LIMIT ?` + ) + .all(...params) as CompressionReplayRow[]; +}