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 (C11, 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 traps (plain double math, no allocation,
 *     no division that can fail — the only division guards uncached == 0).
 *   - Functionally equivalent to the TS reference: same inputs -> same outputs.
 *   - Self-contained: stdlib only (libm for fmax/ceil).
 *
 * 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. TS
 * signals a missing rate with `null`; C has no nullable double, so every
 * optional number is an `md` ("maybe double") {has, value} pair. Any missing
 * rate makes every output absent — 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 long` behind its
 * own presence flag.
 */

#include <assert.h>
#include <math.h>
#include <stdio.h>

/* A maybe-double: `has` == 0 mirrors the TS `null` (absent rate). */
typedef struct {
    int has;
    double value;
} md;

static md md_none(void)
{
    md r;
    r.has = 0;
    r.value = 0.0;
    return r;
}

static md md_some(double v)
{
    md r;
    r.has = 1;
    r.value = v;
    return r;
}

/* The four per-1M-token USD pricing rates cache_math reads from the TS
 * AiModel record. */
typedef struct {
    md input_per_m;       /* Uncached prompt (input) rate. */
    md output_per_m;      /* Completion (output) rate. */
    md cache_read_per_m;  /* Cached prompt read rate. */
    md cache_write_per_m; /* Cache write premium rate. */
} model_rates;

/* Request shape (TS CacheInput). */
typedef struct {
    double prompt_tokens; /* Prompt (input) tokens per request. */
    double output_tokens; /* Completion (output) tokens per request. */
    double hits;          /* Requests reusing the cached prompt; < 1 counts as 1. */
} cache_input;

/* Result shape. Any missing rate makes every field absent. */
typedef struct {
    md uncached;         /* hits × (prompt·in$/M + output·out$/M) / 1e6. */
    md cached;           /* (prompt·write$/M + hits × (prompt·read$/M + output·out$/M)) / 1e6 —
                            one cache write, `hits` cache reads, output billed every request. */
    md savings;          /* uncached − cached (negative when caching costs more). */
    md savings_pct;      /* savings / uncached × 100; 0 when uncached is 0. */
    int has_break_even;  /* ceil(write$/M / read$/M) when read$/M > 0 — hits needed for
                            cumulative READ spend to equal ONE write premium; else absent. */
    long long break_even_hits;
} cache_math_result;

/* The all-absent result used when any pricing rate is missing. */
static cache_math_result nulled(void)
{
    cache_math_result r;
    r.uncached = md_none();
    r.cached = md_none();
    r.savings = md_none();
    r.savings_pct = md_none();
    r.has_break_even = 0;
    r.break_even_hits = 0;
    return r;
}

/* Compare uncached vs prompt-cached cost for one model. Any missing rate
 * (input, output, cache read, cache write) makes every output absent — the
 * caller renders an explanatory empty state instead of partial math. */
cache_math_result cache_math(model_rates model, cache_input in)
{
    if (!model.input_per_m.has || !model.output_per_m.has ||
        !model.cache_read_per_m.has || !model.cache_write_per_m.has) {
        return nulled();
    }

    double hits = fmax(1.0, in.hits);
    double in_t = in.prompt_tokens;
    double out_t = in.output_tokens;
    double ipm = model.input_per_m.value;
    double opm = model.output_per_m.value;
    double cr = model.cache_read_per_m.value;
    double cw = model.cache_write_per_m.value;

    /* One cache write, `hits` cache reads; output tokens are billed on every request. */
    double uncached = hits * (in_t * ipm + out_t * opm) / 1000000.0;
    double cached = (in_t * cw + hits * (in_t * cr + out_t * opm)) / 1000000.0;
    double savings = uncached - cached;
    double savings_pct = uncached == 0.0 ? 0.0 : savings / uncached * 100.0;

    cache_math_result r;
    r.uncached = md_some(uncached);
    r.cached = md_some(cached);
    r.savings = md_some(savings);
    r.savings_pct = md_some(savings_pct);
    if (cr > 0.0) {
        r.has_break_even = 1;
        r.break_even_hits = (long long)ceil(cw / cr);
    } else {
        r.has_break_even = 0;
        r.break_even_hits = 0;
    }
    return r;
}

/* ------------------------------------------------------------------ */
/* 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. */
static model_rates base_rates(void)
{
    model_rates m;
    m.input_per_m = md_some(10.0);
    m.output_per_m = md_some(50.0);
    m.cache_read_per_m = md_some(1.0);
    m.cache_write_per_m = md_some(12.5);
    return m;
}

static cache_input mk_input(double p, double o, double h)
{
    cache_input i = { p, o, h };
    return i;
}

static int close(double a, double b)
{
    return fabs(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. */
static void test_spec_vector(void)
{
    cache_math_result r = cache_math(base_rates(), mk_input(10000.0, 1000.0, 5.0));
    assert(r.uncached.has && close(r.uncached.value, 0.75));
    assert(r.cached.has && close(r.cached.value, 0.425));
    assert(r.savings.has && close(r.savings.value, 0.325));
    assert(r.savings_pct.has && close(r.savings_pct.value, 43.3333333333));
    assert(r.has_break_even && r.break_even_hits == 13); /* ceil(12.5 / 1) */
}

/* At 1 hit caching LOSES 0.035 — an honest negative saving. */
static void test_negative_saving_at_one_hit(void)
{
    cache_math_result r = cache_math(base_rates(), mk_input(10000.0, 1000.0, 1.0));
    assert(close(r.uncached.value, 0.15));
    assert(close(r.cached.value, 0.185));
    assert(close(r.savings.value, -0.035));
    assert(close(r.savings_pct.value, -23.3333333333));
    assert(r.break_even_hits == 13);
}

/* Each missing rate in turn nulls every field. */
static void test_unpriced_rates_null_everything(void)
{
    model_rates variants[4];
    int i;
    for (i = 0; i < 4; i++)
        variants[i] = base_rates();
    variants[0].input_per_m = md_none();
    variants[1].output_per_m = md_none();
    variants[2].cache_read_per_m = md_none();
    variants[3].cache_write_per_m = md_none();

    for (i = 0; i < 4; i++) {
        cache_math_result r = cache_math(variants[i], mk_input(10000.0, 1000.0, 5.0));
        assert(!r.uncached.has && !r.cached.has && !r.savings.has &&
               !r.savings_pct.has && !r.has_break_even);
    }
}

/* hits < 1 counts as 1. */
static void test_hits_below_one(void)
{
    cache_math_result r0 = cache_math(base_rates(), mk_input(10000.0, 1000.0, 0.0));
    cache_math_result r1 = cache_math(base_rates(), mk_input(10000.0, 1000.0, 1.0));
    assert(r0.uncached.value == r1.uncached.value &&
           r0.cached.value == r1.cached.value &&
           r0.savings.value == r1.savings.value &&
           r0.savings_pct.value == r1.savings_pct.value &&
           r0.break_even_hits == r1.break_even_hits);
}

/* Zero tokens -> zero costs with 0%, no division error. */
static void test_zero_tokens(void)
{
    cache_math_result r = cache_math(base_rates(), mk_input(0.0, 0.0, 5.0));
    assert(r.uncached.value == 0.0 && r.cached.value == 0.0 &&
           r.savings.value == 0.0 && r.savings_pct.value == 0.0);
    assert(r.break_even_hits == 13);
}

/* cache_read 0 -> break-even absent but costs kept (10k×$12.5 + 5×(0 + 1k×$50)). */
static void test_zero_read_rate(void)
{
    model_rates m = base_rates();
    m.cache_read_per_m = md_some(0.0);
    cache_math_result r = cache_math(m, mk_input(10000.0, 1000.0, 5.0));
    assert(!r.has_break_even); /* write premium never repaid */
    assert(close(r.uncached.value, 0.75));
    assert(close(r.cached.value, 0.375));
}

/* ceil(4/2) stays 2 — no rounding up at the exact integer boundary. */
static void test_integer_boundary(void)
{
    model_rates m = base_rates();
    m.cache_write_per_m = md_some(4.0);
    m.cache_read_per_m = md_some(2.0);
    cache_math_result r = cache_math(m, mk_input(1000.0, 0.0, 3.0));
    assert(r.has_break_even && r.break_even_hits == 2);
}

int main(void)
{
    test_spec_vector();
    test_negative_saving_at_one_hit();
    test_unpriced_rates_null_everything();
    test_hits_below_one();
    test_zero_tokens();
    test_zero_read_rate();
    test_integer_boundary();

    printf("ok\n");
    return 0;
}

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 →