mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-07-09 17:28:45 +00:00
Some checks are pending
CI / build-check-test (push) Waiting to run
Detect prompt cache misses per turn by comparing each assistant message's cache reads against the previous request's prompt tokens (core/cache-stats.ts). Significant misses emit a warning-colored transcript notice at the turn they occur, noting idle gaps past the cache TTL and model switches when relevant. /session gained cache statistics: a compact token/cache breakdown with hit rate, a $-prefixed cost section with per-model cost breakdown, and the cumulative cost re-billed due to cache misses.
143 lines
5 KiB
TypeScript
143 lines
5 KiB
TypeScript
import type { AssistantMessage } from "@earendil-works/pi-ai";
|
|
import { describe, expect, it } from "vitest";
|
|
import {
|
|
collectCacheMisses,
|
|
computeCacheWaste,
|
|
detectCacheMiss,
|
|
type ModelPriceSource,
|
|
} from "../src/core/cache-stats.ts";
|
|
import type { SessionEntry } from "../src/core/session-manager.ts";
|
|
|
|
const zeroCost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 };
|
|
|
|
const models: ModelPriceSource = {
|
|
// $/million tokens; used as cache-read price fallback on full-miss turns
|
|
find: () => ({ cost: { cacheRead: 0.3 } }),
|
|
};
|
|
|
|
function assistant(options: {
|
|
input?: number;
|
|
cacheRead?: number;
|
|
cacheWrite?: number;
|
|
cost?: Partial<typeof zeroCost>;
|
|
model?: string;
|
|
timestamp?: number;
|
|
}): AssistantMessage {
|
|
return {
|
|
role: "assistant",
|
|
content: [],
|
|
api: "anthropic-messages",
|
|
provider: "test",
|
|
model: options.model ?? "test-model",
|
|
usage: {
|
|
input: options.input ?? 0,
|
|
output: 10,
|
|
cacheRead: options.cacheRead ?? 0,
|
|
cacheWrite: options.cacheWrite ?? 0,
|
|
totalTokens: 0,
|
|
cost: { ...zeroCost, ...options.cost },
|
|
},
|
|
stopReason: "stop",
|
|
timestamp: options.timestamp ?? 0,
|
|
} as AssistantMessage;
|
|
}
|
|
|
|
function entry(message: AssistantMessage): SessionEntry {
|
|
return { type: "message", id: "x", parentId: null, timestamp: "", message } as SessionEntry;
|
|
}
|
|
|
|
// Turn 1: fresh 100k cache write at $3.75/M
|
|
const turn1 = assistant({ cacheWrite: 100_000, cost: { cacheWrite: 0.375 }, timestamp: 0 });
|
|
// Turn 2: healthy, everything read back at $0.30/M
|
|
const turn2 = assistant({
|
|
cacheRead: 100_000,
|
|
cacheWrite: 5_000,
|
|
cost: { cacheRead: 0.03, cacheWrite: 0.019 },
|
|
timestamp: 60_000,
|
|
});
|
|
|
|
describe("computeCacheWaste", () => {
|
|
it("accumulates missed tokens and cost across turns", () => {
|
|
// Turn 3: full miss, previous 105k prompt re-billed at $3.75/M write
|
|
const turn3 = assistant({ cacheWrite: 110_000, cost: { cacheWrite: 0.4125 }, timestamp: 120_000 });
|
|
const totals = computeCacheWaste([entry(turn1), entry(turn2), entry(turn3)], models);
|
|
expect(totals.missedTokens).toBe(105_000);
|
|
// 105k at ($3.75 - $0.30)/M
|
|
expect(totals.missedCost).toBeCloseTo(0.36225, 5);
|
|
});
|
|
|
|
it("counts nothing for healthy sessions", () => {
|
|
const totals = computeCacheWaste([entry(turn1), entry(turn2)], models);
|
|
expect(totals.missedTokens).toBe(0);
|
|
expect(totals.missedCost).toBe(0);
|
|
});
|
|
|
|
it("skips the turn after a compaction reset", () => {
|
|
const reset = { type: "compaction", id: "c", parentId: null, timestamp: "" } as SessionEntry;
|
|
const afterReset = assistant({ cacheWrite: 20_000, cost: { cacheWrite: 0.075 } });
|
|
const totals = computeCacheWaste([entry(turn1), reset, entry(afterReset)], models);
|
|
expect(totals.missedTokens).toBe(0);
|
|
});
|
|
|
|
it("counts misses caused by model switches", () => {
|
|
const otherModel = assistant({ cacheWrite: 100_000, cost: { cacheWrite: 0.375 }, model: "other-model" });
|
|
const totals = computeCacheWaste([entry(turn1), entry(otherModel)], models);
|
|
expect(totals.missedTokens).toBe(100_000);
|
|
expect(totals.missCount).toBe(1);
|
|
});
|
|
|
|
it("skips providers that report no cache activity", () => {
|
|
const a = assistant({ input: 100_000 });
|
|
const b = assistant({ input: 110_000 });
|
|
const totals = computeCacheWaste([entry(a), entry(b)], models);
|
|
expect(totals.missedTokens).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe("collectCacheMisses", () => {
|
|
it("maps counted misses to their assistant messages by reference", () => {
|
|
const missTurn = assistant({ cacheWrite: 110_000, cost: { cacheWrite: 0.4125 }, timestamp: 120_000 });
|
|
const misses = collectCacheMisses([entry(turn1), entry(turn2), entry(missTurn)], models);
|
|
expect(misses.size).toBe(1);
|
|
expect(misses.get(missTurn)?.missedTokens).toBe(105_000);
|
|
});
|
|
});
|
|
|
|
describe("detectCacheMiss", () => {
|
|
it("detects a miss on a just-completed message with idle time", () => {
|
|
const missMessage = assistant({ cacheWrite: 110_000, cost: { cacheWrite: 0.4125 }, timestamp: 600_000 });
|
|
const miss = detectCacheMiss([entry(turn1), entry(turn2)], missMessage, models);
|
|
expect(miss).toBeDefined();
|
|
expect(miss?.missedTokens).toBe(105_000);
|
|
expect(miss?.missedCost).toBeCloseTo(0.36225, 5);
|
|
// 600s - 60s since the previous request
|
|
expect(miss?.idleMs).toBe(540_000);
|
|
expect(miss?.modelChanged).toBe(false);
|
|
});
|
|
|
|
it("flags model switches on detected misses", () => {
|
|
const otherModel = assistant({
|
|
cacheWrite: 110_000,
|
|
cost: { cacheWrite: 0.4125 },
|
|
model: "other-model",
|
|
timestamp: 120_000,
|
|
});
|
|
const miss = detectCacheMiss([entry(turn1), entry(turn2)], otherModel, models);
|
|
expect(miss?.missedTokens).toBe(105_000);
|
|
expect(miss?.modelChanged).toBe(true);
|
|
});
|
|
|
|
it("returns undefined for healthy turns", () => {
|
|
const healthy = assistant({
|
|
cacheRead: 105_000,
|
|
cacheWrite: 2_000,
|
|
cost: { cacheRead: 0.0315, cacheWrite: 0.0075 },
|
|
timestamp: 120_000,
|
|
});
|
|
expect(detectCacheMiss([entry(turn1), entry(turn2)], healthy, models)).toBeUndefined();
|
|
});
|
|
|
|
it("returns undefined for the first turn of a session", () => {
|
|
expect(detectCacheMiss([], turn1, models)).toBeUndefined();
|
|
});
|
|
});
|