Cache Savings Calculator — C# source
See what prompt caching saves — uncached vs cached cost over N requests, with the write-premium break-even point.
This is the C# implementation — the same logic the interactive tool runs, in a shareable, citable form.
// cache_savings — uncached vs prompt-cached LLM cost comparison.
//
// Language: C# (C# 12 / .NET 8, standard library only)
// Source: CosmoDev polyglot showcase port of the Cache Savings Calculator
// tool, ported from src/lib/cacheSavings.ts (the canonical
// TypeScript implementation).
// Tool: https://dev.cosmolabs.org/tools/cache-savings-calculator
// License: display source — part of CosmoDev's polyglot tool pages.
//
// Design goals:
// - Pure + deterministic; never throws (plain double math, no unchecked
// division — the only division guards uncached == 0).
// - Functionally equivalent to the TS reference: same inputs -> same outputs.
// - Self-contained: the BCL only (nullable double mirrors the TS `null`).
//
// The TS original takes a full AiModel record but reads only its four pricing
// rates, so this port narrows the parameter to exactly those fields. Any null
// rate makes every output null — the caller renders an explanatory empty
// state instead of partial math. All rates are per-1M-token USD, mirroring
// the cost conventions of llmCost.ts.
//
// Numeric mapping: TS `number` is a double, so tokens and hits stay `double`
// (fractional hits clamp up to 1.0 exactly like `Math.max(1, hits)`);
// breakEvenHits is a whole hit count, so it lands in `long?`.
using System;
namespace CosmoDev.CacheSavings;
/// <summary>
/// The four per-1M-token USD pricing rates <see cref="CacheSavings.Compute"/>
/// reads from the TS AiModel record.
/// </summary>
public sealed record ModelRates(
double? InputPerM, // Uncached prompt (input) rate, USD per 1M tokens.
double? OutputPerM, // Completion (output) rate, USD per 1M tokens.
double? CacheReadPerM, // Cached prompt read rate, USD per 1M tokens.
double? CacheWritePerM); // Cache write premium rate, USD per 1M tokens.
/// <summary>Request shape (TS <c>CacheInput</c>).</summary>
public sealed record CacheInput(
double PromptTokens, // Prompt (input) tokens per request.
double OutputTokens, // Completion (output) tokens per request.
double Hits); // Requests reusing the cached prompt; < 1 counts as 1.
/// <summary>Result shape. Any missing rate nulls every field.</summary>
public sealed record CacheMath(
double? Uncached, // Hits × (prompt·in$/M + output·out$/M) / 1e6.
double? Cached, // (prompt·write$/M + hits × (prompt·read$/M + output·out$/M)) / 1e6 —
// one cache write, `hits` cache reads, output billed every request.
double? Savings, // Uncached − cached (negative when caching costs more).
double? SavingsPct, // Savings / uncached × 100; 0 when uncached is 0.
long? BreakEvenHits); // Ceil(write$/M / read$/M) when read$/M > 0 — hits needed for
// cumulative READ spend to equal ONE write premium; null otherwise.
public static class CacheSavings
{
/// <summary>
/// Compare uncached vs prompt-cached cost for one model. Any missing rate
/// (input, output, cache read, cache write) nulls every field — the
/// caller renders an explanatory empty state instead of partial math.
/// </summary>
public static CacheMath Compute(ModelRates model, CacheInput input)
{
if (model.InputPerM is null || model.OutputPerM is null
|| model.CacheReadPerM is null || model.CacheWritePerM is null)
{
return Nulled();
}
double hits = Math.Max(1.0, input.Hits);
double inT = input.PromptTokens;
double outT = input.OutputTokens;
double ipm = model.InputPerM.Value;
double opm = model.OutputPerM.Value;
double cr = model.CacheReadPerM.Value;
double cw = model.CacheWritePerM.Value;
// One cache write, `hits` cache reads; output tokens are billed on every request.
double uncached = hits * (inT * ipm + outT * opm) / 1_000_000.0;
double cached = (inT * cw + hits * (inT * cr + outT * opm)) / 1_000_000.0;
double savings = uncached - cached;
double savingsPct = uncached == 0.0 ? 0.0 : savings / uncached * 100.0;
long? breakEvenHits = cr > 0.0 ? (long)Math.Ceiling(cw / cr) : null;
return new CacheMath(uncached, cached, savings, savingsPct, breakEvenHits);
}
/// <summary>The all-null result used when any pricing rate is missing.</summary>
private static CacheMath Nulled() => new(null, null, null, null, null);
}
// ----------------------------------------------------------------------
// Self-test — the reference vectors shared with cacheSavings.test.ts (the
// lock-step contract every port mirrors).
// ----------------------------------------------------------------------
internal static class Program
{
// Fixture model F: InputPerM 10, OutputPerM 50, CacheReadPerM 1,
// CacheWritePerM 12.5. Null one rate (via `with`) to test the unpriced path.
private static ModelRates BaseRates() => new(10.0, 50.0, 1.0, 12.5);
private static CacheInput Input(double p, double o, double h) => new(p, o, h);
private static bool Close(double a, double b) => Math.Abs(a - b) < 1e-9;
private static void Check(bool ok, string what)
{
if (!ok)
{
throw new InvalidOperationException($"cache-savings self-test failed: {what}");
}
}
public static void Main()
{
// Spec vector: 10k in / 1k out / 5 hits -> uncached 0.75, cached 0.425,
// savings 0.325, 43.333...% saved, break-even 13 hits.
CacheMath r = CacheSavings.Compute(BaseRates(), Input(10_000.0, 1_000.0, 5.0));
Check(Close(r.Uncached!.Value, 0.75), $"uncached {r.Uncached}");
Check(Close(r.Cached!.Value, 0.425), $"cached {r.Cached}");
Check(Close(r.Savings!.Value, 0.325), $"savings {r.Savings}");
Check(Close(r.SavingsPct!.Value, 43.3333333333), $"savingsPct {r.SavingsPct}");
Check(r.BreakEvenHits == 13, $"breakEven {r.BreakEvenHits}"); // ceil(12.5 / 1)
// At 1 hit caching LOSES 0.035 — an honest negative saving.
r = CacheSavings.Compute(BaseRates(), Input(10_000.0, 1_000.0, 1.0));
Check(Close(r.Uncached!.Value, 0.15), "one-hit uncached");
Check(Close(r.Cached!.Value, 0.185), "one-hit cached");
Check(Close(r.Savings!.Value, -0.035), "one-hit negative saving");
Check(Close(r.SavingsPct!.Value, -23.3333333333), "one-hit negative pct");
Check(r.BreakEvenHits == 13, "one-hit breakEven");
// Each missing rate in turn nulls every field.
ModelRates[] variants =
{
BaseRates() with { InputPerM = null },
BaseRates() with { OutputPerM = null },
BaseRates() with { CacheReadPerM = null },
BaseRates() with { CacheWritePerM = null },
};
foreach (ModelRates m in variants)
{
CacheMath x = CacheSavings.Compute(m, Input(10_000.0, 1_000.0, 5.0));
Check(x.Uncached is null && x.Cached is null && x.Savings is null
&& x.SavingsPct is null && x.BreakEvenHits is null,
"missing rate nulls everything");
}
// hits < 1 counts as 1.
Check(CacheSavings.Compute(BaseRates(), Input(10_000.0, 1_000.0, 0.0))
== CacheSavings.Compute(BaseRates(), Input(10_000.0, 1_000.0, 1.0)),
"hits < 1 clamps to 1");
// Zero tokens -> zero costs with 0%, no division error.
r = CacheSavings.Compute(BaseRates(), Input(0.0, 0.0, 5.0));
Check(r.Uncached == 0.0 && r.Cached == 0.0 && r.Savings == 0.0 && r.SavingsPct == 0.0,
"zero tokens");
Check(r.BreakEvenHits == 13, "zero-tokens breakEven");
// CacheRead 0 -> break-even absent but costs kept (10k×$12.5 + 5×(0 + 1k×$50)).
r = CacheSavings.Compute(BaseRates() with { CacheReadPerM = 0.0 }, Input(10_000.0, 1_000.0, 5.0));
Check(r.BreakEvenHits is null, "zero read rate nulls breakEven"); // write premium never repaid
Check(Close(r.Uncached!.Value, 0.75), "zero-read uncached");
Check(Close(r.Cached!.Value, 0.375), "zero-read cached");
// Ceil(4/2) stays 2 — no rounding up at the exact integer boundary.
r = CacheSavings.Compute(
BaseRates() with { CacheReadPerM = 2.0, CacheWritePerM = 4.0 },
Input(1_000.0, 0.0, 3.0));
Check(r.BreakEvenHits == 2, "integer boundary");
Console.WriteLine("ok");
}
}
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →