"""
CASE-STUDY-03 report builder (v1.2.0, post-AUDIT-08): measurements_cs03.json ->
engine (untouched cost_model.py) -> CASE-STUDY-03-mooncake.md + calculator states.
The state files are PRIVATE and are not published with the report.

Every dollar and percentage is engine output; measurement-layer statistics are
labelled as such. The 1-hour-tier column re-runs the SAME engine with the vendor
constant w set to that tier's published write multiple, via an in-memory copy of the
library dict — findings_library.json on disk is never modified (rule 10).

Run: python3 build_case_study3.py <findings-dir> [measurements_cs03.json]
"""
from __future__ import annotations

import copy
import json
import pathlib
import sys

HERE = pathlib.Path(__file__).resolve().parent
FINDINGS = (pathlib.Path(sys.argv[1]) if len(sys.argv) > 1 else HERE.parent / "findings").resolve()
sys.path.insert(0, str(FINDINGS))
from cost_model import CostModel, Profile  # noqa: E402

M = json.loads((pathlib.Path(sys.argv[2]) if len(sys.argv) > 2
                else HERE / "measurements_cs03.json").read_text(encoding="utf-8"))
LIB = json.loads((FINDINGS / "findings_library.json").read_text(encoding="utf-8"))
by_id = {f["id"]: f for f in LIB["findings"]}
SELECTION = [f["id"] for f in LIB["findings"] if f["id"] not in ("CA-02", "CA-03")]
R_READ = LIB["meta_cost_model"]["shared_variables"]["r"]["default"]

# B59c: the published checks-count is MEASURED from the suite at build time,
# never typed — build_site and build_www once typed two different stale counts
# (29 and 33) for the same suite. A red or unparseable suite refuses the build:
# a page never asserts a count its own tree cannot demonstrate.
import re as _re
import subprocess as _sp
import sys as _sys
_suite = _sp.run([_sys.executable, "test_mooncake_ingest.py"], cwd=str(HERE),
                 capture_output=True, text=True, encoding="utf-8")
_mm = _re.search(r"mooncake ingest tests: (\d+) passed, (\d+) failed", _suite.stdout or "")
if _suite.returncode != 0 or not _mm or _mm.group(2) != "0":
    raise SystemExit("CHECKS COUNT UNAVAILABLE: test_mooncake_ingest.py did not pass "
                     "cleanly under this build (B59c: a published count is measured, "
                     "never typed, and never published from a red suite)")
CHECKS = int(_mm.group(1))


d2 = lambda x: f"${x:,.2f}"
d0 = lambda x: f"${x:,.0f}"
pc = lambda x, n=1: f"{x * 100:.{n}f}%"


def run(trace: dict, h: float, w: float, r: float = R_READ) -> tuple[dict, dict]:
    """Engine run at a stated (h, w, r). Library copied in memory; disk untouched."""
    lib = copy.deepcopy(LIB)
    lib["meta_cost_model"]["shared_variables"]["w"]["default"] = w
    lib["meta_cost_model"]["shared_variables"]["r"]["default"] = r
    model = CostModel(lib)
    ov = dict(trace["overrides"])
    ov["CA-01.h_target"] = h
    # B62: CA-01 reads C_in_base (library 1.8.0); the base rate comes from the
    # measurements file's OWN pricing block (p_in_per_1m = the card base this study
    # already cites), so the READ-ONLY measurements file is untouched and, with
    # p_in == p_in_base == 2.0 here, every published dollar is unchanged.
    ov["p_in_base"] = M["pricing"]["p_in_per_1m"]
    prof = Profile(**trace["profile"], overrides=ov)
    return model.total_savings(prof, SELECTION), model.shapley(prof, SELECTION)


TIERS = ("5min", "1hour")
runs: dict[str, dict] = {}
for name, t in M["traces"].items():
    runs[name] = {}
    for tier in TIERS:
        ti = t["tiers"][tier]
        for band, hkey in (("refresh", "h_target_refresh"), ("hard", "h_target_hard_expiry")):
            res, cr = run(t, ti[hkey], ti["w_write_multiple"])
            assert not res["ceiling_breached"] and not res["gated_findings"], (name, tier)
            runs[name][(tier, band)] = {"res": res, "credit": cr, "h": ti[hkey]}
    with (HERE / f"calculator_state_{name}.json").open("w", encoding="utf-8", newline="\n") as _f:
        _f.write(json.dumps(
        {"profile": t["profile"], "overrides": dict(t["overrides"]), "selection": None},
        indent=1))

