Describe the bug
Langfuse's OTel ingestion processor does not recognize the canonical OpenInference attribute names for prompt-cache tokens. Specifically, the OpenInference semantic conventions define:
llm.token_count.prompt_details.cache_read — tokens read from cache
llm.token_count.prompt_details.cache_write — tokens written to cache
These names are emitted by every official OpenInference instrumentor that supports prompt caching: openinference-instrumentation-openai, openinference-instrumentation-anthropic, and openinference-instrumentation-agno.
In packages/shared/src/server/otel/OtelIngestionProcessor.ts (v3.169.0), the extractGenericGenAiUsageDetails method has an explicit allow-list of recognized cache-token sub-keys (lines 2530–2539, 2553–2560):
cache_read.input_tokens
cache_read_tokens
details.cache_read_tokens
details.cache_read_input_tokens
cache_creation.input_tokens
cache_write_tokens
details.cache_write_tokens
details.cache_creation_input_tokens
prompt_details.cache_read and prompt_details.cache_write are not in this list.
Effect
The cache token values do reach Langfuse — they end up in usageDetails as raw passthrough keys (e.g. prompt_details.cache_read: 40024). But because they don't match the resolver's recognized names:
- They are NOT subtracted from
input (which can lead to double-counted input on cached calls).
- They are NOT normalized to
input_cached_tokens / input_cache_creation, the canonical Langfuse internal keys.
- They are NOT recognized by the cost engine's cache-aware pricing tiers (defined in
worker/src/constants/default-model-prices.json).
- They are NOT priced at all — the cost engine prices
input at full base rate and silently skips the cache keys.
For a workload using claude-opus-4-6 with prompt caching active, this means Langfuse under-reports cost on cache-heavy observations by ~30% per call, and the Other usage section in the UI shows opaque keys instead of a structured "Input cache read / write" breakdown.
Evidence — live production data
From a 250-observation sample on a production Langfuse v3.169.0 instance (self-hosted), agent using agno==2.5.15 + openinference-instrumentation-agno:
- 28/250 observations (11.2%) had non-zero cache_read or cache_write
usageDetails keys present: input, output, total, prompt_details.cache_read, prompt_details.cache_write
- Single-observation example:
input=443, output=1806, prompt_details.cache_read=40024, prompt_details.cache_write=168
- Langfuse-calculated cost:
$0.047365
- Anthropic actual cost:
$0.068427 (with cache_read at $0.50/MTok, cache_write_5m at $6.25/MTok)
- Per-call under-report: 30.8% on heavily cached observations
Comparison with the established fix pattern (PR #12248)
PR #12248 fixes the same shape of bug for pydantic-ai by adding gen_ai.usage.details.cache_read_tokens / cache_write_tokens to the resolver. The fix proposed here follows the identical pattern for the OpenInference family of instrumentors.
Why this isn't an upstream-OpenInference bug
llm.token_count.prompt_details.cache_read is the documented OpenInference semantic-convention name (see openinference-semantic-conventions/src/openinference/semconv/trace/__init__.py:115). Changing it upstream would break compatibility with every other observability backend that already speaks the OpenInference convention (Arize Phoenix, Datadog, Honeycomb, etc.). The right place to fix this is in Langfuse's OTel ingestion processor.
Steps to reproduce
- Add
agno==2.5.15 and openinference-instrumentation-agno>=0.1.27,<0.2 to a Python environment, plus an OTel exporter targeting a self-hosted Langfuse (v3.169.0 or any release where the resolver allow-list does not include prompt_details.cache_*).
- Configure agno to enable Anthropic prompt caching (e.g.
cache_system_prompt=True plus a mixin that attaches cache_control to the last tool).
- Run any agent flow that produces at least 4096 input tokens (Opus minimum for caching) and is invoked twice within 5 minutes (so the second call hits the cache).
- Inspect the resulting Langfuse observation via the API:
GET /api/public/observations?limit=5&from_start_time=...
- Observe
usageDetails keys: prompt_details.cache_read / prompt_details.cache_write appear as raw keys; input_cached_tokens and input_cache_creation are absent; input is not reduced by the cached amount; cost is computed only on input * base_rate + output * output_rate.
Langfuse Cloud or self-hosted?
Self-hosted
If self-hosted, what version are you running?
v3.169.0 (also confirmed by inspecting v3.173.0 source on main)
SDK and integration versions
agno==2.5.15
openinference-instrumentation-agno>=0.1.27,<0.2
- OpenTelemetry SDK as bundled by
openinference-instrumentation-agno
Additional information
Proposed fix
Two-part change to packages/shared/src/server/otel/OtelIngestionProcessor.ts::extractGenericGenAiUsageDetails:
- Add
prompt_details.cache_read to the cacheReadTokens resolver chain (line 2530).
- Add
prompt_details.cache_write to the cacheCreationTokens resolver chain (line 2535).
- Add both keys to the dedup allow-list at lines 2553–2560 so they're not double-emitted.
Plus a unit test asserting that an OpenInference-style span with llm.token_count.prompt_details.cache_read / cache_write attributes is normalized to input_cached_tokens / input_cache_creation correctly, with input reduced.
I've already implemented and tested this locally against main and will open a PR if a maintainer indicates the diagnosis is correct.
Related issues / PRs
Are you interested in contributing a fix for this bug?
Yes
Describe the bug
Langfuse's OTel ingestion processor does not recognize the canonical OpenInference attribute names for prompt-cache tokens. Specifically, the OpenInference semantic conventions define:
llm.token_count.prompt_details.cache_read— tokens read from cachellm.token_count.prompt_details.cache_write— tokens written to cacheThese names are emitted by every official OpenInference instrumentor that supports prompt caching:
openinference-instrumentation-openai,openinference-instrumentation-anthropic, andopeninference-instrumentation-agno.In
packages/shared/src/server/otel/OtelIngestionProcessor.ts(v3.169.0), theextractGenericGenAiUsageDetailsmethod has an explicit allow-list of recognized cache-token sub-keys (lines 2530–2539, 2553–2560):prompt_details.cache_readandprompt_details.cache_writeare not in this list.Effect
The cache token values do reach Langfuse — they end up in
usageDetailsas raw passthrough keys (e.g.prompt_details.cache_read: 40024). But because they don't match the resolver's recognized names:input(which can lead to double-counted input on cached calls).input_cached_tokens/input_cache_creation, the canonical Langfuse internal keys.worker/src/constants/default-model-prices.json).inputat full base rate and silently skips the cache keys.For a workload using
claude-opus-4-6with prompt caching active, this means Langfuse under-reports cost on cache-heavy observations by ~30% per call, and theOther usagesection in the UI shows opaque keys instead of a structured "Input cache read / write" breakdown.Evidence — live production data
From a 250-observation sample on a production Langfuse v3.169.0 instance (self-hosted), agent using
agno==2.5.15+openinference-instrumentation-agno:usageDetailskeys present:input,output,total,prompt_details.cache_read,prompt_details.cache_writeinput=443,output=1806,prompt_details.cache_read=40024,prompt_details.cache_write=168$0.047365$0.068427(with cache_read at $0.50/MTok, cache_write_5m at $6.25/MTok)Comparison with the established fix pattern (PR #12248)
PR #12248 fixes the same shape of bug for
pydantic-aiby addinggen_ai.usage.details.cache_read_tokens/cache_write_tokensto the resolver. The fix proposed here follows the identical pattern for the OpenInference family of instrumentors.Why this isn't an upstream-OpenInference bug
llm.token_count.prompt_details.cache_readis the documented OpenInference semantic-convention name (seeopeninference-semantic-conventions/src/openinference/semconv/trace/__init__.py:115). Changing it upstream would break compatibility with every other observability backend that already speaks the OpenInference convention (Arize Phoenix, Datadog, Honeycomb, etc.). The right place to fix this is in Langfuse's OTel ingestion processor.Steps to reproduce
agno==2.5.15andopeninference-instrumentation-agno>=0.1.27,<0.2to a Python environment, plus an OTel exporter targeting a self-hosted Langfuse (v3.169.0 or any release where the resolver allow-list does not includeprompt_details.cache_*).cache_system_prompt=Trueplus a mixin that attachescache_controlto the last tool).GET /api/public/observations?limit=5&from_start_time=...usageDetailskeys:prompt_details.cache_read/prompt_details.cache_writeappear as raw keys;input_cached_tokensandinput_cache_creationare absent;inputis not reduced by the cached amount; cost is computed only oninput * base_rate + output * output_rate.Langfuse Cloud or self-hosted?
Self-hosted
If self-hosted, what version are you running?
v3.169.0 (also confirmed by inspecting v3.173.0 source on
main)SDK and integration versions
agno==2.5.15openinference-instrumentation-agno>=0.1.27,<0.2openinference-instrumentation-agnoAdditional information
Proposed fix
Two-part change to
packages/shared/src/server/otel/OtelIngestionProcessor.ts::extractGenericGenAiUsageDetails:prompt_details.cache_readto thecacheReadTokensresolver chain (line 2530).prompt_details.cache_writeto thecacheCreationTokensresolver chain (line 2535).Plus a unit test asserting that an OpenInference-style span with
llm.token_count.prompt_details.cache_read/cache_writeattributes is normalized toinput_cached_tokens/input_cache_creationcorrectly, withinputreduced.I've already implemented and tested this locally against
mainand will open a PR if a maintainer indicates the diagnosis is correct.Related issues / PRs
cache_read_input_tokens/cache_creation_input_tokensin OTEL generation spans for accurate Anthropic cost calculation #12635 — broader "support cache_read_input_tokens / cache_creation_input_tokens in OTEL generation spans" — this PR addresses the OpenInference subset of that issue.fix(otel): correct pydantic-ai v0.7.x+ cache token attribute names— same structural fix for a different upstream.Are you interested in contributing a fix for this bug?
Yes