Token Usage Enrichment#
Token usage enrichment is the process by which Langfuse resolves and populates usage_details (token counts) and cost_details (prices) on observation records during ingestion. The pipeline runs inside the worker after events are consumed from the queue and lives in IngestionService.
The two core private methods are:
| Method | Responsibility |
|---|---|
getGenerationUsage | Orchestrates model lookup → usage units → pricing tier → cost calculation |
getUsageUnits | Resolves final token counts from user-provided values or automatic tokenization |
Enrichment Pipeline#
provided_model_name?
└─ yes → findModel (L1 cache → Redis → Postgres)
└─ no → model = null
model + no user-provided usage + level ≠ ERROR?
└─ yes → tokenCountAsync (worker thread pool)
└─ fails → tokenCount (synchronous fallback)
└─ no → use provided values as-is, synthesize total if missing
model found + pricingTiers available?
└─ yes → matchPricingTier → calculateUsageCosts
└─ no → cost_details = provided or empty
1. Model Lookup Gate (findModel)#
Tokenization and cost calculation both require a resolved model. getGenerationUsage calls findModel only when provided_model_name is present; otherwise internalModel is null and automatic tokenization is skipped entirely.
findModel resolves models through a three-layer cache:
- L1 (in-process TTL cache) — default 10 s, no invalidation
- L2 (Redis) — caches model + pricing tiers when Redis is configured
- Postgres — regex
match_patternfield on theModeltable
Negative lookups are cached with a sentinel (NOT_FOUND_TOKEN) to avoid repeated DB hits for unknown models .
2. User-Provided Values (getUsageUnits)#
getUsageUnits normalizes all values in provided_usage_details to numbers first (guarding against ClickHouse returning UInt64 as strings). If any user-provided usage key is present, automatic tokenization is skipped entirely . When the user supplies individual keys (e.g., input, output) without a total, the total is synthesized by summation .
3. Automatic Tokenization Fallback#
Automatic tokenization only runs when all three conditions hold :
- A model was resolved (
model != null) provided_usage_detailsis empty- Observation
levelis notERROR
The primary path uses tokenCountAsync, which offloads work to a non-blocking worker thread pool. On failure, it falls back to the synchronous tokenCount . If both paths fail, ingestion continues with empty usage_details rather than blocking the record .
Both tokenizers dispatch on model.tokenizerId to pick the right tokenizer — e.g., OpenAI tiktoken or Claude's tokenizer .
4. Pricing Tier Matching and Cost Calculation#
After usage units are resolved, matchPricingTier selects the applicable pricing tier by evaluating non-default tiers in priority order using AND-logic regex conditions on usage detail keys, falling back to the default tier if none match .
calculateUsageCosts then computes costs. The same user-preference rule applies: if any provided_cost_details key is set, all automatic cost calculation is skipped and user values are used directly . Otherwise, costs are computed as price × units per usage type from the matched tier .
Key Source Files#
| File | Purpose |
|---|---|
worker/src/services/IngestionService/index.ts | getGenerationUsage, getUsageUnits, calculateUsageCosts |
packages/shared/src/server/ingestion/modelMatch.ts | findModel with multi-layer caching |
packages/shared/src/server/pricing-tiers/matcher.ts | matchPricingTier |
worker/src/features/tokenisation/async-usage.ts | tokenCountAsync (thread pool) |
worker/src/features/tokenisation/usage.ts | tokenCount (synchronous) |
worker/src/services/IngestionService/tests/IngestionService.integration.test.ts | Integration tests covering enrichment scenarios |