Token spend analytics and cost alerts for Claude Code. 16 report modes including weekly credit consumption against your plan cap, cache intelligence, tooling…

Analytics for your Claude Code token spend.
Reads Claude Code's JSONL session files (and optionally ~/.claude/__store.db) in read-only mode and shows exactly where your tokens and money go — by tool, project, session, thinking blocks, cache efficiency, context bloat, and cost per turn.
Claude Code users have no built-in visibility into token spend. token-scope fills that gap retroactively, with no proxy, no tokenizer, and no daemon. The data is already on disk.
Bun 1.1.0+
curl -fsSL https://bun.sh/install | bash
git clone https://github.com/nhangen/token-scope ~/ML-AI/claude/token-scope
cd ~/ML-AI/claude/token-scope
bun install
bun link # makes `token-scope` available from any directory
token-scope --since 7d
token-scope — Summary (Apr 1, 2026 → now)
────────────────────────────────────────────────────────────
Sessions: 236 Turns: 5,966
Totals
Output Tokens 1,617,245
Cache Read Tokens 412,581,387
Total Cost $401.77
Avg Cost / Session $1.70
Avg Cost / Turn $0.07
Output Tokens by Tool
Tool │ Turns │ Output Tokens │ Cost %
──────────────────┼────────┼───────────────┼───────
(text only) │ 4,227 │ 868,702 │ 56.5%
Bash │ 900 │ 272,348 │ 22.3%
Agent │ 147 │ 167,949 │ 7.1%
Write │ 50 │ 155,838 │ 1.4%
...
Output Tokens by Project
Project │ Sessions │ Turns │ Total Cost │ Avg/Session
─────────────────────────┼──────────┼───────┼────────────┼────────────
appoptinmonstertest │ 8 │ 1,140 │ $96.37 │ $12.05
token-scope │ 1 │ 314 │ $17.03 │ $17.03
...
Weekly Trend
Week │ Sessions │ Turns │ Output Tokens │ Total Cost
─────────┼──────────┼────────┼───────────────┼───────────
2026-W14 │ 222 │ 4,749 │ 1,362,379 │ $198.71
2026-W13 │ 16 │ 1,217 │ 254,866 │ $203.06
token-scope --tool bash --since 7d
token-scope — Tool: Bash
────────────────────────────────────────────────────────────
Turns (dominant) 902
Total Cost $89.78
Share of All Cost 22.3%
Distribution (p50/p95/max) 160 / 799 / 9,875
Command Categories
Category │ Turns │ Output Tokens │ Total Cost
────────────────┼───────┼───────────────┼───────────
Other │ 508 │ 136,524 │ $57.43
File Inspection │ 272 │ 91,402 │ $16.41
Version Control │ 54 │ 10,612 │ $10.99
JS Tooling │ 22 │ 6,773 │ $1.18
...
token-scope --sessions --since 7d
token-scope — Sessions (last 7d)
────────────────────────────────────────────────────────────
Session ID │ Project │ Started │ Duration │ Turns │ Cost
───────────────┼──────────────────────┼─────────────────┼────────────┼───────┼──────────
be299042-801f- │ appoptinmonstertest │ Apr 02 at 20:59 │ 123:47:33 │ 682 │ $105.19
04442160-fea2- │ appoptinmonstertest │ Apr 06 at 14:21 │ 52:38:20 │ 806 │ $47.90
07896aa6-5249- │ mtf-builder │ Apr 07 at 17:45 │ 07:01:05 │ 141 │ $9.88
...
token-scope --session be299042
Turn-by-turn breakdown with output tokens, cost, tool used, and thinking indicator for every turn in the session.
token-scope --spend --session be299042 # whole session
token-scope --spend --session be299042 --turns 5..12 # isolate one task
token-scope --spend --turns 5..12 # most-recent session
Isolates the Claude (billed) token spend — output, input, cache-read, cache-write, and a derived cost — for one session, sliced to a turn range so a single task can be measured. Rolls up subagent (Task/Agent) overhead so PM-loop cost (auditors, explorers) is visible rather than hidden. --session picks the session (defaults to the most recent); --turns N..M (also N.., ..M, N) is a 1-indexed inclusive slice; --since acts as a within-session timestamp floor when set.
Notes: subagent totals are session-wide in v1 (not scoped to the turn range), and require the JSONL source (subagent transcripts aren't in the SQLite store). Turns on a model with no known pricing still count their tokens; only their cost is excluded. Local (ollama) authoring runs off-transcript and is not counted — so a delegated task's spend shows just the thin Claude PM overhead. Model labels in displayed turns pass the credential screen: credential-shaped values print as [redacted], and --json reports the number of withheld turn labels in redacted_labels (the text report adds a footer only when any are redacted).
token-scope --savings # all delegation runs
token-scope --savings --session be299042 # one delegation session
token-scope --savings --counterfactual-model claude-sonnet-5 # value against a cheaper tier
token-scope --savings --session be299042 --pm-turns 4..9 # net vs. just the delegation's PM turns
token-scope --savings --session be299042 --pm-cost 0.87 # net vs. a measured PM figure (subagent PM)
token-scope --savings --ledger /path/to/runs.jsonl # explicit ledger location
token-scope --savings --escalations /path/to/escalations.jsonl # explicit escalation sidecar
Answers the "did delegating authorship to a local ollama model actually save money?" question. Reads the ollama-agent run ledger ($XDG_STATE_HOME/ollama-agent/runs.jsonl, or $OLLAMA_AGENT_LEDGER) — the bridge records each run's local token counts (ground truth from ollama's eval_count/prompt_eval_count) plus the Claude session that spawned it. token-scope prices them; the bridge never guesses cost.
For each delegation session it reports the net savings:
Net = Counterfactual − PM overhead
claude-opus-4-8): an estimate of what Claude authoring the same work would have cost. ollama and Claude tokenize differently, so this is a proxy, not a measured figure.--pm-turns N..M (requires --session) to scope PM overhead to just the delegation's orchestration turns — the only way to get a meaningful per-task net from transcripts. The ledger has no delegation-start marker, so the turn window can't be auto-derived; you isolate it, exactly as with --spend --turns. (Turn-scoped PM excludes session-wide subagent cost, so it's a floor.)--spend runs) and pass it as --pm-cost <usd> (requires --session, mutually exclusive with --pm-turns). The report takes the figure on trust and labels the scope measured (caller). Because no transcript lookup happens, this also attributes sessions that ran on another machine.When a spec hits the turn cap twice, ollama-delegate no longer stops — it hands the same spec to a higher-tier author (a Sonnet subagent in Claude Code, gpt-5.6-terra/-luna in Codex). That author is billed for the job. Pricing the local attempts as "what Claude would have cost" then counts one job twice: once as a counterfactual saving and once as real spend.
So token-scope reads a sidecar written by llm-tools' ~/.claude/scripts/ollama-record-escalation.sh:
$XDG_STATE_HOME/ollama-agent/escalations.jsonl (or $OLLAMA_AGENT_ESCALATIONS, or --escalations)
It is a sibling of runs.jsonl rather than a field on it — that file is written by claude-ceo's bridge, and a new field there risks the parsing this report depends on.
A ledger run is superseded when a record names its run_id, ran in the same cwd, the run itself failed, and it falls in the OLLAMA_ATTEMPT_GAP window (default 4h) before the record. All four matter: a label is a ticket number and gets reused, a later run on the same ticket that succeeded saved real work, and a run outside the window belongs to an earlier cycle. Three shapes cannot be decided and all three stay in the counterfactual, over-stating the saving rather than inventing an exclusion: a run with no timestamp, a legacy run with no cwd, and a run whose reason/completed/verified are all unrecorded (the ledger defines that as "not recorded — never a claim about the run", and the matcher reads it as "succeeded"). A record with an empty cwd is read as having none, so it matches nothing — the recorder refuses to write one and calls such a record not discountable at all.
Superseded runs are excluded from the counterfactual — unlike runs that merely failed, which stay in it and are footnoted — but their tokens remain in the ledger totals, so no spend is hidden. --json adds superseded_run_count, superseded_input, superseded_output, and superseded_excluded_usd (the dollar figure the exclusion removed) whenever the report found any.
A sidecar that fails to load is never reported as one that legitimately held nothing. That distinction is the whole point of the feature: both produce zero exclusions, and zero exclusions is a full, confident, wrong counterfactual — the double-count #81 removed, back again. So --json always carries escalations_path, escalations_status (ok/absent/unreadable), escalations_records, escalations_skipped_lines, escalations_unmatched, and attempt_gap_seconds, whether or not anything was superseded. An unreadable file warns on stderr; so does a missing file whose path you named yourself, since a path you typed is an assertion that it is there.
Records that name a run the ledger does not contain are footnoted rather than dropped. Matching keys on cwd as well as run_id, so a worktree that moved after the escalation breaks every record naming it — silently restoring the double-count if nothing counts the misses. A record whose run is present but ineligible (it succeeded, or it cannot be dated) is doing its job by not firing and is not counted, and the footnote is suppressed on a --session/--since report, where scoping leaves most records naming nothing by construction.
The same disclosure covers the ledger itself: ledger_status and ledger_skipped_lines sit beside ledger_path, and an unreadable ledger warns on stderr. It is the primary source of every figure in the report, so disclosing the sidecar's load status and not the ledger's would advertise an integrity the report does not have.
A positive net means delegation saved money. Note the economics: for a small task the counterfactual is tiny, so a single expensive PM turn can exceed it (net negative) — delegation pays off as the authored task grows large relative to the fixed PM overhead. Runs with no attributable Claude session (null session_id, or a session not in the local transcripts) are counted for token volume but excluded from the net headline. --since floors by ledger timestamp when set.
token-scope --project wp-content --since 30d
Filters all reports to sessions where wp-content appears in the working directory path.
token-scope --cache --since 30d
token-scope --cache-misses --since 30d # focus on miss classification and gap histogram
token-scope --cache-sim --since 30d # focus on policy simulations
token-scope --cache --json # machine-readable full analysis
token-scope — Cache Economics & Efficiency
────────────────────────────────────────────────────────────
Projects 20
Overall Cache Hit % 96.2% (read / (read + write + input))
Read Share (non-write) 99.8% (legacy read / (read + input))
Est. Total Savings $10159.6300
Rebuild Miss Cost $143.2100
Reader vs Tier-Priced $412.6000 vs $427.7000 (Δ $-15.1000)
Input Token & Cost Mix
Category │ Tokens │ Token % │ Cost ($) │ Cost % │ Credits │ Credits %
─────────────────┼────────────────┼───────────┼─────────────┼──────────┼────────────────┼───────────
Cache Read │ 895,151,000 │ 96.2% │ $268.5500 │ 62.8% │ 89,515,100 │ 51.1%
Cache Write │ 24,800,000 │ 2.7% │ $93.0000 │ 21.7% │ 31,000,000 │ 17.7%
Uncached Input │ 10,500,000 │ 1.1% │ $66.1500 │ 15.5% │ 54,500,000 │ 31.2%
By Project
Project │ Sessions │ Turns │ Cache Hit % │ Read Share │ Cache Reads │ Est. Savings
─────────────────────────────┼──────────┼───────┼─────────────┼────────────┼─────────────┼─────────────
public │ 12 │ 3,431 │ 97.1% │ 100.0% │ 405,001,821 │ $3974.1500
wp-content-6879-hubspot-v3 │ 2 │ 449 │ 95.4% │ 100.0% │ 130,063,498 │ $1700.5500
wp-content │ 4 │ 863 │ 96.8% │ 100.0% │ 122,866,030 │ $1513.6800
...
Miss Classification & Extra Rebuild Costs
Loop │ Classification │ Requests │ Extra Cost │ Note
───────────────┼──────────────────┼────────────┼──────────────┼─────────────────────────────────────
Main session │ Expiry (>1h gap) │ 18 │ $112.4000 │ Idle pause exceeded 1h TTL; rebuilt
Main session │ Growth (no gap) │ 4,720 │ $0.0000 │ Normal incremental context expansion
Main session │ Invalidation │ 4 │ $18.2000 │ Prefix invalidated (model/tools)
Main session │ Cold initial │ 24 │ $0.0000 │ Session start / first request
Subagents │ Cold starts │ 82 │ $0.0000 │ Subagent first turn (5m TTL)
Subagents │ Expiry (>5m gap) │ 6 │ $12.2000 │ Subagent pause exceeded 5m TTL
Subagents │ Growth (no gap) │ 190 │ $0.0000 │ Subagent context growth
Subagents │ Invalidation │ 1 │ $0.4100 │ Subagent prefix invalidated
Request Gap Distribution (Main Loop)
Bucket │ Count │ Share │ Distribution
─────────────┼─────────┼─────────┼──────────────────────────────
< 1m │ 3412 │ 71.8% │ █████████████████
1m – 5m │ 980 │ 20.6% │ █████
─────────────── 5m TTL (subagents / 5m policy / overage) ───────────────
5m – 15m │ 210 │ 4.4% │ █
15m – 30m │ 95 │ 2.0% │
30m – 60m │ 38 │ 0.8% │
─────────────── 1h TTL (default main-session policy) ───────────────────
1h – 2h │ 12 │ 0.3% │
2h – 4h │ 4 │ 0.1% │
> 4h │ 2 │ 0.0% │
What Would Have Saved (Simulations)
# │ Policy Change │ Est. Savings/Wk │ Credits Saved │ Simulated Cost
────┼────────────────────────────────────────────┼───────────────────┼─────────────────┼───────────────
1 │ Run /compact before breaks > 1 hour │ ~$24.50/wk│ ~6,125,000/wk│ $403.2000
2 │ Start a fresh session instead of resuming after > 24h │ ~$18.20/wk│ ~4,550,000/wk│ $409.5000
3 │ Lower auto-compact threshold to 35% of context window│ ~$11.40/wk│ ~2,850,000/wk│ $416.3000
4 │ Switch main session to 5m TTL │ +$8.90/wk cost│ +2,225,000/wk│ $436.6000
--credits):usage.cache_creation.ephemeral_1h_input_tokens; writes with no split are priced at the 5m rate.hit and are excluded from miss counts and items:expiry: Time gap since the previous request on the same loop $> \text{TTL}$ (1h default for main session, 5m for subagents). The cache expired due to idle time; the previous context prefix had to be rewritten. $$\text{Rebuild Prefix} = \min(\text{prev\_context}, \text{cache\_write\_tokens})$$ $$\text{Expiry Cost} = \frac{\text{Rebuild Prefix} \times (P_{\text{write}} - P_{\text{read}})}{1,000,000}$$ where $P_{\text{write}}$ is the turn's blended write rate across its 5m and 1h tiers.growth: Gap $\le \text{TTL}$, and write corresponds to new content entering context ($\text{cache\_read} \approx \text{prev\_context}$). Extra miss cost is $\$0.00$.invalidation: Gap $\le \text{TTL}$, but prefix read was missed and write is far larger than new content (model switch, system prompt / tools change, or compaction). Priced with the same rebuild formula as expiry; the summary's Rebuild Miss Cost is expiry plus invalidation across both loops.cold: First turn of a main session or subagent loop.summary.unpriced_turns and summary.unpriced_models.token-scope --efficiency --since 30d
token-scope — Session Efficiency
────────────────────────────────────────────────────────────
Total Sessions 1,162
Cost Per Turn by Session Length
Turn Bucket │ Sessions │ Avg Turns │ Avg Per-Turn Cost │ Avg Session Cost
────────────┼──────────┼───────────┼──────────────────┼─────────────────
1–5 │ 962 │ 2.7 │ $0.010 │ $0.027
6–15 │ 130 │ 7.7 │ $0.025 │ $0.191
16–30 │ 28 │ 20.1 │ $0.034 │ $0.678
31–50 │ 13 │ 40.5 │ $0.077 │ $3.105
51+ │ 29 │ 315.6 │ $0.192 │ $60.626
Longer sessions accumulate more context, driving up per-turn cost. A 51+ turn session costs ~20× more per turn than a 1–5 turn session.
token-scope --context --since 30d
token-scope — Context Bloat Analysis
────────────────────────────────────────────────────────────
Sessions Analyzed 20
Sessions Ranked by Bloat (early = avg of first 3 turns, late = avg of last 3 turns)
Session │ Project │ Turns │ Early Avg Input │ Late Avg Input │ Bloat
───────────────┼───────────────────┼───────┼─────────────────┼────────────────┼──────
cbb12eb7-b13b- │ HW │ 347 │ 3 │ 3 │ 1.0×
58279086-c21b- │ observer-sessions │ 23 │ 10 │ 10 │ 1.0×
...
Sessions with ≥6 turns, ranked by how much input token count grows from early turns to late turns. High bloat ratios indicate context accumulation driving up costs.
token-scope --thinking --since 7d
token-scope — Thinking Analysis
────────────────────────────────────────────────────────────
~Total Thinking Tokens (est) ~126,832
~Thinking % of Output ~7.8%
Turns with Thinking 2,055 (34.4% of all turns)
Sessions with Thinking 232 (98.3%)
By Project
Project │ Thinking Sessions │ Thinking Turns │ ~Total Thinking Tokens
─────────────────┼───────────────────┼────────────────┼───────────────────────
observer-sessions│ 202 │ 1,899 │ ~117,274
3body │ 1 │ 20 │ ~8,159
wp-content │ 2 │ 16 │ ~155
...
Thinking token estimates use a character-ratio proxy (±15–30% error). All thinking figures are prefixed with ~.
token-scope --tools --since 30d
token-scope — Tooling Analysis
────────────────────────────────────────────────────────────
Total Tool Calls 4,716
Distinct Tools 23
Layers Active 4 of 5
Unclassified Tools 0
Cost by Layer
Layer │ Calls │ Attributed Cost │ Cost % │ Avg/Call
─────────────┼────────┼─────────────────┼─────────┼──────────
Built-in │ 3,890 │ $342.18 │ 85.2% │ $0.088
MCP │ 61 │ $12.40 │ 3.1% │ $0.203
Plugin │ 17 │ $4.80 │ 1.2% │ $0.282
Skill │ 67 │ $3.42 │ 0.9% │ $0.051
Meta │ 241 │ $5.26 │ 1.3% │ $0.022
(no tool) │ 440 │ $33.57 │ 8.4% │ $0.076
Classifies every tool call into five layers and attributes cost proportionally by input payload size. Plugins are MCP servers provided by Claude Code plugins (mcp__plugin_*). Hooks are invisible (shell commands, not tool_use blocks).
hooks/register.ts 188 lines1import type { Register, Timer } from "claude-code";
2
3export function isEnvTrue(val: string | undefined): boolean {
4 if (!val) return false;
5 const s = val.trim().toLowerCase();
6 return s !== "" && s !== "0" && s !== "false" && s !== "no" && s !== "off";
7}
8
9export function parseTtl(val: unknown): number | undefined {
10 let ms: number | undefined;
11 if (typeof val === "number") {
12 ms = val * 1000;
13 } else if (typeof val === "string") {
14 const match = /^(\d+(?:\.\d+)?)\s*(ms|s|m|h)?$/.exec(val.trim().toLowerCase());
15 if (match) {
16 const n = parseFloat(match[1]!);
17 const unit = match[2] ?? "s";
18 ms = n * { ms: 1, s: 1000, m: 60 * 1000, h: 60 * 60 * 1000 }[unit]!;
19 }
20 }
21 if (ms === undefined || !Number.isFinite(ms) || ms <= 0) return undefined;
22 return Math.round(ms);
23}
24
25export function resolveTtl(
26 env: Record<string, string | undefined> = {},
27 settings?: Record<string, unknown>
28): number | null {
29 if (isEnvTrue(env["DISABLE_PROMPT_CACHING"])) {
30 return null;
31 }
32 if (isEnvTrue(env["FORCE_PROMPT_CACHING_5M"])) {
33 return 5 * 60 * 1000;
34 }
35 for (const candidate of [env["CLAUDE_CODE_PROMPT_CACHE_TTL"], settings?.["promptCacheTtl"]]) {
36 const parsed = parseTtl(candidate);
37 if (parsed !== undefined) return parsed;
38 }
39 return 60 * 60 * 1000;
40}
41
42export function isOptedOut(
43 options?: Record<string, unknown>,
44 env: Record<string, string | undefined> = {}
45): boolean {
46 if (options?.["cacheTimer"] === false) return true;
47 const timer = env["TOKEN_SCOPE_CACHE_TIMER"];
48 if (timer !== undefined && timer.trim() !== "" && !isEnvTrue(timer)) return true;
49 if (isEnvTrue(env["TOKEN_SCOPE_DISABLE_CACHE_TIMER"])) return true;
50 return false;
51}
52
53export function isOverageFallback(
54 gap: number,
55 effectiveTtl: number,
56 read: number,
57 write: number
58): boolean {
59 const fiveMinMs = 5 * 60 * 1000;
60 if (gap <= fiveMinMs || gap >= effectiveTtl) return false;
61 return read <= 100 && write >= 1000;
62}
63
64export function label(
65 now: number,
66 lastAt: number | undefined,
67 hit: number | undefined,
68 ttlMs: number | null | undefined = 60 * 60 * 1000,
69 fallback5m: boolean = false
70): string | undefined {
71 if (lastAt === undefined) return undefined;
72 if (ttlMs === null || ttlMs === undefined || ttlMs <= 0) return undefined;
73 const left = ttlMs - (now - lastAt);
74 if (left <= 0) return "cache cold";
75 const s = Math.ceil(left / 1000);
76 const mmss = `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`;
77 const tag = fallback5m ? " (5m)" : "";
78 return hit === undefined ? `cache warm ${mmss}${tag}` : `cache warm ${mmss}${tag} · hit ${hit}%`;
79}
80
81export const register: Register = (on, options) => {
82 const opts = options as Record<string, unknown> | undefined;
83
84 let timer: Timer | undefined;
85 let effectiveTtl = 60 * 60 * 1000;
86 let is5mFallback = false;
87 let lastAt: number | undefined;
88 let hit: number | undefined;
89 let isEnabled = true;
90
91 on("session.start", async ($, e, next) => {
92 timer?.cancel();
93 timer = undefined;
94 lastAt = undefined;
95 hit = undefined;
96 is5mFallback = false;
97
98 if (isOptedOut(opts)) {
99 isEnabled = false;
100 $.ui.status(undefined);
101 return next(e);
102 }
103
104 // String literals, not a loop over names: claude plugin validate scans for them.
105 const reads = await Promise.allSettled([
106 $.env.get("DISABLE_PROMPT_CACHING"),
107 $.env.get("FORCE_PROMPT_CACHING_5M"),
108 $.env.get("CLAUDE_CODE_PROMPT_CACHE_TTL"),
109 $.env.get("TOKEN_SCOPE_DISABLE_CACHE_TIMER"),
110 $.env.get("TOKEN_SCOPE_CACHE_TIMER"),
111 ]);
112 const keys = [
113 "DISABLE_PROMPT_CACHING",
114 "FORCE_PROMPT_CACHING_5M",
115 "CLAUDE_CODE_PROMPT_CACHE_TTL",
116 "TOKEN_SCOPE_DISABLE_CACHE_TIMER",
117 "TOKEN_SCOPE_CACHE_TIMER",
118 ];
119 const env: Record<string, string | undefined> = {};
120 reads.forEach((read, i) => {
121 if (read.status === "fulfilled") env[keys[i]!] = read.value;
122 });
123
124 if (isOptedOut(opts, env)) {
125 isEnabled = false;
126 $.ui.status(undefined);
127 return next(e);
128 }
129
130 let settings: Record<string, unknown> | undefined;
131 try {
132 settings = (await $.settings.read()) as Record<string, unknown>;
133 } catch {
134 // A failed settings read falls back to the env or default TTL rather than disabling the timer.
135 }
136
137 const ttl = resolveTtl(env, settings);
138 if (ttl === null) {
139 isEnabled = false;
140 $.ui.status(undefined);
141 return next(e);
142 }
143
144 isEnabled = true;
145 effectiveTtl = ttl;
146
147 timer = $.clock.every(1000, () => {
148 $.clock
149 .now()
150 .then(now => $.ui.status(label(now, lastAt, hit, effectiveTtl, is5mFallback)))
151 .catch(() => {});
152 });
153
154 return next(e);
155 });
156
157 // /clear ends the conversation without a following session.start. The 5m fallback is
158 // account-level usage overage, so it outlives the conversation.
159 on("session.end", ($, e, next) => {
160 lastAt = undefined;
161 hit = undefined;
162 if (isEnabled) $.ui.status(undefined);
163 return next(e);
164 });
165
166 on("turn.step", async function* ($, e, next) {
167 const r: any = yield* next(e);
168 if (!isEnabled || e.agentId !== undefined || !r?.usage) return r;
169 const u = r.usage;
170 const now = await $.clock.now();
171
172 if (lastAt !== undefined && !is5mFallback && effectiveTtl > 5 * 60 * 1000) {
173 const gap = now - lastAt;
174 const read = u.cache_read_input_tokens ?? 0;
175 const write = u.cache_creation_input_tokens ?? 0;
176 if (isOverageFallback(gap, effectiveTtl, read, write)) {
177 effectiveTtl = 5 * 60 * 1000;
178 is5mFallback = true;
179 }
180 }
181
182 lastAt = now;
183 const total = (u.input_tokens ?? 0) + (u.cache_read_input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0);
184 hit = total > 0 ? Math.round((100 * (u.cache_read_input_tokens ?? 0)) / total) : undefined;
185 return r;
186 });
187};
188