Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 9 additions & 80 deletions src/lib/usage/usageHistory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@

import { getDbInstance } from "../db/core";
import { protectPayloadForLog } from "../logPayloads";
import {
asRecord,
normalizeServiceTier,
percentile,
stdDev,
toNumber,
toStringOrNull,
truncatePendingPreview,
} from "./usageHistory/helpers";
import {
clearCompletedDetails,
maybeEnrichCompletedDetail,
Expand All @@ -25,7 +34,6 @@ import {
getReasoningTokens,
} from "./tokenAccounting";

type JsonRecord = Record<string, unknown>;
export type PendingRequestMetadata = {
clientEndpoint?: string | null;
clientRequest?: unknown;
Expand Down Expand Up @@ -67,85 +75,6 @@ export type PendingRequestDetail = {
} | null;
};

function asRecord(value: unknown): JsonRecord {
return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : {};
}

function toStringOrNull(value: unknown): string | null {
return typeof value === "string" && value.trim().length > 0 ? value : null;
}

function normalizeServiceTier(value: unknown): string {
const tier = typeof value === "string" ? value.trim().toLowerCase() : "";
if (tier === "priority" || tier === "fast") return "priority";
if (tier === "flex") return "flex";
return "standard";
}

function toNumber(value: unknown): number {
if (typeof value === "number" && Number.isFinite(value)) return value;
if (typeof value === "string" && value.trim().length > 0) {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : 0;
}
return 0;
}

function percentile(sortedValues: number[], p: number): number {
if (sortedValues.length === 0) return 0;
if (sortedValues.length === 1) return sortedValues[0];
const bounded = Math.max(0, Math.min(1, p));
const idx = Math.round((sortedValues.length - 1) * bounded);
return sortedValues[idx] ?? sortedValues[sortedValues.length - 1];
}

function stdDev(values: number[], avg: number): number {
if (values.length <= 1) return 0;
const variance = values.reduce((acc, v) => acc + (v - avg) ** 2, 0) / values.length;
return Math.sqrt(Math.max(0, variance));
}

const MAX_PREVIEW_DEPTH = 6;
const MAX_PREVIEW_STRING = 1200;
const MAX_PREVIEW_ARRAY_ITEMS = 12;
const MAX_PREVIEW_OBJECT_KEYS = 24;

function truncatePendingPreview(value: unknown, depth = 0): unknown {
if (depth >= MAX_PREVIEW_DEPTH) {
return "[TRUNCATED_DEPTH]";
}

if (typeof value === "string") {
return value.length > MAX_PREVIEW_STRING ? `${value.slice(0, MAX_PREVIEW_STRING)}...` : value;
}

if (Array.isArray(value)) {
const preview = value
.slice(0, MAX_PREVIEW_ARRAY_ITEMS)
.map((item) => truncatePendingPreview(item, depth + 1));
if (value.length > MAX_PREVIEW_ARRAY_ITEMS) {
preview.push({ _truncatedItems: value.length - MAX_PREVIEW_ARRAY_ITEMS });
}
return preview;
}

if (!value || typeof value !== "object") {
return value;
}

const entries = Object.entries(value as JsonRecord);
const truncatedEntries = entries
.slice(0, MAX_PREVIEW_OBJECT_KEYS)
.map(([key, entryValue]) => [key, truncatePendingPreview(entryValue, depth + 1)]);
const preview = Object.fromEntries(truncatedEntries);

if (entries.length > MAX_PREVIEW_OBJECT_KEYS) {
preview._truncatedKeys = entries.length - MAX_PREVIEW_OBJECT_KEYS;
}

return preview;
}

function normalizePendingMetadata(metadata?: PendingRequestMetadata): PendingRequestMetadata {
if (!metadata) return {};

Expand Down
85 changes: 85 additions & 0 deletions src/lib/usage/usageHistory/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/**
* Pure, stateless helpers extracted from usageHistory.ts.
* No DB access, no module-level state — safe to import anywhere.
*/

type JsonRecord = Record<string, unknown>;

export function asRecord(value: unknown): JsonRecord {
return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : {};
}

export function toStringOrNull(value: unknown): string | null {
return typeof value === "string" && value.trim().length > 0 ? value : null;
}

export function normalizeServiceTier(value: unknown): string {
const tier = typeof value === "string" ? value.trim().toLowerCase() : "";
if (tier === "priority" || tier === "fast") return "priority";
if (tier === "flex") return "flex";
return "standard";
}

export function toNumber(value: unknown): number {
if (typeof value === "number" && Number.isFinite(value)) return value;
if (typeof value === "string" && value.trim().length > 0) {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : 0;
}
return 0;
}

export function percentile(sortedValues: number[], p: number): number {
if (sortedValues.length === 0) return 0;
if (sortedValues.length === 1) return sortedValues[0];
const bounded = Math.max(0, Math.min(1, p));
const idx = Math.round((sortedValues.length - 1) * bounded);
return sortedValues[idx] ?? sortedValues[sortedValues.length - 1];
}

export function stdDev(values: number[], avg: number): number {
if (values.length <= 1) return 0;
const variance = values.reduce((acc, v) => acc + (v - avg) ** 2, 0) / values.length;
return Math.sqrt(Math.max(0, variance));
}

export const MAX_PREVIEW_DEPTH = 6;
export const MAX_PREVIEW_STRING = 1200;
export const MAX_PREVIEW_ARRAY_ITEMS = 12;
export const MAX_PREVIEW_OBJECT_KEYS = 24;

export function truncatePendingPreview(value: unknown, depth = 0): unknown {
if (depth >= MAX_PREVIEW_DEPTH) {
return "[TRUNCATED_DEPTH]";
}

if (typeof value === "string") {
return value.length > MAX_PREVIEW_STRING ? `${value.slice(0, MAX_PREVIEW_STRING)}...` : value;
}

if (Array.isArray(value)) {
const preview = value
.slice(0, MAX_PREVIEW_ARRAY_ITEMS)
.map((item) => truncatePendingPreview(item, depth + 1));
if (value.length > MAX_PREVIEW_ARRAY_ITEMS) {
preview.push({ _truncatedItems: value.length - MAX_PREVIEW_ARRAY_ITEMS });
}
return preview;
}

if (!value || typeof value !== "object") {
return value;
}

const entries = Object.entries(value as JsonRecord);
const truncatedEntries = entries
.slice(0, MAX_PREVIEW_OBJECT_KEYS)
.map(([key, entryValue]) => [key, truncatePendingPreview(entryValue, depth + 1)]);
const preview = Object.fromEntries(truncatedEntries);

if (entries.length > MAX_PREVIEW_OBJECT_KEYS) {
preview._truncatedKeys = entries.length - MAX_PREVIEW_OBJECT_KEYS;
}

return preview;
}
144 changes: 144 additions & 0 deletions tests/unit/usagehistory-helpers-split.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/**
* Characterization + API-surface test: usageHistory.ts god-file decomposition.
*
* The pure, DB-free helpers (coercers + percentile/stdDev stats + the
* depth-limited preview truncator) were extracted verbatim from
* src/lib/usage/usageHistory.ts into src/lib/usage/usageHistory/helpers.ts.
* The in-memory pending-request state machine + DB CRUD stay in the host.
*
* Verifies that:
* 1. The pure helpers behave correctly (stats formulas pinned).
* 2. The host usageHistory.ts still exposes the FULL public API.
* 3. The helpers leaf exports its pieces directly.
*
* Pure value assertions — no DB handle is opened.
*/

import { describe, it } from "node:test";
import assert from "node:assert/strict";

import {
asRecord,
toNumber,
toStringOrNull,
normalizeServiceTier,
percentile,
stdDev,
truncatePendingPreview,
} from "../../src/lib/usage/usageHistory/helpers.ts";

describe("usageHistory/helpers — coercers", () => {
it("asRecord keeps plain objects, rejects arrays/primitives", () => {
const o = { a: 1 };
assert.equal(asRecord(o), o);
assert.deepEqual(asRecord([1]), {});
assert.deepEqual(asRecord(null), {});
});
it("toNumber / toStringOrNull coerce by type", () => {
assert.equal(toNumber("12"), 12);
assert.equal(toNumber("x"), 0);
assert.equal(toStringOrNull("hi"), "hi");
assert.equal(toStringOrNull(" "), null);
});
});

describe("usageHistory/helpers — normalizeServiceTier", () => {
it("maps priority/fast → priority, flex → flex, else → standard", () => {
assert.equal(normalizeServiceTier("priority"), "priority");
assert.equal(normalizeServiceTier("Fast"), "priority");
assert.equal(normalizeServiceTier("flex"), "flex");
assert.equal(normalizeServiceTier("default"), "standard");
assert.equal(normalizeServiceTier(undefined), "standard");
});
});

describe("usageHistory/helpers — percentile (round-index on sorted input)", () => {
const data = [10, 20, 30, 40, 50];
it("returns 0 for empty, the sole value for length 1", () => {
assert.equal(percentile([], 0.5), 0);
assert.equal(percentile([42], 0.9), 42);
});
it("picks round((n-1)*p) clamped to [0,1]", () => {
assert.equal(percentile(data, 0), 10);
assert.equal(percentile(data, 0.25), 20);
assert.equal(percentile(data, 0.5), 30);
assert.equal(percentile(data, 1), 50);
assert.equal(percentile(data, 5), 50); // p clamped to 1
});
});

describe("usageHistory/helpers — stdDev (population)", () => {
it("returns 0 for <=1 value or all-equal", () => {
assert.equal(stdDev([], 0), 0);
assert.equal(stdDev([7], 7), 0);
assert.equal(stdDev([3, 3, 3], 3), 0);
});
it("computes sqrt of the population variance", () => {
assert.equal(stdDev([0, 2], 1), 1); // variance (1+1)/2 = 1
assert.equal(stdDev([2, 4, 4, 4, 5, 5, 7, 9], 5), 2); // variance 32/8 = 4
});
});

describe("usageHistory/helpers — truncatePendingPreview", () => {
it("passes small primitives through", () => {
assert.equal(truncatePendingPreview(5), 5);
assert.equal(truncatePendingPreview("ok"), "ok");
});
it("shortens an over-long string", () => {
const out = truncatePendingPreview("x".repeat(5000));
assert.equal(typeof out, "string");
assert.ok((out as string).length < 5000, "a 5000-char string must be truncated");
});
});

// ── host public API surface ──────────────────────────────────────────────────

const host = await import("../../src/lib/usage/usageHistory.ts");

describe("usageHistory.ts public API surface", () => {
const expected = [
"appendRequestLog",
"clearPendingRequests",
"finalizeMostRecentPendingRequest",
"finalizePendingRequest",
"finalizePendingRequestById",
"getMaxPendingRequestAgeMs",
"getModelLatencyStats",
"getPendingById",
"getPendingRequests",
"getRecentLogs",
"getUsageDb",
"getUsageHistory",
"saveRequestUsage",
"sweepStalePendingRequests",
"trackPendingRequest",
"updatePendingRequest",
"updatePendingRequestById",
"updatePendingRequestStreamChunks",
"getCompletedDetails", // pre-existing re-export, must survive
];
for (const name of expected) {
it(`exposes ${name} as a function`, () => {
assert.equal(typeof host[name], "function", `${name} must be a function on the host`);
});
}
it("loses no public function in the split", () => {
const missing = expected.filter((n) => typeof host[n] !== "function");
assert.deepEqual(missing, [], `missing: ${missing.join(", ")}`);
});
});

describe("helpers.ts exports its pieces directly", () => {
it("the moved helpers are functions on the leaf", async () => {
const h = await import("../../src/lib/usage/usageHistory/helpers.ts");
for (const fn of [
"asRecord",
"percentile",
"stdDev",
"normalizeServiceTier",
"truncatePendingPreview",
]) {
assert.equal(typeof h[fn], "function", fn);
}
});
});
Loading