summaryrefslogtreecommitdiffhomepage
path: root/packages/frontend/src/lib/components/CacheRatePanel.svelte
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-05-30 18:53:42 +0900
committerAdam Malczewski <[email protected]>2026-05-30 18:53:42 +0900
commit2228691e14be2368394e38e600bfa2ce227487b1 (patch)
treed6b3cfffd11dc9f7eec8c88f5fc97a7ec81e61a0 /packages/frontend/src/lib/components/CacheRatePanel.svelte
parent497b397e873f96d6fde3d8a44b3318e1ee1cbef4 (diff)
downloaddispatch-2228691e14be2368394e38e600bfa2ce227487b1.tar.gz
dispatch-2228691e14be2368394e38e600bfa2ce227487b1.zip
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
Diffstat (limited to 'packages/frontend/src/lib/components/CacheRatePanel.svelte')
-rw-r--r--packages/frontend/src/lib/components/CacheRatePanel.svelte129
1 files changed, 129 insertions, 0 deletions
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 @@
+<script lang="ts">
+import type { CacheStats } from "../types.js";
+
+const {
+ cacheStats = null,
+ tabTitle = null,
+}: {
+ cacheStats?: CacheStats | null;
+ tabTitle?: string | null;
+} = $props();
+
+// Cache hit rate = cached-read tokens / total prompt tokens. `inputTokens` is
+// the TOTAL prompt (fresh + cache read + cache write), so this is the share of
+// the prompt that was served from Anthropic's prompt cache.
+function rate(read: number, totalInput: number): number {
+ if (totalInput <= 0) return 0;
+ return Math.max(0, Math.min(1, read / totalInput));
+}
+
+// For caching, a HIGH hit rate is GOOD — invert the usual color thresholds.
+function rateClass(r: number): string {
+ if (r >= 0.7) return "progress-success";
+ if (r >= 0.3) return "progress-warning";
+ return "progress-error";
+}
+
+function fmt(n: number): string {
+ return n.toLocaleString();
+}
+
+const hitRate = $derived(cacheStats ? rate(cacheStats.cacheReadTokens, cacheStats.inputTokens) : 0);
+const hitPct = $derived(Math.round(hitRate * 100));
+const uncached = $derived(
+ cacheStats
+ ? Math.max(0, cacheStats.inputTokens - cacheStats.cacheReadTokens - cacheStats.cacheWriteTokens)
+ : 0,
+);
+const lastHitPct = $derived(
+ cacheStats?.last
+ ? Math.round(rate(cacheStats.last.cacheReadTokens, cacheStats.last.inputTokens) * 100)
+ : 0,
+);
+</script>
+
+<div class="flex flex-col gap-3 flex-1 min-h-0 overflow-y-auto">
+ {#if !cacheStats || cacheStats.requests === 0}
+ <p class="text-xs text-base-content/50">
+ No cache data yet. Send a message to a Claude model — prompt-cache usage
+ appears here after the first response.
+ </p>
+ {:else}
+ <div class="bg-base-200 rounded-lg p-2">
+ <div class="flex items-center gap-1.5 mb-2">
+ <span class="text-xs font-semibold">Cache Hit Rate</span>
+ {#if tabTitle}
+ <span class="badge badge-xs badge-ghost">{tabTitle}</span>
+ {/if}
+ <span class="badge badge-xs ml-auto">{cacheStats.requests} req</span>
+ </div>
+
+ <!-- Headline cumulative hit rate -->
+ <div class="flex flex-col gap-0.5">
+ <div class="flex items-center justify-between">
+ <span class="text-xs text-base-content/50">Session (this tab)</span>
+ <span class="text-xs font-mono">{hitPct}%</span>
+ </div>
+ <progress
+ class="progress w-full h-2 {rateClass(hitRate)}"
+ value={hitPct}
+ max="100"
+ ></progress>
+ </div>
+
+ <!-- Most recent request -->
+ {#if cacheStats.last}
+ <div class="flex flex-col gap-0.5 mt-2">
+ <div class="flex items-center justify-between">
+ <span class="text-xs text-base-content/50">Last request</span>
+ <span class="text-xs font-mono">{lastHitPct}%</span>
+ </div>
+ <progress
+ class="progress w-full h-2 {rateClass(lastHitPct / 100)}"
+ value={lastHitPct}
+ max="100"
+ ></progress>
+ </div>
+ {/if}
+ </div>
+
+ <!-- Token breakdown (cumulative, this tab) -->
+ <div class="bg-base-200 rounded-lg p-2">
+ <div class="text-xs font-semibold mb-1.5">Tokens (cumulative)</div>
+ <div class="flex flex-col gap-1 pl-1">
+ <div class="flex items-center justify-between">
+ <span class="text-xs text-base-content/50">
+ <span class="badge badge-xs badge-success badge-soft mr-1">read</span>Cache hits
+ </span>
+ <span class="text-xs font-mono">{fmt(cacheStats.cacheReadTokens)}</span>
+ </div>
+ <div class="flex items-center justify-between">
+ <span class="text-xs text-base-content/50">
+ <span class="badge badge-xs badge-warning badge-soft mr-1">write</span>Cache writes
+ </span>
+ <span class="text-xs font-mono">{fmt(cacheStats.cacheWriteTokens)}</span>
+ </div>
+ <div class="flex items-center justify-between">
+ <span class="text-xs text-base-content/50">
+ <span class="badge badge-xs badge-error badge-soft mr-1">fresh</span>Uncached input
+ </span>
+ <span class="text-xs font-mono">{fmt(uncached)}</span>
+ </div>
+ <div class="border-t border-base-300 my-0.5"></div>
+ <div class="flex items-center justify-between">
+ <span class="text-xs text-base-content/50">Total input</span>
+ <span class="text-xs font-mono">{fmt(cacheStats.inputTokens)}</span>
+ </div>
+ <div class="flex items-center justify-between">
+ <span class="text-xs text-base-content/50">Output</span>
+ <span class="text-xs font-mono">{fmt(cacheStats.outputTokens)}</span>
+ </div>
+ </div>
+ </div>
+
+ <p class="text-xs text-base-content/40">
+ 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.
+ </p>
+ {/if}
+</div>