From 7ffb6b28f5b6bdbfc53ebed94fc68af557612189 Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Thu, 11 Jun 2026 14:11:13 +0900 Subject: fix(cache-warming): accurate cache rate + expectedCacheRate (retention) metric The Claude cache % read 100% whenever anything was cached, because the metric's denominator (inputTokens) excluded cached tokens on Anthropic. Fixed upstream in ../claude/provider-anthropic (inputTokens = total prompt); this commit adds the companion retention metric and exposes it: - transport-contract: WarmResponse += expectedCacheRate - transport-http: POST /chat/warm returns expectedCacheRate = cacheRead/(cacheRead+cacheWrite) - cache-warming: computeExpectedCacheRate + a per-conversation 'cache retention' surface stat - handoff: documents the fix + cache-rate vs expected-cache (cross-turn) for the FE Live-verified vs claude haiku: real turn cache rate 61% (was inflated 100%); warm within TTL expectedCacheRate=100%, after expiry=0%. --- packages/cache-warming/src/pure.ts | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) (limited to 'packages/cache-warming/src/pure.ts') diff --git a/packages/cache-warming/src/pure.ts b/packages/cache-warming/src/pure.ts index 7b91b11..ab6fc79 100644 --- a/packages/cache-warming/src/pure.ts +++ b/packages/cache-warming/src/pure.ts @@ -17,6 +17,7 @@ export interface ConversationSettings { export interface ConversationState extends ConversationSettings { readonly active: boolean; readonly lastPct: number | null; + readonly lastExpectedPct: number | null; readonly token: number; } @@ -42,6 +43,21 @@ export function computeCachePct(inputTokens: number, cacheReadTokens: number): n return Math.round(clamped * 100); } +/** + * Compute expected cache retention rate from token counts. + * Of the cacheable prefix the warm touched, how much was still warm (read back) + * vs. had to be (re)written. + * Returns an integer in [0, 100]. cacheRead + cacheWrite ≤ 0 → 0. + */ +export function computeExpectedCacheRate( + cacheReadTokens: number, + cacheWriteTokens: number, +): number { + const total = cacheReadTokens + cacheWriteTokens; + if (total <= 0) return 0; + return Math.round((cacheReadTokens / total) * 100); +} + /** * Decide whether a conversation should be warmed right now. * Requires: enabled, idle (not active), and the token is current (not superseded). @@ -120,8 +136,10 @@ export function buildConversationSpec( enabled: boolean, intervalMs: number, lastPct: number | null, + lastExpectedPct: number | null, ): SurfaceSpec { const pctDisplay = lastPct === null ? "—" : `${lastPct}%`; + const retentionDisplay = lastExpectedPct === null ? "—" : `${lastExpectedPct}%`; const toggle: ToggleField = { kind: "toggle", label: "Enabled", @@ -142,11 +160,16 @@ export function buildConversationSpec( label: "Last Cache %", value: pctDisplay, }; + const retentionStat: StatField = { + kind: "stat", + label: "Cache retention", + value: retentionDisplay, + }; return { id: "cache-warming", region: "side", title: "Cache Warming", - fields: [toggle, interval, stat], + fields: [toggle, interval, stat, retentionStat], }; } -- cgit v1.2.3