LLM Cost Calculator — Go source
Estimate LLM costs per request or per month at billion-token scale — with realistic prompt-cache hit rates, four-lane pricing, and side-by-side model comparison from a dated pricing snapshot.
This is the Go implementation — the same logic the interactive tool runs, in a shareable, citable form.
// Package llmcost is the Go twin of CosmoDev's src/lib/llmCost.ts (dual
// source: the web lib is TypeScript, the CLI lib is Go — kept in lock-step).
// All rates flow from the model snapshot accessor (cosmodev/aimodels) — never
// hardcoded here; custom rates are the one exception, supplied by the caller
// as a CostRates value. Pure + deterministic, never panics. The table-driven
// tests in llm-cost-calculator_test.go share vectors with
// src/lib/llmCost.test.ts so the two implementations are held to the same
// contract.
package llmcost
import (
"fmt"
"math"
"slices"
"strconv"
"strings"
"cosmodev/aimodels"
)
// BatchDiscount is the multiplier applied to Batch API pricing (the standard
// 50% discount). Mirrors BATCH_DISCOUNT in src/lib/llmCost.ts.
const BatchDiscount = 0.5
// CostRates is a per-million-token price pair. Pointer fields are nil when
// the upstream value is null (unpriced / unknown), mirroring `number | null`.
type CostRates struct {
InputPerM *float64
OutputPerM *float64
}
// CostInput describes one workload. Requests is a *int so the zero value
// means "1 request" (the TS default `requests ?? 1`) while an explicit 0 is
// still honored verbatim — same pointer-for-omitted pattern as slugify's
// Options.Separator.
type CostInput struct {
InputTokens int // input tokens per request
OutputTokens int // output tokens per request
Requests *int // nil → 1
Batch bool // apply BatchDiscount
}
// ModelCost is one comparison row: a model's id, its rates, the workload cost
// (nil when unpriced), and output tokens per USD (nil-safe).
type ModelCost struct {
ID string
InputPerM *float64
OutputPerM *float64
Cost *float64
TokensPerDollar *float64
}
// CostFor returns the cost in USD for a workload, or nil when either rate is
// unpriced. It is the Go twin of costFor() in src/lib/llmCost.ts and must
// agree with it on every shared vector:
// ((inputTokens/1e6)·inputPerM + (outputTokens/1e6)·outputPerM) × requests
// × BatchDiscount when batch.
func CostFor(rates CostRates, input CostInput) *float64 {
if rates.InputPerM == nil || rates.OutputPerM == nil {
return nil
}
requests := 1
if input.Requests != nil {
requests = *input.Requests
}
discount := 1.0
if input.Batch {
discount = BatchDiscount
}
base := (float64(input.InputTokens)/1e6)*(*rates.InputPerM) +
(float64(input.OutputTokens)/1e6)*(*rates.OutputPerM)
cost := base * float64(requests) * discount
return &cost
}
// RatesFor projects a model onto its CostRates pair, mirroring ratesFor().
func RatesFor(m *aimodels.Model) CostRates {
return CostRates{InputPerM: m.InputPerM, OutputPerM: m.OutputPerM}
}
// -- Cache-aware monthly estimator (FEAT-070; twin of the TS extension) --
// CacheRates is all four pricing lanes; cache fields nil when the model has
// no caching, mirroring CacheRates in src/lib/llmCost.ts.
type CacheRates struct {
InputPerM *float64
OutputPerM *float64
CacheReadPerM *float64
CacheWritePerM *float64
}
// MonthlyInput is a monthly workload at scale. Pointer fields mirror the TS
// optionals: CacheableFraction nil → 0.9; CacheWriteTokensPerMonth nil →
// derive from this month's misses.
type MonthlyInput struct {
InputTokensPerMonth int64
OutputTokensPerMonth int64
CacheHit float64
CacheableFraction *float64
CacheWritePerMonth *int64
Batch bool
}
// MonthlyCost is the four-lane monthly estimate. Lane pointers are nil when
// unpriced (input/output) or on the degrade path (cache wanted, rates
// absent); CacheUnavailable flags that degrade. BlendedPerM is
// total / (input+output in millions), nil when total is nil.
type MonthlyCostResult struct {
Total *float64
UncachedInput *float64
CacheReads *float64
CacheWrites *float64
Output *float64
CacheUnavailable bool
BlendedPerM *float64
}
// CacheRatesFor projects a model onto all four lanes, mirroring cacheRatesFor().
func CacheRatesFor(m *aimodels.Model) CacheRates {
return CacheRates{
InputPerM: m.InputPerM,
OutputPerM: m.OutputPerM,
CacheReadPerM: m.CacheReadPerM,
CacheWritePerM: m.CacheWritePerM,
}
}
// MonthlyCost prices a monthly workload with cache economics. Caching is
// active iff CacheHit > 0 AND both cache rates are priced; otherwise the
// estimate degrades to plain no-cache math with CacheUnavailable set.
// Twin of monthlyCost() in src/lib/llmCost.ts — must agree on every shared
// vector in the test files.
func MonthlyCost(rates CacheRates, w MonthlyInput) MonthlyCostResult {
unpriced := rates.InputPerM == nil || rates.OutputPerM == nil
cacheable := 0.9
if w.CacheableFraction != nil {
cacheable = *w.CacheableFraction
}
wantedCache := w.CacheHit > 0
hasCacheRates := rates.CacheReadPerM != nil && rates.CacheWritePerM != nil
active := wantedCache && hasCacheRates
hit := 0.0
if active {
hit = w.CacheHit
}
if unpriced {
return MonthlyCostResult{CacheUnavailable: wantedCache && !hasCacheRates}
}
inT := float64(w.InputTokensPerMonth)
missTokens := inT * (1 - hit)
writeTokens := 0.0
if w.CacheWritePerMonth != nil {
writeTokens = float64(*w.CacheWritePerMonth)
} else if active {
writeTokens = missTokens * cacheable
}
uncachedInput := (missTokens / 1e6) * *rates.InputPerM
// Lane semantics: real 0 when caching is simply off; nil only on the
// degrade path (cache wanted, rates absent — the price is unknowable).
var cacheReads, cacheWrites *float64
degrade := wantedCache && !hasCacheRates
if active {
cr := (inT * hit / 1e6) * *rates.CacheReadPerM
cw := (writeTokens / 1e6) * *rates.CacheWritePerM
cacheReads, cacheWrites = &cr, &cw
} else if degrade {
cacheReads, cacheWrites = nil, nil
} else {
zero := 0.0
cacheReads, cacheWrites = &zero, &zero
}
output := (float64(w.OutputTokensPerMonth) / 1e6) * *rates.OutputPerM
lanes := uncachedInput + output
if cacheReads != nil {
lanes += *cacheReads
}
if cacheWrites != nil {
lanes += *cacheWrites
}
if w.Batch {
lanes *= BatchDiscount
}
totalMTok := (inT + float64(w.OutputTokensPerMonth)) / 1e6
out := MonthlyCostResult{
Total: &lanes,
UncachedInput: &uncachedInput,
CacheReads: cacheReads,
CacheWrites: cacheWrites,
Output: &output,
CacheUnavailable: wantedCache && !hasCacheRates,
}
if totalMTok > 0 {
b := lanes / totalMTok
out.BlendedPerM = &b
}
return out
}
// ParseTokensPerMonth parses "2B"/"1.5b"/"500M"/"10m"/"2K"/"2000"/"1_000_000"
// (case-insensitive suffix, underscores allowed) into a token count. Twin of
// parseTokensPerMonth(); returns an error on anything else.
func ParseTokensPerMonth(s string) (int64, error) {
t := strings.TrimSpace(s)
lower := strings.ToLower(t)
mult := 1.0
switch {
case strings.HasSuffix(lower, "b"):
mult, t = 1e9, lower[:len(lower)-1]
case strings.HasSuffix(lower, "m"):
mult, t = 1e6, lower[:len(lower)-1]
case strings.HasSuffix(lower, "k"):
mult, t = 1e3, lower[:len(lower)-1]
}
t = strings.ReplaceAll(t, "_", "")
if t == "" {
return 0, fmt.Errorf("unparseable tokens/month: %q", s)
}
v, err := strconv.ParseFloat(t, 64)
if err != nil || v < 0 {
return 0, fmt.Errorf("unparseable tokens/month: %q", s)
}
return int64(math.Round(v * mult)), nil
}
// WorkloadPreset is an archetype default for realistic cache modeling.
type WorkloadPreset struct {
ID string
Label string
CacheHit float64
CacheableFraction float64
Rationale string
}
// WorkloadPresets mirrors WORKLOAD_PRESETS in src/lib/llmCost.ts — planning
// numbers from published prompt-caching writeups, refined as data accumulates.
var WorkloadPresets = []WorkloadPreset{
{ID: "agent-coding", Label: "Agent / coding", CacheHit: 0.75, CacheableFraction: 0.95,
Rationale: "Large stable system prompt plus repo context, re-sent every turn — the highest-hit profile."},
{ID: "chat", Label: "Chat / assistant", CacheHit: 0.30, CacheableFraction: 0.60,
Rationale: "Short varied conversations; only the system prompt and early turns stay hot."},
{ID: "rag", Label: "RAG / retrieval", CacheHit: 0.50, CacheableFraction: 0.80,
Rationale: "Preamble caches well but each query pulls fresh retrieved chunks."},
{ID: "batch-summarize", Label: "Batch summarize", CacheHit: 0.60, CacheableFraction: 0.50,
Rationale: "Shared instruction prefix across many unique documents."},
}
// CompareModels costs every requested model for one workload and sorts the
// rows: cost asc, nil-cost last, ties (equal cost or nil-nil) by id asc.
// Unknown ids are dropped. A nil models slice means "use the live snapshot"
// (via aimodels.AllModels), mirroring the TS default parameter. It is the Go
// twin of compareModels() in src/lib/llmCost.ts.
func CompareModels(ids []string, input CostInput, models []aimodels.Model) []ModelCost {
all := models
if all == nil {
var err error
if all, err = aimodels.AllModels(); err != nil {
return nil
}
}
rows := make([]ModelCost, 0, len(ids))
for _, id := range ids {
for i := range all {
if all[i].ID != id {
continue
}
m := &all[i]
var tpd *float64
if v, ok := aimodels.TokensPerDollar(m); ok {
tpd = &v
}
rows = append(rows, ModelCost{
ID: id,
InputPerM: m.InputPerM,
OutputPerM: m.OutputPerM,
Cost: CostFor(RatesFor(m), input),
TokensPerDollar: tpd,
})
break
}
}
slices.SortStableFunc(rows, func(a, b ModelCost) int {
if a.Cost == nil || b.Cost == nil {
if a.Cost == nil && b.Cost == nil {
return strings.Compare(a.ID, b.ID)
}
if a.Cost == nil {
return 1
}
return -1
}
if *a.Cost == *b.Cost {
return strings.Compare(a.ID, b.ID)
}
if *a.Cost < *b.Cost {
return -1
}
return 1
})
return rows
}
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 →