Before you start
You need Node.js 22 or later and a Langfuse public and secret key. The Narev pricing endpoints in this guide are public, so you don’t need a Narev API key. 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 providedModelName, 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 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.
For a typical Langfuse observation, map input to prompt_tokens and output to completion_tokens. Map cache_read_input_tokens and cache_write_input_tokens to their corresponding Narev cache fields. If the observation separates reasoning tokens into output_reasoning_tokens, send them as reasoning_tokens.
The important rule is to count each token once. Langfuse treats these values as separate buckets. If your integration includes cached tokens in input, subtract them before sending the prompt count to Narev.
3. Match the model to a price
Use the model name fromprovidedModelName 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. In that mode, the script calculates and prints costs without changing Langfuse. Expand the code only when you’re ready to save it asbackfill-langfuse-costs.mjs.
View the complete backfill script
View the complete backfill script
backfill-langfuse-costs.mjs
A successful write prints
Updated <observation-id>: $<cost>. Open that observation in Langfuse and confirm that its total cost matches the script output.Troubleshooting
A model couldn’t be matched
Expected one Narev price means the search found no exact price or found the model under several providers. Add the Langfuse model name and your chosen Narev model and provider to modelOverrides.
The usage shape isn’t supported
Unsupported usageDetails means your Langfuse integration uses token field names that mapUsage doesn’t recognize. Inspect the observation, add the relevant keys, and confirm that prompt, cache, and reasoning tokens don’t overlap.
Langfuse rejected an update
Read the event error returned with the HTTP207 response. Confirm that the observation ID and trace ID came from the same generation and that the API keys belong to its Langfuse project.
The script found nothing to update
Check the UTC date range first. If the range is correct, inspect one expected generation and confirm that it hasusageDetails but doesn’t already have costDetails.total.
The calculated cost is too high
Check whether theinput bucket includes cached tokens. If it does, subtract the cached count before sending prompt_tokens; otherwise, the script charges those tokens twice.