Skip to content

Codex cached input is double counted in Langfuse usage/cost #22

Description

@os-groot

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:

p.info?.last_token_usage

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:

117231 + 225 = 117456

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.

Image Image

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions