Pricing math
`@pleach/core/pricing` turns token counts into USD against a (family × callClass) rate matrix — the cost math behind the audit ledger's per-turn dollar figure.
@pleach/core/pricing is the cost math. It turns a turn's token
counts into a dollar figure against a (family × callClass) rate
matrix. It's what puts the USD number next to a row in the
audit ledger — pure functions plus an optional
live-rate resolver, no side effects in the hot path.
computeTurnCost
The pure primitive. Give it the token counts and a matrix; get back
the cost, or null when the matrix has no cell for that
(family, callClass).
import { computeTurnCost, DEFAULT_PRICING_MATRIX } from "@pleach/core/pricing"
const cost = computeTurnCost(
{
family: "anthropic",
callClass: "converse",
inputTokens: 1_200,
outputTokens: 800,
reasoningTokens: 400, // folded into output at the output rate
},
DEFAULT_PRICING_MATRIX, // optional; this is the default
)
// cost: ComputeTurnCostResult | nullComputeTurnCostInput carries family, callClass, inputTokens,
outputTokens, and optional reasoningTokens. Input tokens are
fresh tokens — cache reads are not billed at the input rate.
Reasoning tokens bill at the output rate. A missing matrix cell
returns null rather than guessing, so a caller never silently
under-reports.
DEFAULT_PRICING_MATRIX
The bundled rate table. Every (family × callClass) cell is present —
no missing-cell fallback needed. Each cell is a PricingEntry:
interface PricingEntry {
readonly inputCostPer1M: number // USD per 1M input tokens
readonly outputCostPer1M: number // USD per 1M output tokens
}Live rates — createPricingResolver
Bundled rates drift. createPricingResolver caches a fetched matrix
and re-fetches when it goes stale.
import { createPricingResolver, DEFAULT_PRICING_MATRIX } from "@pleach/core/pricing"
const resolver = createPricingResolver({
ttlMs: 60 * 60 * 1000, // consider a fetched matrix fresh for 1h
fallback: DEFAULT_PRICING_MATRIX, // used until the first fetch lands
})
const matrix = await resolver.resolve()
const cost = computeTurnCost(input, matrix)ttlMs— freshness window. Within it,.resolve()returns the cached matrix synchronously (wrapped in a resolved promise); past it, the next.resolve()triggers a fetch that blocks until the new data lands. SetttlMs: Infinityto fetch exactly once — the right choice in a short-lived function invocation.fallback— returned when no matrix is cached and the first fetch fails. After that, a failed fetch returns the last-good matrix. PassDEFAULT_PRICING_MATRIX.- Concurrent
.resolve()calls during a fetch share one in-flight request — no fetch stampede.
schedulePricingRefresh(resolver, { intervalMs }) drives the refresh
on a timer instead of lazily on read.
OpenRouter rates
The live source behind the resolver is OpenRouter's model list.
fetchOpenRouterPricing(opts?)→FetchOpenRouterPricingResult— pulls current per-model rates (overrideendpointto point at a mirror).applyToMatrix({ base, byModelId, modelIdMap })— folds fetched per-model rates into a(family × callClass)matrix, mapping model IDs to matrix cells.
Helpers
parseModelSlug(slug)→ParseModelSlugResult | null— splits a"vendor/model"slug into its parts, ornullwhen the slug isn't a singlevendor/modelpair.resolveCostEventUsd(event, matrix?)— returns the event's owncostUsdwhen the seam already priced it (a real rate is never overridden), and otherwise computes the cost from the event's token counts. This is the reconciliation seam: providers that report a price keep it; providers that don't get one derived.
Where it sits
The cost figure this module computes rolls up per turn into the audit
ledger's TokenCostRecord (see Typed records)
and, in a multi-tenant deployment, into
@pleach/gateway's cost events. Pricing
is the shared arithmetic underneath both.
The AuditableCall row
The typed, append-only row shape that every LLM invocation writes to the audit ledger — identity, principal, call, decision, outcome, and typed payload slots.
Tamper-evident hash chain
prev_hash + row_hash columns on the audit-ledger event log — what the chain proves, how rows get verified, and how the default-on writer-side stamping ships in the @pleach/core schema bundle.