TOOL, CONV = runs["toolagent"], runs["conversation"]
MT, MC = M["traces"]["toolagent"], M["traces"]["conversation"]
base = TOOL[("5min", "refresh")]["res"]

unm = base["unmeasured_findings"]
assert unm == CONV[("5min", "refresh")]["res"]["unmeasured_findings"]
sized = sorted(k for k, v in TOOL[("5min", "refresh")]["credit"].items() if abs(v) > 1e-9)
assert sized == ["AG-03", "CA-01"], sized
enablers = sorted(fid for fid in SELECTION
                  if ((by_id[fid].get("savings_formula") or "0") or "0").strip() == "0"
                  and fid not in base["excluded_findings"])
assert len(SELECTION) == 2 + len(unm) + 1 + len(enablers)
unm_rows = "\n".join(f"| {fid} | {by_id[fid]['title']} | `{'`, `'.join(sorted(v))}` |"
                     for fid, v in sorted(unm.items()))

from cards_cs03 import CARDS, crossover        # the ONE card list (AUDIT-08)


def _hspan(meas, wins):
    """Hit rates at a card's documented window(s), from the retention probe."""
    return [meas["retention_probe"][str(x)] for x in wins]


def _fmt_span(vals, fn):
    out = [fn(v) for v in vals]
    return out[0] if len(set(out)) == 1 else f"{out[0]}–{out[-1]}"


card_rows = []
for c in CARDS:
    be = (c["w"] - 1) / (c["w"] - c["r"])
    hts, hcs = _hspan(MT, c["windows"]), _hspan(MC, c["windows"])
    st = [run(MT, h, c["w"], c["r"])[0]["savings_pct"] for h in hts]
    sc = [run(MC, h, c["w"], c["r"])[0]["savings_pct"] for h in hcs]
    win = "/".join(str(x) for x in c["windows"]) + " s"
    card_rows.append(
        f"| {c['label']} | {c['r']:.2f}× / {c['w']:.2f}× | {be:.3f} | {win} "
        f"| {_fmt_span(hts, lambda v: f'{v:.3f}')} / {_fmt_span(hcs, lambda v: f'{v:.3f}')} "
        f"| **{_fmt_span(st, lambda v: pc(v, 2))}** | **{_fmt_span(sc, lambda v: pc(v, 2))}** "
        f"| {c['note']} |")
card_table = "\n".join(card_rows)

CROSSOVER = crossover(CARDS[0], CARDS[3])          # Anthropic 5-min vs no-premium
CROSS_56 = crossover(CARDS[0], CARDS[2])           # Anthropic 5-min vs GPT-5.6+  → None
# the retention-window result, measured rather than asserted
PROBE_T = MT["retention_probe"]
PROBE_C = MC["retention_probe"]
PT300, PT1800 = PROBE_T["300"], PROBE_T["1800"]
PC300, PC1800 = PROBE_C["300"], PROBE_C["1800"]
WIN_GAIN_T = (PT1800 - PT300) * 100.0        # percentage POINTS, not per cent
WIN_GAIN_C = (PC1800 - PC300) * 100.0
UNTIMED_T = next(p["hit_rate_tokens"] for p in MT["lru_curve_untimed"]
                 if p["capacity_blocks"] == MT["reference_capacity_blocks"])
UNTIMED_C = next(p["hit_rate_tokens"] for p in MC["lru_curve_untimed"]
                 if p["capacity_blocks"] == MC["reference_capacity_blocks"])
assert abs(PROBE_T["3600"] - UNTIMED_T) < 1e-12 and abs(PROBE_C["3600"] - UNTIMED_C) < 1e-12, \
    "saturation claim requires the long-window replay to equal the untimed one"
