Skip to content

Cache Savings Calculator — Kotlin source

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

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

// cache_savings — uncached vs prompt-cached LLM cost comparison.
//
// Language: Kotlin (1.9, 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 force-bang on
//     data that can be absent — only on values just proven present).
//   - Functionally equivalent to the TS reference: same inputs -> same outputs.
//   - Self-contained: the Kotlin stdlib only (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?`.

import kotlin.math.abs
import kotlin.math.ceil
import kotlin.math.max

/** The four per-1M-token USD pricing rates [cacheMath] reads from the TS
 *  AiModel record. */
data class ModelRates(
    val inputPerM: Double?,      // Uncached prompt (input) rate, USD per 1M tokens.
    val outputPerM: Double?,     // Completion (output) rate, USD per 1M tokens.
    val cacheReadPerM: Double?,  // Cached prompt read rate, USD per 1M tokens.
    val cacheWritePerM: Double?, // Cache write premium rate, USD per 1M tokens.
)

/** Request shape (TS `CacheInput`). */
data class CacheInput(
    val promptTokens: Double, // Prompt (input) tokens per request.
    val outputTokens: Double, // Completion (output) tokens per request.
    val hits: Double,         // Requests reusing the cached prompt; < 1 counts as 1.
)

/** Result shape. Any missing rate nulls every field. */
data class CacheMath(
    val uncached: Double?,     // hits × (prompt·in$/M + output·out$/M) / 1e6.
    val cached: Double?,       // (prompt·write$/M + hits × (prompt·read$/M + output·out$/M)) / 1e6 —
                               // one cache write, `hits` cache reads, output billed every request.
    val savings: Double?,      // uncached − cached (negative when caching costs more).
    val savingsPct: Double?,   // savings / uncached × 100; 0 when uncached is 0.
    val breakEvenHits: Long?,  // ceil(write$/M / read$/M) when read$/M > 0 — hits needed for
                               // cumulative READ spend to equal ONE write premium; null otherwise.
)

/** The all-null result used when any pricing rate is missing. */
private fun nulled() = CacheMath(null, null, null, null, null)

/** 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. */
fun cacheMath(model: ModelRates, input: CacheInput): CacheMath {
    val ipm = model.inputPerM ?: return nulled()
    val opm = model.outputPerM ?: return nulled()
    val cr = model.cacheReadPerM ?: return nulled()
    val cw = model.cacheWritePerM ?: return nulled()

    val hits = max(1.0, input.hits)
    val inT = input.promptTokens
    val outT = input.outputTokens

    // One cache write, `hits` cache reads; output tokens are billed on every request.
    val uncached = hits * (inT * ipm + outT * opm) / 1_000_000.0
    val cached = (inT * cw + hits * (inT * cr + outT * opm)) / 1_000_000.0
    val savings = uncached - cached
    val savingsPct = if (uncached == 0.0) 0.0 else savings / uncached * 100.0
    val breakEvenHits: Long? = if (cr > 0.0) ceil(cw / cr).toLong() else null

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

// ----------------------------------------------------------------------
// Self-test — the reference vectors shared with cacheSavings.test.ts (the
// lock-step contract every port mirrors).
// ----------------------------------------------------------------------
fun main() {
    fun check(ok: Boolean, what: String) {
        if (!ok) error("cache-savings self-test failed: $what")
    }

    // Fixture model F: inputPerM 10, outputPerM 50, cacheReadPerM 1,
    // cacheWritePerM 12.5. Null one rate (via copy) to test the unpriced path.
    fun base() = ModelRates(10.0, 50.0, 1.0, 12.5)
    fun request(p: Double, o: Double, h: Double) = CacheInput(p, o, h)
    fun close(a: Double, b: Double) = abs(a - b) < 1e-9

    // Spec vector: 10k in / 1k out / 5 hits -> uncached 0.75, cached 0.425,
    // savings 0.325, 43.333...% saved, break-even 13 hits.
    var 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 == 13L, "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 == 13L, "one-hit breakEven")

    // Each missing rate in turn nulls every field.
    val variants = listOf(
        base().copy(inputPerM = null),
        base().copy(outputPerM = null),
        base().copy(cacheReadPerM = null),
        base().copy(cacheWritePerM = null),
    )
    for (m in variants) {
        val 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))
        == 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 == 13L, "zero-tokens breakEven")

    // cacheRead 0 -> break-even null but costs kept (10k×$12.5 + 5×(0 + 1k×$50)).
    r = cacheMath(base().copy(cacheReadPerM = 0.0), 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(base().copy(cacheReadPerM = 2.0, cacheWritePerM = 4.0), request(1_000.0, 0.0, 3.0))
    check(r.breakEvenHits == 2L, "integer boundary")

    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 →