惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

T
Threatpost
G
Google Developers Blog
Latest news
Latest news
Know Your Adversary
Know Your Adversary
O
OpenAI News
腾讯CDC
月光博客
月光博客
P
Privacy International News Feed
Google Online Security Blog
Google Online Security Blog
Help Net Security
Help Net Security
L
LINUX DO - 最新话题
雷峰网
雷峰网
AI
AI
Hacker News - Newest:
Hacker News - Newest: "LLM"
有赞技术团队
有赞技术团队
N
News and Events Feed by Topic
V
Vulnerabilities – Threatpost
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
D
Docker
Google DeepMind News
Google DeepMind News
T
Tor Project blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Hacker News: Ask HN
Hacker News: Ask HN
爱范儿
爱范儿
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
H
Heimdal Security Blog
I
Intezer
WordPress大学
WordPress大学
C
CERT Recently Published Vulnerability Notes
Attack and Defense Labs
Attack and Defense Labs
www.infosecurity-magazine.com
www.infosecurity-magazine.com
P
Privacy & Cybersecurity Law Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
V
V2EX
博客园 - 三生石上(FineUI控件)
G
GRAHAM CLULEY
Security Archives - TechRepublic
Security Archives - TechRepublic
F
Fortinet All Blogs
L
LangChain Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
Spread Privacy
Spread Privacy
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
V2EX - 技术
V2EX - 技术
Stack Overflow Blog
Stack Overflow Blog
Recent Announcements
Recent Announcements
T
Tenable Blog
Microsoft Azure Blog
Microsoft Azure Blog
V
Visual Studio Blog
SecWiki News
SecWiki News
Cisco Talos Blog
Cisco Talos Blog

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Prompt Cache Break: Hit-Rate Fell 100% to 40% in 40 Lines
Alexey Spinov · 2026-06-21 · via DEV Community

In short: a prompt cache-break is when one change atop your prompt prefix — a fresh timestamp, a reordered tool block — makes the cache miss from there down, so cached reads silently re-bill as fresh input. cache_break.py hashes each prefix segment, localizes the break, and fails CI. On my fixture, one timestamp dropped the estimated cache-hit-rate 100% to 40%.

AI disclosure: I drafted this with an AI writing assistant. The tool, the two fixtures, and every number below come from a real local run on tiktoken o200k_base — I ran it, checked the exit codes, hashed the output twice to confirm it's deterministic, and edited every line before publishing.

Turning on prompt caching feels like a free win. It usually isn't the win you think.

Here's the trap. Anthropic and OpenAI both price a cache read at a fraction of a fresh input token — Anthropic quotes cached reads at roughly $0.30/M vs $3.00/M for fresh, about a 10x gap on the cached portion (Anthropic prompt caching docs). Flip the feature on, watch the dashboard, move on. But that discount is only paid for a prefix that is byte-for-byte identical to what's already cached. Change one character near the top and the cache misses from there down. You still have caching on. You're just not hitting it. And nothing in your config screams about it.

So the number that actually matters isn't "is caching enabled." It's your real cache-hit-rate — and almost nobody meters it.

TL;DR. Prompt caching only pays out on an exact-prefix match. Agents quietly break that match: a dynamic now=... in the system block, two tool definitions that got reordered, a memory snippet inserted at the top. When the prefix breaks, the cache misses below the break and re-bills it as fresh input. cache_break.py (below, keyless, offline) hashes each prefix segment per step, localizes the first one that diverges from the baseline, computes the hit-rate, and gates it. On a clean trace it estimated 100% hit-rate, exit 0. On the same content with one injected timestamp, 40%, cache-break flagged at segment 0 step 3, exit 1.

The contrarian bit: "caching is on" is not "caching is working"

Most caching write-ups stop at how to turn it on. Mark the prefix, set cache_control, done. That's the easy 80%. The expensive 20% is everything that silently invalidates the match afterward, on a running agent, where you'll never notice from the totals.

Three ways an agent breaks its own cache without anyone touching the config:

  • A dynamic value in the system prompt. The classic is a current timestamp — now=2026-06-21T08:14:03Z — stamped into the system block so the model "knows the time." It changes every call. It sits at the very top. So the cache misses on everything, every step, forever.
  • Reordered tool definitions. Your framework serializes tool schemas from a dict or a set. Run two, the order flips, the bytes differ, the prefix no longer matches. Same tools. Broken cache.
  • A memory snippet inserted at the front. Retrieval-augmented memory that prepends "what we learned last session" pushes a variable block above the stable one. Everything below it is now fresh.

