Skip to content

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 →