pleach
Operate

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 | null

ComputeTurnCostInput 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. Set ttlMs: Infinity to 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. Pass DEFAULT_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 (override endpoint to 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, or null when the slug isn't a single vendor/model pair.
  • resolveCostEventUsd(event, matrix?) — returns the event's own costUsd when 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.

On this page