The falsifiable claim: if you take a clean trace and a byte-identical-content trace where only the prefix ordering breaks, a real measurement should show the hit-rate collapse on the broken one and stay high on the clean one. If it doesn't collapse, I'm wrong and this tool is useless. It collapsed — 100% to 40% — and the detector named the exact segment. Run is below.

There's even a name for the failure mode in the research now. The arXiv note Don't Break the Cache (2601.06007, Lumer et al., Jan 2026) is entirely about prefix-stability discipline for cached agentic inference — worth a read if you want the formal treatment. The 70% default gate here is my own pick, not theirs: log your cached-token count, compute hits / (hits + full), and alert when it drops under ~70%. That number is a starting line, not a law — tune it to your own traces.

The tool: 40 lines, no API key, read-only

cache_break.py reads one JSONL trace. Each line is one agent step with a prefix: an ordered list of named segments (system, tool_defs, memory) — the part you expect to be cached. It does four deterministic things.

  1. Prefix-stability hash. Canonicalize each segment (json.dumps with sorted keys) and sha256 it. Lock the step-1 prefix as the baseline.
  2. Break-point localization. For every step, compare segment hashes to the baseline. The first segment that diverges is the break point — the cache misses from there down, because everything after a changed byte re-bills as fresh.
  3. Hit-rate compute. Tokens above the break = cached; the break and everything below = fresh. hit-rate = cached / (cached + fresh), summed across steps. tiktoken o200k_base, len/4 fallback if tiktoken's missing.
  4. Gate. Compare overall hit-rate to --min-hit-rate (default 0.70). Below it or any cache-break detected → exit 1. This is a pre-execution gate: don't ship a prompt ordering that punches through your own cache.

One honesty rule baked in: the output says source: estimated from prefix stability. If your trace carried real provider usage fields (Anthropic/OpenAI return cached-token counts), you'd use those instead and it'd say measured. I'm not dressing an estimate up as a meter reading.

#!/usr/bin/env python3
"""cache_break.py - measure prompt cache-hit-rate and localize cache-break in a JSONL trace."""
import json, hashlib, sys

MIN_HIT_RATE = 0.70       # my default gate: alert when cache-hit-rate drops below 70% (tune to your traces)
CACHE_READ = 0.30         # $/1M cached-read tokens  (public price flag, NOT a measurement)
FRESH = 3.00              # $/1M fresh-input tokens  (public price flag, NOT a measurement)

try:
    import tiktoken
    _enc = tiktoken.get_encoding("o200k_base")
    def count(t): return len(_enc.encode(t))
    TOKENIZER = "tiktoken o200k_base (exact)"
except Exception:                                  # honest fallback, ~+-15% vs real BPE
    def count(t): return max(1, round(len(t) / 4))
    TOKENIZER = "len/4 heuristic (tiktoken not installed; ~+-15%)"

def canon(seg): return hashlib.sha256(json.dumps(seg, sort_keys=True, ensure_ascii=False).encode()).hexdigest()[:12]

def main(argv):
    if len(argv) < 2:
        print("usage: cache_break.py <trace.jsonl> [--min-hit-rate 0.70]"); return 2
    min_hit = float(argv[argv.index("--min-hit-rate") + 1]) if "--min-hit-rate" in argv else MIN_HIT_RATE
    try:
        steps = [json.loads(ln) for ln in open(argv[1], encoding="utf-8") if ln.strip()]
        if not steps: raise ValueError("empty trace")
        base = [canon(s) for s in steps[0]["prefix"]]          # baseline prefix from step 1
        names = [s.get("name", f"seg{i}") for i, s in enumerate(steps[0]["prefix"])]
        toks = [count(json.dumps(s.get("text", s), ensure_ascii=False)) for s in steps[0]["prefix"]]
    except (KeyError, ValueError, json.JSONDecodeError, IndexError) as e:
        print(f"cache_break | BAD INPUT: {e}"); return 2

    cached, fresh, break_at, break_step = 0, 0, None, None
    for i, s in enumerate(steps):
        cur = [canon(seg) for seg in s["prefix"]]
        diff = next((j for j in range(min(len(base), len(cur))) if cur[j] != base[j]), None)
        if diff is None and len(cur) != len(base): diff = min(len(base), len(cur))
        if diff is None:                                       # whole prefix byte-identical -> all cached
            cached += sum(toks)
        else:                                                  # cache breaks at first divergence, rest re-bills fresh
            cached += sum(toks[:diff]); fresh += sum(toks[diff:])
            if break_at is None or diff < break_at:
                break_at, break_step = diff, i + 1

    hit = cached / (cached + fresh) if (cached + fresh) else 1.0
    broke = break_at is not None
    eff = CACHE_READ * hit + FRESH * (1 - hit)                 # blended $/1M on the prefix at this hit-rate

    print(f"cache_break | {argv[1]} | tokenizer: {TOKENIZER} | source: estimated from prefix stability")
    print("-" * 78)
    print(f"  steps={len(steps)}  prefix_segments={len(base)}  ({', '.join(names)})")
    print(f"  cached_tokens={cached}  fresh_tokens={fresh}")
    print(f"  cache_hit_rate     : {hit:.2%}   (threshold {min_hit:.0%})")
    if broke:
        print(f"  cache_break        : TRUE at segment {break_at} '{names[break_at] if break_at < len(names) else '?'}', first seen step {break_step}")
    else:
        print(f"  cache_break        : FALSE (prefix byte-identical across all steps)")
    print(f"  effective $/1M prefix : ${eff:.2f}  (vs ${CACHE_READ:.2f} all-cached / ${FRESH:.2f} all-fresh; public-price illustration)")
    rc = 0 if (hit >= min_hit and not broke) else 1
    print(f"  exit               : {rc}  ({'PASS' if rc == 0 else 'FAIL: hit-rate below threshold or cache_break'})")
    return rc

