Files
OmniRoute/tests/unit/usage-cache-health.test.ts
Diego Rodrigues de Sa e Souza aa85fa02bb feat(api): prompt-cache health summary endpoint and analytics tab (#8827)
Adds GET /api/usage/cache-health and a Cache Health tab under
/dashboard/analytics, both backed by a pure summary over the cache columns
already present in call_logs.

Motivated by a production diagnosis where the aggregate ratio was actively
misleading. The window read 24.8M cached tokens and wrote 9.5M — a
write/read of 0.385, which reads as merely mediocre. The actual shape was
very different: the median call wrote 848 tokens while 18% of the calls
carried 94% of every written token, and two models in the same window sat
at 0.13 (Sonnet) and 0.51 (Opus). Averaging hid all three facts.

So the summary reports what the average cannot: the distribution
(p50/p90/p99), the concentration (how few calls carry how much of the
write), and the per-model split. The heavy-write threshold is relative to
the window (10x the median, floored at 1024) because a cutoff tuned for
130k-token conversations reports nothing at all on 2k-token ones; 1024 is
the minimum Anthropic bills for cache creation, below which a write carries
no signal.

Calls that neither read nor wrote are counted separately from thrash — a
route that does not cache is an absence of caching, not a sick cache — and
only successful calls are summarized, since a 4xx/5xx never reached the
provider cache and would dilute the ratio.

Tests cover the summary (8) and the route (6, against a real SQLite so the
WHERE clause itself is exercised), including that an internal failure
answers 500 without leaking the stack trace, the SQL text or a table name.
2026-07-28 03:53:28 -03:00

148 lines
5.3 KiB
TypeScript

/**
* Prompt-cache health summary.
*
* Context: production diagnosis on 2026-07-27 found that 18% of the Opus calls
* carried 94% of all cache_creation tokens, while the median call wrote only
* 848 tokens. A plain average hid this completely — `write/read = 0.385` looks
* merely mediocre, and the aggregate says nothing about WHERE the waste is.
* The summary therefore has to expose the concentration (heavy-write share),
* not just the ratio, and split per model, because Sonnet sat at 0.13 while
* Opus sat at 0.51 in the very same window.
*
* The numbers in these fixtures are the real shape observed on the box.
*/
import test from "node:test";
import assert from "node:assert/strict";
import { summarizeCacheHealth, type CacheHealthRow } from "@/lib/usage/cacheHealth";
function row(
model: string,
cacheRead: number,
cacheCreation: number,
timestamp = "2026-07-28T02:00:00.000Z"
): CacheHealthRow {
return { model, cacheRead, cacheCreation, timestamp };
}
test("uma conversa saudável: escreve o prefixo uma vez e depois só o delta", () => {
const rows = [
row("claude-sonnet-5", 0, 11660),
row("claude-sonnet-5", 11660, 14),
row("claude-sonnet-5", 11674, 15),
row("claude-sonnet-5", 11689, 12),
];
const s = summarizeCacheHealth(rows);
assert.equal(s.totalCalls, 4);
assert.equal(s.cacheReadTotal, 35023);
assert.equal(s.cacheWriteTotal, 11701);
// O write inicial é inevitável; o que importa é que os turnos seguintes leem.
assert.equal(s.warmCalls, 3);
assert.equal(s.coldCalls, 1);
assert.equal(s.verdict, "healthy");
});
test("thrash: repaga o prefixo a cada turno em vez de ler", () => {
const rows = [
row("claude-opus-4-8", 0, 200000),
row("claude-opus-4-8", 0, 205000),
row("claude-opus-4-8", 0, 210000),
];
const s = summarizeCacheHealth(rows);
assert.equal(s.verdict, "thrash");
assert.ok(s.writeReadRatio > 1, "razão write/read acima de 1 é a assinatura do thrash");
assert.equal(s.warmCalls, 0);
});
test("expõe a CONCENTRAÇÃO do write, que a média esconde", () => {
// 20 chamadas: 18 baratas + 2 caras. A média mente, a concentração não.
const rows = [
...Array.from({ length: 18 }, () => row("claude-opus-4-8", 127000, 800)),
row("claude-opus-4-8", 37509, 91878),
row("claude-opus-4-8", 37509, 87949),
];
const s = summarizeCacheHealth(rows);
assert.equal(s.heavyWriteCalls, 2);
assert.equal(s.heavyWriteCallShare, 0.1, "2 de 20 chamadas");
assert.ok(
s.heavyWriteTokenShare > 0.9,
`essas 2 chamadas concentram >90% do write, não 10% (obtido: ${s.heavyWriteTokenShare})`
);
// A mediana continua barata — é exatamente por isso que o p50 sozinho engana.
assert.equal(s.writeP50, 800);
assert.ok(s.writeP90 > 50000, "o p90 é onde a dor aparece");
});
test("quebra por modelo — Opus e Sonnet na mesma janela têm saúde diferente", () => {
const rows = [
...Array.from({ length: 8 }, () => row("claude-sonnet-5", 100000, 1000)),
...Array.from({ length: 6 }, () => row("claude-opus-4-8", 100000, 1000)),
...Array.from({ length: 2 }, () => row("claude-opus-4-8", 37509, 90000)),
];
const s = summarizeCacheHealth(rows);
const byModel = Object.fromEntries(s.byModel.map((m) => [m.model, m]));
assert.equal(s.byModel.length, 2);
assert.ok(
byModel["claude-opus-4-8"].writeReadRatio > byModel["claude-sonnet-5"].writeReadRatio,
"o modelo problemático tem de se destacar no relatório"
);
// Ordenado pelo pior primeiro: é o que o operador precisa ver de cara.
assert.equal(s.byModel[0].model, "claude-opus-4-8");
});
test("chamadas sem cache nenhum são contadas à parte, não como thrash", () => {
// Um modelo/rota que não cacheia não é um cache doente — é ausência de cache.
const rows = [row("gpt-4o", 0, 0), row("gpt-4o", 0, 0), row("claude-sonnet-5", 9000, 20)];
const s = summarizeCacheHealth(rows);
assert.equal(s.uncachedCalls, 2);
assert.equal(s.warmCalls, 1);
assert.equal(s.verdict, "healthy", "ausência de cache não pode ser reportada como thrash");
});
test("janela vazia não divide por zero nem inventa veredicto", () => {
const s = summarizeCacheHealth([]);
assert.equal(s.totalCalls, 0);
assert.equal(s.writeReadRatio, 0);
assert.equal(s.heavyWriteTokenShare, 0);
assert.equal(s.verdict, "no-data");
assert.deepEqual(s.byModel, []);
});
test("tolera linhas com null vindas do banco (colunas são nullable)", () => {
const rows = [
{ model: "claude-sonnet-5", cacheRead: null, cacheCreation: 5000, timestamp: "x" },
{ model: "claude-sonnet-5", cacheRead: 5000, cacheCreation: null, timestamp: "x" },
] as unknown as CacheHealthRow[];
const s = summarizeCacheHealth(rows);
assert.equal(s.cacheReadTotal, 5000);
assert.equal(s.cacheWriteTotal, 5000);
assert.equal(s.totalCalls, 2);
});
test("o limiar de 'heavy' acompanha a janela em vez de ser um número mágico fixo", () => {
// Conversas pequenas (cache de ~2k) não podem ser julgadas pelo mesmo corte
// absoluto de uma conversa de 130k, senão nada é 'heavy' e o relatório cala.
const pequenas = [
...Array.from({ length: 9 }, () => row("m", 2000, 20)),
row("m", 500, 1800),
];
const s = summarizeCacheHealth(pequenas);
assert.equal(s.heavyWriteCalls, 1, "a chamada cara relativa à janela tem de ser detectada");
});