From 2228691e14be2368394e38e600bfa2ce227487b1 Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Sat, 30 May 2026 18:53:42 +0900 Subject: feat(cache): Anthropic prompt caching, usage telemetry, and Cache Rate view - send prompt-caching + oauth anthropic-beta headers on the Claude OAuth provider - restructure the OAuth request body (billing header, identity split, relocate third-party system prompt to the first user message) to match Claude Code - apply rolling cache_control breakpoints and group a turn's tool results into a single role:tool message for correct breakpoint placement - emit per-step usage events (cache read/write split) and add the Cache Rate sidebar panel - dedup byte-identical tool calls within a single batch --- .../src/lib/components/CacheRatePanel.svelte | 129 +++++++++++++++++++++ .../src/lib/components/SidebarPanel.svelte | 14 ++- packages/frontend/src/lib/tabs.svelte.ts | 30 +++++ packages/frontend/src/lib/types.ts | 32 +++++ 4 files changed, 202 insertions(+), 3 deletions(-) create mode 100644 packages/frontend/src/lib/components/CacheRatePanel.svelte (limited to 'packages/frontend/src/lib') diff --git a/packages/frontend/src/lib/components/CacheRatePanel.svelte b/packages/frontend/src/lib/components/CacheRatePanel.svelte new file mode 100644 index 0000000..c35cbb5 --- /dev/null +++ b/packages/frontend/src/lib/components/CacheRatePanel.svelte @@ -0,0 +1,129 @@ + + +
+ {#if !cacheStats || cacheStats.requests === 0} +

+ No cache data yet. Send a message to a Claude model — prompt-cache usage + appears here after the first response. +

+ {:else} +
+
+ Cache Hit Rate + {#if tabTitle} + {tabTitle} + {/if} + {cacheStats.requests} req +
+ + +
+
+ Session (this tab) + {hitPct}% +
+ +
+ + + {#if cacheStats.last} +
+
+ Last request + {lastHitPct}% +
+ +
+ {/if} +
+ + +
+
Tokens (cumulative)
+
+
+ + readCache hits + + {fmt(cacheStats.cacheReadTokens)} +
+
+ + writeCache writes + + {fmt(cacheStats.cacheWriteTokens)} +
+
+ + freshUncached input + + {fmt(uncached)} +
+
+
+ Total input + {fmt(cacheStats.inputTokens)} +
+
+ Output + {fmt(cacheStats.outputTokens)} +
+
+
+ +

+ Cache reads cost ~10% of fresh input; writes cost ~25% more. A high hit + rate after the first turn means caching is working. Resets on reload. +

+ {/if} +
diff --git a/packages/frontend/src/lib/components/SidebarPanel.svelte b/packages/frontend/src/lib/components/SidebarPanel.svelte index fca53b7..33b2158 100644 --- a/packages/frontend/src/lib/components/SidebarPanel.svelte +++ b/packages/frontend/src/lib/components/SidebarPanel.svelte @@ -1,6 +1,7 @@ @@ -156,6 +162,8 @@ function contentClass(selected: string): string { /> {:else if panel.selected === "Key Usage"} + {:else if panel.selected === "Cache Rate"} + {:else if panel.selected === "Claude Reset"} {:else if panel.selected === "Model Status"} diff --git a/packages/frontend/src/lib/tabs.svelte.ts b/packages/frontend/src/lib/tabs.svelte.ts index bb8f4cf..6e2d157 100644 --- a/packages/frontend/src/lib/tabs.svelte.ts +++ b/packages/frontend/src/lib/tabs.svelte.ts @@ -12,6 +12,7 @@ import { config } from "./config.js"; import { appSettings } from "./settings.svelte.js"; import type { AgentEvent, + CacheStats, ChatMessage, Chunk, DebugInfo, @@ -89,6 +90,12 @@ export interface Tab { oldestLoadedSeq: number | null; /** Total number of messages for this tab on the backend */ totalMessages: number; + /** + * Cumulative prompt-cache token telemetry for this tab since the page + * loaded (in-memory only — resets on reload). Undefined until the first + * `usage` event arrives. Drives the "Cache Rate" sidebar view. + */ + cacheStats?: CacheStats; } /** @@ -846,6 +853,29 @@ export function createTabStore() { applyChunkEvent(tabId, event); break; } + case "usage": { + if (!tabId) break; + const tab = getTabById(tabId); + if (!tab) break; + const u = event.usage; + const prev = tab.cacheStats; + updateTab(tabId, { + cacheStats: { + inputTokens: (prev?.inputTokens ?? 0) + u.inputTokens, + outputTokens: (prev?.outputTokens ?? 0) + u.outputTokens, + cacheReadTokens: (prev?.cacheReadTokens ?? 0) + u.cacheReadTokens, + cacheWriteTokens: (prev?.cacheWriteTokens ?? 0) + u.cacheWriteTokens, + requests: (prev?.requests ?? 0) + 1, + last: { + inputTokens: u.inputTokens, + outputTokens: u.outputTokens, + cacheReadTokens: u.cacheReadTokens, + cacheWriteTokens: u.cacheWriteTokens, + }, + }, + }); + break; + } case "done": { if (!tabId) break; const tab5 = getTabById(tabId); diff --git a/packages/frontend/src/lib/types.ts b/packages/frontend/src/lib/types.ts index f9add94..8c34d69 100644 --- a/packages/frontend/src/lib/types.ts +++ b/packages/frontend/src/lib/types.ts @@ -11,6 +11,29 @@ export interface DebugInfo { httpBody?: string; } +/** + * Per-tab prompt-cache telemetry, accumulated from the `usage` AgentEvent + * (one per LLM round-trip). Token counts are cumulative across the session + * since the page loaded; `last` holds the most recent request's split. Powers + * the "Cache Rate" sidebar view. The cache hit rate is + * `cacheReadTokens / inputTokens` (inputTokens is the TOTAL prompt, including + * cached tokens). + */ +export interface CacheStats { + inputTokens: number; + outputTokens: number; + cacheReadTokens: number; + cacheWriteTokens: number; + /** Number of LLM requests (usage events) counted. */ + requests: number; + last: { + inputTokens: number; + outputTokens: number; + cacheReadTokens: number; + cacheWriteTokens: number; + } | null; +} + /** * Mirror of the core `Chunk` union (see packages/core/src/types/index.ts). * @@ -119,6 +142,15 @@ export type AgentEvent = type: "tool-result"; toolResult: { toolCallId: string; result: string; isError: boolean }; } + | { + type: "usage"; + usage: { + inputTokens: number; + outputTokens: number; + cacheReadTokens: number; + cacheWriteTokens: number; + }; + } | { type: "error"; error: string } | { type: "notice"; message: string } | { type: "model-changed"; keyId: string; modelId: string } -- cgit v1.2.3