if __name__ == "__main__":
    sys.exit(main(sys.argv))

No key, no network, nothing written to disk. pip install tiktoken, point it at a trace, read the exit code. If tiktoken isn't installed it falls back to len/4 and says so in the header (~±15% off real BPE). I'd rather print the caveat than fake the precision.

The real run

Two fixtures ship with it. Both are synthetic five-step coding sessions (no private data), same payments-svc task, same three-segment prefix: system, tool_defs, memory.

trace_clean.jsonl keeps that prefix byte-identical on every step — only the user tail changes, and the user tail isn't part of the cached prefix. Actual output:

cache_break | fixtures/trace_clean.jsonl | tokenizer: tiktoken o200k_base (exact) | source: estimated from prefix stability
------------------------------------------------------------------------------
  steps=5  prefix_segments=3  (system, tool_defs, memory)
  cached_tokens=335  fresh_tokens=0
  cache_hit_rate     : 100.00%   (threshold 70%)
  cache_break        : FALSE (prefix byte-identical across all steps)
  effective $/1M prefix : $0.30  (vs $0.30 all-cached / $3.00 all-fresh; public-price illustration)
  exit               : 0  (PASS)

100% hit-rate, no break, exit 0. Green. The whole prefix rides the cache every step, effective price sits at the floor — $0.30/M.

trace_broken.jsonl is the exact same content, with one change: starting at step 3, the system segment carries a live clock — now=2026-06-21T08:14:03Z, a different value each step. That's the only edit. Real output:

cache_break | fixtures/trace_broken.jsonl | tokenizer: tiktoken o200k_base (exact) | source: estimated from prefix stability
------------------------------------------------------------------------------
  steps=5  prefix_segments=3  (system, tool_defs, memory)
  cached_tokens=134  fresh_tokens=201
  cache_hit_rate     : 40.00%   (threshold 70%)
  cache_break        : TRUE at segment 0 'system', first seen step 3
  effective $/1M prefix : $1.92  (vs $0.30 all-cached / $3.00 all-fresh; public-price illustration)
  exit               : 1  (FAIL: hit-rate below threshold or cache_break)

40% hit-rate, exit 1. And it points at the culprit precisely: segment 0, system, first seen at step 3. Steps 1–2 cached fine; from step 3 on the timestamp lives at the very top of the prefix, so the cache misses on the system block and on the tool_defs and memory below it — even though those two never changed. That's the cruel part of cache-break: damage at the top voids everything underneath. The blended price on this prefix went from $0.30 to $1.92/M, a 6.4x jump on this fixture, driven by one field a developer added to be helpful.

Watch the two numbers that prove it's the timestamp and nothing else. Same payments-svc content. Same tool list. Same memory. Cached tokens fell 335 → 134; fresh went 0 → 201. The only delta in the input was a clock.

One more run worth showing, because it's the design decision people argue with. A break is a gate condition on its own — not just a low rate. So even if you slacken the threshold all the way to 0.30, the broken trace still fails:

$ python3 cache_break.py fixtures/trace_broken.jsonl --min-hit-rate 0.30
  cache_hit_rate     : 40.00%   (threshold 30%)
  cache_break        : TRUE at segment 0 'system', first seen step 3
  exit               : 1  (FAIL: hit-rate below threshold or cache_break)

