Before you start
You need Node.js 22 or later, a Langfuse public and secret key for the target project, and access to the v2 Observations API. The current Langfuse compatibility documentation lists this endpoint for Langfuse Cloud and self-hosted v4; self-hosted v3 requires a different read implementation. The Narev pricing endpoints in this guide are public, so you don’t need a Narev API key. The result is an estimate at Narev’s lookup-time rates, not a reconstruction of the original invoice. The cost request has no historical pricing date. Keep an export of the target observations and retain the dry-run output if you need an audit trail. Choose a bounded time range for the backfill. Smaller ranges are easier to review and retry. They also prevent the script from scanning your complete Langfuse history.1. Find generations without costs
Start with the Langfuse Observations API v2. Ask for generation observations and include themodel and usage field groups. You don’t need prompts, outputs, or metadata to calculate cost.
id, a traceId, a model, and usageDetails. Ignore observations that already have costDetails.total or totalCost.
Langfuse returns a cursor in meta.cursor when another page exists. Send that cursor with the next request and continue until it is absent or returns null. The complete script handles this loop, so a backfill isn’t limited to the first page.
2. Translate the token usage
Narev needs prompt, completion, cache, and reasoning token counts. Langfuse stores the same information in named buckets insideusageDetails, but the names depend on the integration that created the observation.
Inspect several usageDetails objects and compare them with the integration that wrote them. The same field name can describe a total in one exporter and an exclusive bucket in another. The script requires an explicit USAGE_BUCKETS setting: use exclusive when input excludes cache tokens and output excludes reasoning, or inclusive when they already contain those subcategories. Run separate batches or customize mapUsage if exporters in the same project use different conventions.
The script constructs total prompt and completion counts for Narev. For example, exclusive input of 500 plus 1,000 cache-read tokens becomes prompt_tokens: 1500 and cache_read_tokens: 1000. Exclusive output of 200 plus 50 reasoning tokens becomes completion_tokens: 250 and reasoning_tokens: 50. This matches the verified direct DeepSeek calculation. Validate the selected provider’s accounting before enabling writes; do not assume an unfamiliar modality or pricing tier follows the same calculation.
Unknown usage keys, conflicting aliases, and invalid counts cause the script to skip an observation. Extend the mapping for your exporter rather than quietly ignoring a billable bucket. A plain text observation with only input and output counts needs no cache adjustment.
3. Match the model to a price
Use the model name frommodel to search Narev:
modelOverrides in the complete script. This keeps an ambiguous match from producing the wrong cost.
4. Calculate and write the cost
Send the resolved model, provider, and token counts to the Narev cost endpoint:cost_breakdown.total. Write that value to the existing generation with a Langfuse generation-update event. Reuse the observation’s id and traceId; only the event envelope gets a new UUID.
Langfuse responds to ingestion requests with HTTP 207, including successful requests. You still need to inspect the response’s errors array. The script checks both the HTTP response and each event result before it reports an update as successful.
5. Run the backfill
The script combines the four stages above. It pages through the selected time range, skips observations that already have costs, and handles one observation at a time. A bad model alias or unfamiliar usage shape doesn’t stop the rest of the run. Dry-run mode is enabled by default. The script updates onlycostDetails.total; it does not fabricate input/output cost components or send customer billing events. In that mode, the script calculates and prints costs without changing Langfuse. Expand the code only when you’re ready to save it as backfill-langfuse-costs.mjs.
View the complete backfill script
View the complete backfill script
backfill-langfuse-costs.mjs
inclusive if inspection of your exporter confirms it already sends totals:
Updated <observation-id>: $<cost>. Open that observation in Langfuse and confirm that its total cost matches the script output. Confirm that the original trace and generation IDs remain unchanged. Re-read a small completed range after ingestion becomes visible and check that priced observations are skipped. The initial export is the evidence you need to restore a value if the chosen mapping was wrong.
Troubleshooting
A model-match error means that search found no exact row, multiple rows, or no public pricing. Add an explicitmodelOverrides entry using the actual serving provider; do not choose the first search result. The cost endpoint can still reject a pair that has no usable public price.
An unmapped usage key or ambiguous alias means mapUsage cannot interpret the observation safely. Inspect the exporter’s contract and adjust the key mapping. For an unexpectedly low or high cost, compare the reconstructed totals with the original provider usage. In particular, avoid subtracting cache hits from a prompt total that Narev already adjusts internally.
For a rejected update, read the ingestion event error even when HTTP status is 207. Confirm that the observation and trace IDs belong to the project associated with your keys. The script reports per-observation failures and exits unsuccessfully if any were skipped; review those entries before declaring the range complete. If a read request fails, rerun the bounded range after resolving the error. Wait for prior writes to become visible before retrying.
If the script finds nothing, check the UTC range and the source observation’s cost fields. Zero is an existing cost, so it is skipped rather than treated as missing. Also confirm v2 endpoint availability and allow for your Langfuse ingestion/read delay before diagnosing a missing record.