assert abs(PROBE_T["600"] - PROBE_T["3600"]) < 1e-12, "saturation claim: tool/agent"
assert abs(PROBE_C["600"] - PROBE_C["3600"]) < 1e-12, "saturation claim: conversation"
SAT_T5 = run(MT, PROBE_T["1800"], 1.25, 0.10)[0]["savings_pct"]
CONV_BEST = max(run(MC, MC["retention_probe"][str(x)], c["w"], c["r"])[0]["savings_pct"]
                for c in CARDS for x in c["windows"])


def tier_table(name: str) -> str:
    t = M["traces"][name]
    out = []
    for tier in TIERS:
        ti = t["tiers"][tier]
        rr = runs[name][(tier, "refresh")]["res"]
        rh = runs[name][(tier, "hard")]["res"]
        out.append(
            f"| **{ti['label']}** | {ti['ttl_seconds'] // 60} min | {ti['w_write_multiple']:.2f}× "
            f"| {ti['h_target_refresh']:.4f} | {ti['break_even_h']:.4f} "
            f"| **{pc(rr['savings_pct'], 2)}** | {pc(rh['savings_pct'], 2)} "
            f"| {d0(rr['monthly_savings'])} |")
    return "\n".join(out)


def curve_table(name: str) -> str:
    rows = []
    for p in M["traces"][name]["lru_curve"]:
        cap = ("unbounded" if p["capacity_blocks"] is None
               else f"{p['capacity_blocks']:,} blocks (~{p['capacity_tokens'] // 1_000_000}M tok)")
        rows.append(f"| {cap} | {pc(p['hit_rate_tokens'], 2)} | {pc(p['hit_rate_occurrences'], 2)} |")
    return "\n".join(rows)


T5 = TOOL[("5min", "refresh")]["res"]
T1 = TOOL[("1hour", "refresh")]["res"]
C5 = CONV[("5min", "refresh")]["res"]
C1 = CONV[("1hour", "refresh")]["res"]
RD_T, RD_C = MT["reuse_distance"], MC["reuse_distance"]
h5t = TOOL[("5min", "refresh")]["h"]
h1t = TOOL[("1hour", "refresh")]["h"]
h1c = CONV[("1hour", "refresh")]["h"]

