Skip to content

Cache Savings Calculator — Java source

See what prompt caching saves — uncached vs cached cost over N requests, with the write-premium break-even point.

This is the Java implementation — the same logic the interactive tool runs, in a shareable, citable form.

// cache_savings — uncached vs prompt-cached LLM cost comparison.
//
// Language: Java (Java 17, 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 JDK only (boxed Double/Long mirror 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 boxed `Long`.

// Package-private so the single-file launcher can run it despite the
// display filename (`java java.java`).
final class CacheSavings {

    private CacheSavings() {}

    /** The four per-1M-token USD pricing rates {@code cacheMath} reads from
     *  the TS AiModel record. */
    public 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.
    {}

    /** Request shape (TS {@code CacheInput}). */
    public 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.
    {}

    /** Result shape. Any missing rate nulls every field. */
    public 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.
    {}

    /** 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. */
    public static CacheMath cacheMath(ModelRates model, CacheInput input) {
        if (model.inputPerM() == null || model.outputPerM() == null
                || model.cacheReadPerM() == null || model.cacheWritePerM() == null) {
            return nulled();
        }

        double hits = Math.max(1.0, input.hits());
        double inT = input.promptTokens();
        double outT = input.outputTokens();
        double ipm = model.inputPerM();
        double opm = model.outputPerM();
        double cr = model.cacheReadPerM();
        double cw = model.cacheWritePerM();

        // 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.ceil(cw / cr) : null;

        return new CacheMath(uncached, cached, savings, savingsPct, breakEvenHits);
    }

    /** The all-null result used when any pricing rate is missing. */
    private static CacheMath nulled() {
        return new CacheMath(null, null, null, null, null);
    }

    // ------------------------------------------------------------------
    // Self-test — the reference vectors shared with cacheSavings.test.ts
    // (the lock-step contract every port mirrors).
    // ------------------------------------------------------------------

    /** Fixture model F: inputPerM 10, outputPerM 50, cacheReadPerM 1,
     *  cacheWritePerM 12.5. Null one rate to test the unpriced path. */
    private static final ModelRates BASE = new ModelRates(10.0, 50.0, 1.0, 12.5);

    private static CacheInput request(double p, double o, double h) {
        return new CacheInput(p, o, h);
    }

    private static boolean close(double a, double b) {
        return Math.abs(a - b) < 1e-9;
    }

    private static void check(boolean ok, String what) {
        if (!ok) {
            throw new IllegalStateException("cache-savings self-test failed: " + what);
        }
    }

    public static void main(String[] args) {
        // 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 = cacheMath(BASE, request(10_000.0, 1_000.0, 5.0));
        check(close(r.uncached(), 0.75), "uncached " + r.uncached());
        check(close(r.cached(), 0.425), "cached " + r.cached());
        check(close(r.savings(), 0.325), "savings " + r.savings());
        check(close(r.savingsPct(), 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 = cacheMath(BASE, request(10_000.0, 1_000.0, 1.0));
        check(close(r.uncached(), 0.15), "one-hit uncached");
        check(close(r.cached(), 0.185), "one-hit cached");
        check(close(r.savings(), -0.035), "one-hit negative saving");
        check(close(r.savingsPct(), -23.3333333333), "one-hit negative pct");
        check(r.breakEvenHits() == 13, "one-hit breakEven");

        // Each missing rate in turn nulls every field.
        ModelRates[] variants = {
            new ModelRates(null, 50.0, 1.0, 12.5),
            new ModelRates(10.0, null, 1.0, 12.5),
            new ModelRates(10.0, 50.0, null, 12.5),
            new ModelRates(10.0, 50.0, 1.0, null),
        };
        for (ModelRates m : variants) {
            CacheMath x = cacheMath(m, request(10_000.0, 1_000.0, 5.0));
            check(x.uncached() == null && x.cached() == null && x.savings() == null
                    && x.savingsPct() == null && x.breakEvenHits() == null,
                    "missing rate nulls everything");
        }

        // hits < 1 counts as 1.
        check(cacheMath(BASE, request(10_000.0, 1_000.0, 0.0))
                .equals(cacheMath(BASE, request(10_000.0, 1_000.0, 1.0))),
                "hits < 1 clamps to 1");

        // Zero tokens -> zero costs with 0%, no division error.
        r = cacheMath(BASE, request(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 null but costs kept (10k×$12.5 + 5×(0 + 1k×$50)).
        r = cacheMath(new ModelRates(10.0, 50.0, 0.0, 12.5), request(10_000.0, 1_000.0, 5.0));
        check(r.breakEvenHits() == null, "zero read rate nulls breakEven"); // premium never repaid
        check(close(r.uncached(), 0.75), "zero-read uncached");
        check(close(r.cached(), 0.375), "zero-read cached");

        // ceil(4/2) stays 2 — no rounding up at the exact integer boundary.
        r = cacheMath(new ModelRates(10.0, 50.0, 2.0, 4.0), request(1_000.0, 0.0, 3.0));
        check(r.breakEvenHits() == 2, "integer boundary");

        System.out.println("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 →