Docs menu
Cost & pricing observability
Computes per-run token/cost/trace from the journal; reads the pricing table (__pricing__) from the journal, versioned.
What it's for / when to use it#
GNL writes model calls' usage info (input/output/cached tokens) to the journal. getRunCost scans this journal POST-HOC to produce an exact, deterministic per-run cost — not a live/estimated "cost guard", but a literal sum of what actually happened. Use it when you need to answer "how much did this conversation cost me, how much did each model consume" after a run finishes, when generating a billing/usage report, or when feeding an OTel-compatible observability chain (trace exporter).
Setup / import#
No separate sub-package is needed; it comes from the @gnldev/durable root export.
import { getRunCost, toTraceSpans } from '@gnldev/durable';Step-by-step usage#
getRunCost takes a JournalReader and a runId; it sums the usage of all model entries in the run and computes cost from the pricing table (default DEFAULT_PRICING):
const cost = await getRunCost(reader, runId);
// { runId, inputTokens, outputTokens, cachedTokens, totalTokens,
// modelCalls, toolCalls, costUsd, byModel: { 'claude-sonnet-5': { calls, tokens, costUsd }, ... } }If you want to pass your own pricing table, you can override it with opts.pricing; if the journal has no model id (e.g. a mock model), assign a default with opts.modelId:
const cost = await getRunCost(reader, runId, { modelId: 'claude-sonnet-5' });To feed an OTel exporter or for a waterfall visualization, convert the journal to a gen_ai.* semantic-compatible span list:
const spans = await toTraceSpans(reader, runId);
// [{ name: 'llm.generate' | 'tool.execute', kind: 'model' | 'tool', runId, seq, attributes }, ...]If you want to update the pricing table without a deploy, you can read the __pricing__ document from the journal to get the effective table (falls back to DEFAULT_PRICING if there's no record in the journal):
import { effectivePricingTable } from '@gnldev/durable';
const table = await effectivePricingTable(journal); // __pricing__ (varsa) > DEFAULT_PRICINGStudio serves the same computation over REST — you don't need to re-implement it in your own backend:
GET /runs/:id/cost → getRunCost(reader, id)
GET /runs/:id/trace → journal'dan waterfall span'leri (maliyet dahil)API reference#
getRunCostComputes total/per-model token and USD cost for a run from the journal (async).
toTraceSpansConverts journal entries into a TraceSpan list compatible with OTel gen_ai semantics (async).
RunCostgetRunCost return type: inputTokens/outputTokens/cachedTokens/totalTokens/modelCalls/toolCalls/costUsd/byModel.
TraceSpanA single span record with name/kind/runId/seq/attributes fields.
DEFAULT_PRICINGAn approximate model pricing table ($/1M tokens) for the current Anthropic and OpenAI families. Lookup is longest-prefix, so a single entry prices a whole family, and the journal’s own pricing document layers over it (effectivePricingTable).
priceForResolves the price for a modelId: exact match first, otherwise the longest (most specific) prefix match.
costOfComputes the USD cost from a usage record; cached tokens are priced separately.
readPricingReads the __pricing__ PricingDoc live from the journal (undefined if absent).
effectivePricingTableEffective pricing table: journal __pricing__ (if present) > DEFAULT_PRICING.
PRICING_KEYThe journal key for the pricing document: '__pricing__'.
ModelPricingA pricing record with inputPer1M / outputPer1M / cachedInputPer1M (optional) fields.
DEFAULT_PRICING table are approximate — you can switch to a live, versioned pricing table tailored to your own agreement by writing a __pricing__ document to the journal (via Studio or directly); every update increments PricingDoc.version and keeps a full history via the audit log.budget-quota page if you want to enforce quotas/limits, and the studio-inspector page to inspect this cost/trace data visually in Studio's timeline.