report = f"""# CASE STUDY 03 — The same workload is worth {pc(T5['savings_pct'])} or {pc(T1['savings_pct'])}, depending on which cache tier you bought

Quiet Overrun, operated by Local Good Work LLC · 2026-07-29, revised 2026-07-30 ·
case study v1.2.3 · findings library v{LIB['meta']['version']} · engine `cost_model.py`,
run unmodified. Same pipeline as CASE-STUDY-01: ingest → engine → report, nothing
bespoke.

**The finding.** On one hour of Moonshot AI's Kimi production traffic — the only
public production trace that ships prefix-block hashes — measured prefix reuse is
**{pc(MT['reuse']['B_token_weighted_share'])} of tool/agent input tokens** and
**{pc(MC['reuse']['B_token_weighted_share'])} of conversation input tokens**. What
that reuse is *worth* is not a number. It is a range, and the range is set by a
procurement decision most teams make without measuring:

| Tool/agent workload | 5-minute cache tier | 1-hour cache tier |
|---|---|---|
| Achievable hit rate (LRU replay, same capacity) | {h5t:.4f} | {h1t:.4f} |
| Cache write multiple on that tier | 1.25× | 2.00× |
| Break-even hit rate the tier requires | {MT['tiers']['5min']['break_even_h']:.4f} | {MT['tiers']['1hour']['break_even_h']:.4f} |
| **Savings** | **{pc(T5['savings_pct'], 2)} of spend** | **{pc(T1['savings_pct'], 2)} of spend** |

Same traffic. Same reuse. Same measurement. **{pc(T5['savings_pct'])} or
{pc(T1['savings_pct'])}** — a {T5['savings_pct'] / T1['savings_pct']:.1f}× difference
decided entirely by which retention tier the buyer is on. The longer tier retains more
(hit rate rises from {h5t:.3f} to {h1t:.3f}) but charges 2.00× to write instead of
1.25×, and on this traffic the write premium overwhelms the retention gain.

This is a sensitivity result, not a point estimate. That is the useful form: a reader
who does not know which tier they are on now knows the question to ask, and roughly
what the answer is worth.

## 1. The proof underneath: break-even hit rate

Prompt caching is not free. You pay a premium to write a block and a discount to read
it, so caching only pays when the hit rate clears

`h_break-even = (w − 1) / (w − r)`

where `w` is the cache-write multiple and `r` the cache-read multiple of base input
price. On the cited card (`r` = {R_READ}):

- **5-minute tier** (`w` = 1.25): break-even **{MT['tiers']['5min']['break_even_h']:.4f}**.
  Tool/agent measures **{h5t:.4f}** — comfortably clear.
- **1-hour tier** (`w` = 2.00): break-even **{MT['tiers']['1hour']['break_even_h']:.4f}**.
  Tool/agent measures **{h1t:.4f}** — clear by only
  **{(h1t - MT['tiers']['1hour']['break_even_h']) * 100:.1f} percentage points.**
  Conversation measures **{h1c:.4f}** — it does **not** clear break-even at all, and
  the engine correctly sizes CA-01 at **$0** there, leaving only the duplicate-call
  finding.

That is why the conversational workload's two tiers read **{pc(C5['savings_pct'], 2)}**
and **{pc(C1['savings_pct'], 2)}**: on the extended tier, caching this traffic *loses
money*, and the finding's own falsifier says so before a consultant can talk anyone
into it.

A workload sitting near break-even is the single most valuable thing this measurement
can tell a buyer, and it is invisible without both the hit rate and the tier's write
multiple in the same calculation.

## 2. Both tiers, both expiry semantics, per trace

`refresh` = a cache read extends the entry's life (the vendor's "hits & refreshes"
wording). `hard` = expiry runs from write time regardless of reads — the pessimistic
edge of the band. Everything else is identical.

### tool/agent — {MT['rows']:,} requests, {MT['span_seconds']:.0f}s, mean {MT['mean_in']:.0f} in / {MT['mean_out']:.0f} out tokens

| Tier | TTL | write | h_target | break-even | savings (refresh) | savings (hard) | $/1M requests |
|---|---|---|---|---|---|---|---|
{tier_table('toolagent')}

### conversation — {MC['rows']:,} requests, {MC['span_seconds']:.0f}s, mean {MC['mean_in']:.0f} in / {MC['mean_out']:.0f} out tokens

| Tier | TTL | write | h_target | break-even | savings (refresh) | savings (hard) | $/1M requests |
|---|---|---|---|---|---|---|---|
{tier_table('conversation')}

Priced cost before any finding: **{d2(T5['monthly_spend_now'])} per 1M tool/agent
requests**, **{d2(C5['monthly_spend_now'])} per 1M conversation requests** (cited
card, this token mix).

## 3. Why the tier matters so much here: reuse distance

A 5-minute cache cannot serve a prefix last seen 40 minutes ago. How much reuse falls
outside the window is a property of the workload, and the two traces differ sharply:

| | tool/agent | conversation |
|---|---|---|
| Reuse events | {RD_T['reuse_events']:,} | {RD_C['reuse_events']:,} |
| Median gap between reuses | **{RD_T['median_gap_s']:.0f} s** | **{RD_C['median_gap_s']:.0f} s** |
| 90th percentile gap | {RD_T['p90_gap_s']:.0f} s | {RD_C['p90_gap_s']:.0f} s |
| Share of reuse beyond 5 minutes | **{pc(RD_T['share_beyond_5min'])}** | **{pc(RD_C['share_beyond_5min'])}** |

Agentic traffic reuses its prefix *immediately* — median gap {RD_T['median_gap_s']:.0f}
seconds — so a short cache captures nearly all of it. Conversational traffic reuses
across minutes, so {pc(RD_C['share_beyond_5min'])} of its reuse falls outside a
5-minute window. **This is why agent workloads are the natural caching target and chat
workloads are not**, and it is measurable from timestamps alone.

## 4. The reuse measurement — three definitions, and the one that matters

- **A — block-occurrence share**: reused block occurrences ÷ all block occurrences.
  `A = Σ_r Σ_i [hash seen before] / Σ_r |hash_ids_r|`
- **B — token-weighted share**: input tokens in previously-seen blocks ÷ all input
  tokens, where block *i* carries `min(512, input_length − 512·i)` tokens.
- **C — contiguous-prefix share**: as B, but a block counts only if **every block
  before it in the same request also hit**. This is what a real prefix cache can
  serve: KV state is positional, so a match at position 5 is worthless if position 4
  missed.

| Trace | A | **B** | **C (contiguity enforced)** | Paper (reported) |
|---|---|---|---|---|
| tool/agent | {pc(MT['reuse']['A_block_occurrence_share'], 2)} | **{pc(MT['reuse']['B_token_weighted_share'], 2)}** | **{pc(MT['reuse']['C_contiguous_token_share'], 2)}** | ~59% |
| conversation | {pc(MC['reuse']['A_block_occurrence_share'], 2)} | **{pc(MC['reuse']['B_token_weighted_share'], 2)}** | **{pc(MC['reuse']['C_contiguous_token_share'], 2)}** | ~40% |

### C ≡ B exactly, and here is why

Enforcing contiguity changes nothing: **C equals B to floating-point precision on both
traces.** The reason is checkable in one pass of the ingest — **zero hits occur after a
miss within the same request**: {MT['reuse']['hits_after_miss']:,} of
{MT['reuse']['total_hits']:,} hits (tool/agent) and {MC['reuse']['hits_after_miss']:,}
of {MC['reuse']['total_hits']:,} (conversation). The released `hash_ids` are cumulative
prefix hashes, so any reuse is by construction a clean prefix.

That matters because it removes the strongest objection to this method. Block-
independent counting *could* over-credit a cache by counting mid-request matches no
positional cache could serve. On this data it provably does not. We publish B because
it is what a cache prices on, and C because it is what a cache can serve — and they
are the same number.

**Against the paper's reported figures** (~59% / ~40%): B closes most of the gap that
occurrence-counting leaves open. The residual ~2 points is **not resolved from the
released artifact** — the paper measured its production system over data we cannot
access; the release is a one-hour sample. We publish what the committed script computes
on pinned bytes and do not adjust toward the reported number.

## 5. Cache pricing is not one number — the card changes the answer

Each row pairs a card with the hit rate measured at **its own documented retention
window**, taken from the ingest's retention probe — pairing a 1-hour hit rate with a
5-minute write price is precisely the error this study was corrected for in v1.1.0. The
OpenAI rows are split by model generation because **both** the write price and the
window changed at GPT-5.6. Every multiple and window below was re-read from the
vendor's own page on 2026-07-29 (v1.2.0 — the previous OpenAI row was wrong; see
changelog).

| Cache card | read / write multiple | break-even h | window | h (tool / conv) | tool/agent savings | conversation savings | note |
|---|---|---|---|---|---|---|---|
{card_table}

Three things a reader should take from this table:

1. **The reuse ratios and hit-rate curves are card-independent. The savings percentage
   is not.** Conversation swings from {pc(C1['savings_pct'], 2)} to {pc(CONV_BEST, 2)}
   of spend across published cards on identical traffic. Any vendor quoting a single
   "caching saves X%" figure without naming a card *and* a generation is quoting nothing.

2. **The cited card is the conservative choice, not a flattering one.** Against a
   no-premium card at the same 0.10× read, Anthropic's 5-minute tier is worse for every
   hit rate below **h = {CROSSOVER:.3f}** — that is, at every hit rate short of a
   perfect cache. As a share of the input-token bill:

   ```
   Anthropic 5-minute   1 − (0.10h + 1.25(1−h))  =  1.15h − 0.25
   no write premium     1 − (0.10h + 1.00(1−h))  =  0.90h
   equal when           1.15h − 0.25 = 0.90h  →  0.25h = 0.25  →  h = 1
   ```

   So pricing this study on OpenAI's pre-5.6 card would have produced a **larger**
   savings figure at every hit rate we measured. We kept the smaller one.

   v1.1.0 published a different crossover here, because it fed this same equation a
   misread OpenAI card. The arithmetic was never wrong; the card was. The superseded
   figures are in the changelog and deliberately not restated here — a live document
   that repeats its own retired numbers is a document a reader can quote back wrongly.
   The build now derives the crossover from the card list in closed form and asserts
   that the closed form still reproduces the old published value when fed the old card,
   so the correction is provably to the data and not to the mathematics.

3. **Identical multiples, different window — and the window saturates.** The current
   GPT-5.6+ card is 0.10× / 1.25×: the *same line* as Anthropic's 5-minute tier, same
   break-even, no crossover to compute. The only difference is retention — "at least 30
   minutes" against five. That is worth **+{WIN_GAIN_T:.1f} points of hit rate** on the
   tool/agent trace ({PT300:.1%} → {PT1800:.1%}) and
   **+{WIN_GAIN_C:.1f} points** on the conversation trace
   ({PC300:.1%} → {PC1800:.1%}), lifting tool/agent savings from
   {pc(T5['savings_pct'], 2)} to {pc(SAT_T5, 2)} of spend.

   **And no more than that.** The measured hit rate is identical at 600 s, 1,800 s and
   3,600 s — to the last digit, on both traces — because from ten minutes upward the
   replay is *exactly* the untimed, capacity-only replay at the stated 32,000-block
   reference capacity ({UNTIMED_T:.4f} tool/agent, {UNTIMED_C:.4f} conversation;
   asserted in the build, not eyeballed). Past ten minutes this cache is capacity-bound,
   not time-bound. Buying a longer window buys nothing here; buying more capacity might.
   That is the opposite of the intuition that a longer TTL is strictly better value, and
   it is only visible because the window and the price were separated.

**On DeepSeek.** v1.1.0 carried a "DeepSeek-style" row at 0.10× / 1.00×. With OpenAI's
read multiple corrected to 0.10×, that row became numerically identical to the pre-5.6
OpenAI row, so it has been removed rather than double-counted. DeepSeek's own published
card is in fact more aggressive than either vendor here — cache-hit input at 0.02× of
cache-miss input on V4-Flash and 0.0083× on V4-Pro, so the read multiple is not even
constant across their own card — but their pricing page documents **no** cache-write
charge at all. An absence of a documented charge is not a documented zero, and this
study will not price a card on an absence.

## 6. Unit economics only — a deliberate constraint

Both traces span {MT['span_seconds'] / 60:.1f} minutes. We report **cost per request,
cost per 1M requests, and savings as a percentage of the priced window — never a monthly
or annual figure.** Scaling one hour to a month multiplies by ~720 and assumes flat
demand; our own CASE-STUDY-01 measured 121 days of real traffic whose daily demand
ranged from 0.7% of median to 11× median. A monthly number here would contradict the
published finding sitting next to it.

The engine's canonical window is monthly, so the standard renderer prints "/mo" labels.
With N set to 1,000,000 requests as the unit, every such figure reads exactly "$ per 1M
requests at this mix." No annualised figure appears in this artifact or its screenshots.

## 7. The two findings that sized, and where they overlap

**CA-01 (prompt caching)** and **AG-03 (duplicate calls)** are the only findings this
schema can size. Their engine credits at the default tier
({MT['tiers']['5min']['label']}, refresh):

| | tool/agent | conversation |
|---|---|---|
| CA-01 | {d2(TOOL[('5min', 'refresh')]['credit']['CA-01'])} | {d2(CONV[('5min', 'refresh')]['credit']['CA-01'])} |
| AG-03 | {d2(TOOL[('5min', 'refresh')]['credit']['AG-03'])} | {d2(CONV[('5min', 'refresh')]['credit']['AG-03'])} |
| Composed total | **{d2(T5['monthly_savings'])}** | **{d2(C5['monthly_savings'])}** |

**Disclosed overlap.** Input tokens belonging to exact duplicate requests
(**{pc(MT['duplicates']['input_token_share'], 2)}** of tool/agent input tokens,
**{pc(MC['duplicates']['input_token_share'], 2)}** of conversation) are counted *both* inside
CA-01's `f_prefix` (they are reused blocks) and inside AG-03's `pct_duplicate_calls`.
The two findings sit in different overlap groups (`input_price` vs `call_volume`), so
the engine's partition panel does not flag them. Multiplicative composition prevents
additive inflation — the composed total is below the naive sum — and AG-03 is only
{TOOL[('5min', 'refresh')]['credit']['AG-03'] / T5['monthly_savings'] * 100:.1f}% of the
tool/agent result. We disclose it rather than leave a reader to find it.

## 8. Everything the audit declined to size — engine output, verbatim

{len(unm)} of {len(SELECTION)} selected findings held at $0 by the measurement invariant, each naming
the variables it would need. Accounting: {len(SELECTION)} = 2 sized + {len(unm)} unmeasured + 1
excluded (RS-02) + {len(enablers)} enablers ({', '.join(enablers)}).

| Finding | Title | Missing variables |
|---|---|---|
{unm_rows}

## 9. LRU capacity curves (at the default 5-minute tier)

**tool/agent**

| Cache capacity | Hit rate (tokens) | Hit rate (block occurrences) |
|---|---|---|
{curve_table('toolagent')}

**conversation**

| Cache capacity | Hit rate (tokens) | Hit rate (block occurrences) |
|---|---|---|
{curve_table('conversation')}

Capacity is not the binding constraint here: {MT['reference_capacity_blocks']:,} blocks
= {MT['reference_capacity_blocks'] * 512 / 1e6:.1f}M tokens ≈ 2–5 GB of KV cache for
GQA models of the relevant class — comfortably a single node. **Retention, not
capacity, is what sets the answer on this traffic**, which is the opposite of the usual
assumption.

## 10. What one hour of trace cannot show

- **No demand shape.** Commitment and utilisation findings (IN-09, IN-01/02, IN-10's
  time signatures) need weeks of demand curve. CASE-STUDY-01 (121 days) is the
  complement that measures exactly that family.
- **No sessions.** Agent-loop findings (AG-01/AG-02) need request grouping the release
  does not carry.
- **No models, latency classes, quality data, or infrastructure** — the MS-*, RS-01,
  QR-*, IN-* families stay unmeasured, named in section 8.
- **No billing.** Dollars are modeled from tokens at the cited card, never observed.
- **One hour is not a TTL experiment.** A 1-hour trace cannot distinguish a 1-hour cache
  from an unbounded one, so the 1-hour column is a *lower bound* on that tier's
  retention benefit — which makes the tier gap we report conservative.

## Methodology note

**Provenance.** {M['provenance']['source']}. Pinned SHA-256 (the ingest refuses
non-matching bytes):
{chr(10).join(f"- `{f}` — {M['provenance']['files'][f]['rows']:,} rows — `{M['provenance']['files'][f]['sha256']}`" for f in M['provenance']['files'])}

**Excluded file:** {M['provenance']['excluded']}.

**Block semantics.** `hash_ids` are remapped 512-token prefix-block hashes; the final
block of a request may be partial (`input_length ≤ 512 × len(hash_ids)` is validated on
every row). Timestamps are milliseconds; both spans verified ≈{MT['span_seconds'] / 60:.1f} min.

**Cache replay.** Block-granular LRU, replayed in trace order, with **a stated TTL and a
stated capacity**, touch-on-hit, insert-on-miss, lazy expiry. Each tier is priced at the
write multiple that tier actually costs on the cited card — the 5-minute replay at
1.25×, the 1-hour replay at 2.00×. An untimed replay on a 59-minute trace *is* a 1-hour
replay; pricing that at the 5-minute multiple describes a configuration no card sells,
and v1.0.0 of this case study did exactly that (corrected in v1.1.0 — see changelog).
Real schedulers differ in admission policy and eviction; the capacity curve and both
expiry semantics are published so any operating point can be read off. The recipe is
unit-tested against hand-derived fixtures including reuse just inside and just outside
the window (`test_mooncake_ingest.py`, {CHECKS} checks).

**Pricing (stated, cited, substituted openly).** Kimi is a Chinese provider; its
collection-era rate card is not verifiable. We price at **Anthropic's published card as
of 2026-07-29, re-read 2026-08-25** (platform.claude.com): Claude Sonnet 5 at $2/1M
input, $10/1M output — announced as introductory pricing through 31 August 2026 and
now the standard rate; the scheduled increase to $3/$15 will not occur, so the
unit-cost figures in §4 do not expire. Cache read 0.1×; cache write 1.25× (5-minute)
or 2.00× (1-hour) — the same r/w constants the findings library carries as
vendor-verified.
**Moonshot's own card would scale the dollar figures; it would not change the reuse
ratios or the hit-rate curves.** It *would* change the savings percentages, as section 5
shows — which is why that section exists. The 1-hour column re-runs the same engine with
`w` set to 2.00 in an in-memory copy of the library; `findings_library.json` on disk is
unmodified.

**Reproduce the measurements.** Every step below runs on files published with this
page, plus the public traces.
```
git clone --depth 1 --filter=blob:none --sparse https://github.com/kvcache-ai/Mooncake
cd Mooncake && git sparse-checkout set FAST25-release/traces
python3 mooncake_ingest.py FAST25-release/traces my_measurements.json
python3 test_mooncake_ingest.py
diff my_measurements.json measurements_cs03.json     # expect no output
```
The ingest aborts on any trace whose SHA-256 differs from the pinned hashes above, so a
clean diff verifies every measured quantity on this page: both reuse definitions, the
TTL-aware replay, the retention probe, the capacity curve and the duplicate shares.

**What you cannot reproduce from this page, stated plainly.** Converting those
measurements into savings percentages runs on `cost_model.py` and the findings library.
Neither is published — they are the audit product, not a download. `build_case_study3.py`
and `cards_cs03.py` ARE published, so the card list, the crossover closed form and its
assertions can be read and checked directly. And the headline arithmetic does not need
the engine: CA-01's formula is printed above, so
`C_in · f_prefix · max(0, 1 − (h·r + (1−h)·w))` can be evaluated by hand from the
measurements file for any row of the card table. The engine's contribution beyond that is
multiplicative composition across findings, which moves the plan total and not the
single-finding figures this study leads on.

---
*Sources, every vendor figure re-read from the vendor's own live page on 2026-07-29
(v1.2.0 — the previous OpenAI row was not): Mooncake traces & paper —
github.com/kvcache-ai/Mooncake, Qin et al., arXiv 2407.00079 (FAST'25), Apache-2.0.
**Anthropic** — platform.claude.com prompt-caching documentation: "5-minute cache write
tokens are 1.25 times the base input tokens price", "1-hour cache write tokens are 2
times the base input tokens price", "Cache read tokens are 0.1 times the base input
tokens price", "The cache is refreshed for no additional cost each time the cached
content is used"; Claude Sonnet 5 at $2/$10 per MTok — announced as introductory
pricing through 2026-08-31 and confirmed the standard rate on 2026-08-25, with the
scheduled $3/$15 increase cancelled, so the unit-cost figures in §4 do not expire.
**OpenAI** —
developers.openai.com prompt-caching guide: "Cache writes have no additional fee on
models before the GPT-5.6 family", "On GPT-5.6 models and later model families, cache
writes cost 1.25× the uncached input token rate", "A cached prefix remains eligible for
reuse for at least 30 minutes", "cached prefixes generally remain active for 5 to 10
minutes of inactivity, up to a maximum of one hour". The 0.10× read multiple is taken
from the cached-input-to-input **ratio within each row** of the developers.openai.com
pricing table rather than from its absolute prices, because that page carries Standard,
Batch, Flex and Priority tiers; the ratio is invariant to which tier a reader is billed
on, the absolute prices are not, and this study never needs them. **DeepSeek** —
api-docs.deepseek.com pricing page (read multiples only; no cache-write charge is
documented, so no DeepSeek row is priced). CASE-STUDY-01 (this repo) for the
demand-shape constraint.*
"""

with (HERE / "CASE-STUDY-03-mooncake.md").open("w", encoding="utf-8", newline="\n") as _f:
    _f.write(report)
print(f"report v1.2.3: tool 5min={pc(T5['savings_pct'], 2)} 1hour={pc(T1['savings_pct'], 2)} | "
      f"conv 5min={pc(C5['savings_pct'], 2)} 1hour={pc(C1['savings_pct'], 2)} | unmeasured={len(unm)}")
