Summary
The Codex tracing plugin currently maps Codex token usage into Langfuse usage details in a way that mixes two different semantics:
- Codex/OpenAI semantics:
cached_input_tokens is a subset of input_tokens
- Langfuse
cache_read_input_tokens semantics: cached input is an additional usage bucket alongside input
This causes Langfuse to display duplicated input usage and infer incorrect costs for cache-heavy Codex generations.
Current behavior
In plugins/tracing/src/parse.ts, per-generation usage is taken from:
In plugins/tracing/src/trace.ts, toUsageDetails() currently maps Codex usage approximately as:
details.input = usage.input_tokens;
details.output = usage.output_tokens;
details.total = usage.total_tokens;
details.cache_read_input_tokens = usage.cached_input_tokens;
For a Codex token_count event such as:
{
"input_tokens": 117231,
"cached_input_tokens": 115584,
"output_tokens": 225,
"total_tokens": 117456
}
the plugin sends:
{
input: 117231,
cache_read_input_tokens: 115584,
output: 225,
total: 117456
}
However, cached_input_tokens is already included in input_tokens. The actual input breakdown is:
uncached input = 117231 - 115584 = 1647
cached input = 115584
total input = 117231
output = 225
total tokens = 117456
Impact
Langfuse groups usage types containing input as input usage. As a result, the table can show:
117231 + 115584 = 232815 input-side tokens
while the explicit total remains:
This is confusing in the UI and leads to inflated inferred cost when input is charged at the full input-token rate. Cached input is effectively treated as full-price input because it is still included in input.
For the example above, the correct cost calculation should be based on:
uncached input tokens = input_tokens - cached_input_tokens
cached input tokens = cached_input_tokens
output tokens = output_tokens
not:
input tokens + cached input tokens + output tokens
Proposed fix
Normalize Codex usage before sending it to Langfuse:
function toUsageDetails(
usage: TokenUsage | undefined,
): Record<string, number> | undefined {
if (!usage) return undefined;
const details: Record<string, number> = {};
const inputTokens =
typeof usage.input_tokens === "number" ? usage.input_tokens : undefined;
const cachedInputTokens =
typeof usage.cached_input_tokens === "number"
? usage.cached_input_tokens
: 0;
if (typeof inputTokens === "number") {
details.input = Math.max(inputTokens - cachedInputTokens, 0);
}
if (cachedInputTokens > 0) {
details.cache_read_input_tokens = cachedInputTokens;
}
if (typeof usage.output_tokens === "number") {
details.output = usage.output_tokens;
}
if (typeof usage.total_tokens === "number") {
details.total = usage.total_tokens;
}
if (typeof usage.reasoning_output_tokens === "number") {
details.reasoning_tokens = usage.reasoning_output_tokens;
}
return Object.keys(details).length > 0 ? details : undefined;
}
With the same example, the plugin would send:
{
input: 1647,
cache_read_input_tokens: 115584,
output: 225,
total: 117456
}
That aligns the emitted usage with Langfuse’s additive cache_read_input_tokens model:
input + cache_read_input_tokens + output
= 1647 + 115584 + 225
= 117456
It also allows model pricing to work correctly when the Langfuse model definition has prices for:
input
cache_read_input_tokens
output
Notes
This changes the meaning of usageDetails.input emitted by the plugin from “total prompt/input tokens” to “uncached input tokens”. I think that is the right behavior when using cache_read_input_tokens, because Langfuse treats that field as a separate additive usage bucket.

Summary
The Codex tracing plugin currently maps Codex token usage into Langfuse usage details in a way that mixes two different semantics:
cached_input_tokensis a subset ofinput_tokenscache_read_input_tokenssemantics: cached input is an additional usage bucket alongsideinputThis causes Langfuse to display duplicated input usage and infer incorrect costs for cache-heavy Codex generations.
Current behavior
In
plugins/tracing/src/parse.ts, per-generation usage is taken from:In
plugins/tracing/src/trace.ts,toUsageDetails()currently maps Codex usage approximately as:For a Codex
token_countevent such as:{ "input_tokens": 117231, "cached_input_tokens": 115584, "output_tokens": 225, "total_tokens": 117456 }the plugin sends:
However,
cached_input_tokensis already included ininput_tokens. The actual input breakdown is:Impact
Langfuse groups usage types containing
inputas input usage. As a result, the table can show:while the explicit total remains:
This is confusing in the UI and leads to inflated inferred cost when
inputis charged at the full input-token rate. Cached input is effectively treated as full-price input because it is still included ininput.For the example above, the correct cost calculation should be based on:
not:
Proposed fix
Normalize Codex usage before sending it to Langfuse:
With the same example, the plugin would send:
That aligns the emitted usage with Langfuse’s additive
cache_read_input_tokensmodel:It also allows model pricing to work correctly when the Langfuse model definition has prices for:
Notes
This changes the meaning of
usageDetails.inputemitted by the plugin from “total prompt/input tokens” to “uncached input tokens”. I think that is the right behavior when usingcache_read_input_tokens, because Langfuse treats that field as a separate additive usage bucket.