40% clears a 30% bar, but the break still trips exit 1. I made that call on purpose: a detected prefix-break means money is leaking somewhere measurable, and "the average is still okay" is exactly the reasoning that lets it leak for a month. Disagree with me on that — it's a real design tradeoff, not a law.

Bad input is the third exit code. A malformed JSONL line returns exit 2, not a crash and not a false pass:

$ python3 cache_break.py fixtures/trace_bad.jsonl
cache_break | BAD INPUT: Expecting ',' delimiter: line 2 column 1 (char 82)
[exit 2]

Both real runs are reproducible. I hashed two consecutive clean runs and two consecutive broken runs with shasum -a 256; each pair was byte-identical (3608e4d5… for clean, fb72c110… for broken). Deterministic, not a one-shot fluke.

6.4x on my run, 10x at the ceiling — the honest version

You'll see "10x" thrown around for cache breaks, so here's where it comes from and what I actually got. The 10x is the unit gap: Anthropic's published cached-read vs fresh-input ratio is roughly $0.30 to $3.00, a 10x difference on tokens that go from cached to fresh (Anthropic docs). That's their public number, not mine.

What the tool computed on this fixture's prefix was 6.4x — $0.30/M up to $1.92/M — because only 60% of the prefix tokens flipped to fresh, not all of them. If a break lands at segment 0 on step 1 of a long-running agent (the dynamic-timestamp case, which is the common one), every prefix token re-bills fresh and you approach the full 10x. So 10x is the ceiling from public prices; 6.4x is the real, smaller, honestly-labeled number from my run. I'd rather you trust the 6.4x I can show you than the 10x I can't.

This is the same shape as the context tax, but a different leak. There, you re-bill the whole transcript every step because it grows — a meter, not a guard. Here the prefix is supposed to be free-ish via cache, and a one-byte change quietly un-frees it. Different axis, different number, same lesson: measure the thing, don't assume the feature did its job.

Where this fits

The prefix this tool watches — the long, stable system + tool definitions + memory block — is the same one I metered for raw cost in your MCP server's token tax. Token-tax asks "how big is this prefix"; cache-break asks "are you actually paying the cached price for it, or silently the fresh one." Run both on the same trace and you've covered both halves: the size of the prefix and whether it's hitting cache.

And it's the same philosophy as the pre-execution gate: catch the broken prompt ordering before you ship it, fail the build, not after the invoice. Cache-break is a perfect CI gate because it's deterministic — same trace, same verdict, every time. If you want the runtime spend brake instead of a structural check, that's the sliding-window spend guard, which caps cumulative cost over a window rather than auditing prefix stability.

What this is NOT (so I don't oversell it)

  • It does not read your provider's real cached-token counts unless you put them in the trace. The hit-rate here is estimated from prefix stability — it assumes the cache misses from the first changed byte down, which is how prefix caching works, but it's a structural model, not a meter reading off your bill. If your trace carries real usage fields, wire those in and trust those instead. The header tells you which mode you're in.
  • It does not compute your invoice. The $/M figures use Anthropic's public $0.30/$3.00 prices as a flag, to illustrate the cached-vs-fresh gap. Your real bill depends on cache TTL, minimum cacheable length, output tokens, and your vendor — none of which this models.
  • It does not account for cache expiry. A prefix can be byte-identical and still miss because the cache entry aged out (Anthropic's default TTL is short). This tool catches the structural break — a changed prefix — not a timed-out one. Those need provider usage data to see.
  • It assumes you've segmented your prefix in the trace (system / tool_defs / memory). Garbage segmentation in, vague break-point out. The localization is only as precise as your segments — a one-character edit inside the system block re-bills the whole system segment as fresh here, even though a real tokenizer would only lose the tokens from that character down. So I say "from the first changed byte down" as the model, but the tool resolves it at the segment boundary, not the byte. That makes it slightly pessimistic on mid-segment edits, never optimistic.
  • It does not charge the cache-write premium. Anthropic bills a cache write at ~1.25x base input, so the very first step (and every step that re-breaks the cache) pays more than the fresh-input number, not less. The "$0.30 all-cached floor" is the steady-state read price, not an achievable per-run average — which means a repeatedly-broken cache costs a bit more than the $1.92 here suggests, not less. I left write-cost out to keep the model simple; it only ever understates the leak.

What's the dumbest thing that's broken your prompt cache — a timestamp, a reordered tool list, a uuid someone logged into the system prompt? Run the detector on a real trace and tell me where it pointed. I'm collecting break points, and I read every reply. Follow for the next number from the next run.