Conversation
LLM spans carried token counts and the model only under OpenInference keys (llm.token_count.*, llm.model_name). Backends that read the OpenTelemetry GenAI conventions therefore saw a span they could identify as GenAI, via the gen_ai.provider.name already set, but found no usage or model data on it and could not price the call. Mirror the existing values onto the GenAI keys. Nothing is renamed or removed, so OpenInference consumers are unaffected: gen_ai.request.model gen_ai.response.model gen_ai.usage.input_tokens gen_ai.usage.output_tokens gen_ai.usage.cache_read_input_tokens gen_ai.usage.cache_creation_input_tokens The cache keys are spelled out rather than taken from semconv, which emits gen_ai.usage.cache_read.input_tokens. Anthropic's API and the GenAI consumers this targets read the underscored form. The new tests assert the literal strings for the same reason: a constant renamed upstream must fail the suite rather than silently change the wire format. Cache tokens matter for correctness here, not just completeness. A coding agent against a prompt-cached Anthropic model routinely reports thousands of cache-creation tokens against single-digit input tokens, so a consumer mapping only input and output understates cost by orders of magnitude. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Sep 19, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
LLM spans carry token counts and the model only under OpenInference keys
(
llm.token_count.*,llm.model_name). Backends that read the OpenTelemetryGenAI conventions identify the span as GenAI —
gen_ai.provider.nameisalready set — but then find no usage or model data on it, so they cannot
price the call.
Change
Mirror the existing values onto the GenAI keys. Nothing is renamed or removed,
so OpenInference consumers (Arize/Phoenix) are unaffected:
Four come from
@opentelemetry/semantic-conventions(already a dependency).Why the cache keys are string literals
semconv spells these
gen_ai.usage.cache_read.input_tokens(dotted). Anthropic'sAPI and the GenAI consumers this targets read the underscored form, so the
constants would emit attributes nothing reads. The new tests assert the literal
strings for the same reason — a constant renamed upstream should fail the suite
rather than silently change the wire format.
Why cache tokens matter here
Not just completeness. A coding agent against a prompt-cached Anthropic model
routinely reports thousands of cache-creation tokens against single-digit input
tokens — one real session showed
cache_write=16739againstprompt=3. Aconsumer mapping only input/output understates cost by orders of magnitude.
Verification
Tested against a live GenAI backend with a control, 14 seconds apart, identical
token counts — the only difference being attribute naming:
gen_ai.*)unknownThe backend derived the cost from token counts alone (no cost attribute was
sent) and its figure matched this plugin's own
llm.cost.totalto eightdecimal places.
Tests
Three added to
tests/handlers/spans.test.ts; all fail against unpatched source.bun run lint,bun run check:jsdoc-coverage,bun run typecheckall pass.End-to-end verification
Since opening this, I ran the patched plugin against a live GenAI backend and
captured the wire payload through a logging proxy.
The span the plugin emits:
The resulting record on the backend:
Every field round-trips, cache tokens included. The backend derived the cost
from token counts alone — no cost attribute is read from the span — and its
figure matched this plugin's own
llm.cost.totalto eight decimal places.Without the patch the same traffic records as
model: unknown, 0 tokens, $0,matching the control in the table above.