You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The LLM span (gen_ai / llm.completion) stamps only input + output token counts. The prompt-cache token counts forge already parses are dropped from the span, so cache stats aren't visible in traces:
resp.Usage already carries CacheReadInputTokens and CacheCreationInputTokens (added in #431 / merged #432), and the llm_callaudit event already emits them — but the span does not. There are no cache-token attribute constants in forge-core/observability/attrs.go (only AttrGenAIUsageInputTokens / AttrGenAIUsageOutputTokens at :53-54).
A user observing an Anthropic response wants these on the span:
Cache stats invisible in traces. Operators can see input/output on the span but not how much of the input was a cache hit (cache_read) vs a cache write (cache_creation) — the numbers that explain cost on cache-heavy runs.
Span diverges from the audit event. Since fix(usage): capture Anthropic cache tokens + emit total_input_tokens (#431) #432 the llm_call audit event carries cache_read_input_tokens / cache_creation_input_tokens / total_input_tokens, but the span carries none of them. This breaks the documented trace↔audit consistency: docs/security/audit-logging.md states an llm_call row's span_id "resolves to the llm.completion span carrying matching gen_ai.usage.* tokens" — they no longer match.
Fix
Stamp the cache tokens on the LLM span alongside input/output, mirroring the audit-event parity #432 established.
Add attribute constants in forge-core/observability/attrs.go next to AttrGenAIUsageInputTokens (:53-54). OTel GenAI semconv does not (yet) standardize cache-token attributes, so follow forge's existing gen_ai.usage.* prefix:
(optional) AttrGenAIUsageTotalInputTokens = "gen_ai.usage.total_input_tokens" — the bill-from sum, matching the audit event's total_input_tokens.
Stamp them at loop.go:531-533. Use omitempty-style conditionals so non-caching / non-Anthropic calls don't add zero-valued attributes (keep span cardinality/shape stable), matching how the audit emitter treats the cache fields:
Related (secondary — flag, likely a separate follow-up)
Forge parses only the flatcache_read_input_tokens / cache_creation_input_tokens (forge-core/llm/providers/anthropic.go:348-349). The user's example also carries the newer nested breakdown:
i.e. the extended-cache TTL split (5-minute vs 1-hour). Forge does not parse cache_creation.* at all, so even after the span fix the 5m/1h split can't be surfaced. Decide whether v1 stops at the flat counts (recommended — matches the audit event) or also parses the nested TTL breakdown into new UsageInfo fields + span attributes.
Acceptance
LLM span carries gen_ai.usage.cache_read_input_tokens and gen_ai.usage.cache_creation_input_tokens when non-zero.
Non-caching / non-Anthropic calls emit no zero-valued cache attributes (span shape unchanged for them).
Span cache tokens match the llm_call audit event's cache fields for the same call (trace↔audit consistency restored).
loop_spans_test.go asserts the new attributes on a cache-heavy call (extend the existing attr-map assertions at :132-134).
docs/core-concepts/observability-tracing.md span-attribute table + the docs/security/audit-logging.md trace-link note updated.
Problem
The LLM span (
gen_ai/llm.completion) stamps only input + output token counts. The prompt-cache token counts forge already parses are dropped from the span, so cache stats aren't visible in traces:forge-core/runtime/loop.go:531-533resp.Usagealready carriesCacheReadInputTokensandCacheCreationInputTokens(added in #431 / merged #432), and thellm_callaudit event already emits them — but the span does not. There are no cache-token attribute constants inforge-core/observability/attrs.go(onlyAttrGenAIUsageInputTokens/AttrGenAIUsageOutputTokensat:53-54).A user observing an Anthropic response wants these on the span:
Impact
cache_read) vs a cache write (cache_creation) — the numbers that explain cost on cache-heavy runs.llm_callaudit event carriescache_read_input_tokens/cache_creation_input_tokens/total_input_tokens, but the span carries none of them. This breaks the documented trace↔audit consistency:docs/security/audit-logging.mdstates anllm_callrow'sspan_id"resolves to thellm.completionspan carrying matchinggen_ai.usage.*tokens" — they no longer match.Fix
Stamp the cache tokens on the LLM span alongside input/output, mirroring the audit-event parity #432 established.
forge-core/observability/attrs.gonext toAttrGenAIUsageInputTokens(:53-54). OTel GenAI semconv does not (yet) standardize cache-token attributes, so follow forge's existinggen_ai.usage.*prefix:AttrGenAIUsageCacheReadInputTokens = "gen_ai.usage.cache_read_input_tokens"AttrGenAIUsageCacheCreationInputTokens = "gen_ai.usage.cache_creation_input_tokens"AttrGenAIUsageTotalInputTokens = "gen_ai.usage.total_input_tokens"— the bill-from sum, matching the audit event'stotal_input_tokens.loop.go:531-533. Useomitempty-style conditionals so non-caching / non-Anthropic calls don't add zero-valued attributes (keep span cardinality/shape stable), matching how the audit emitter treats the cache fields:Related (secondary — flag, likely a separate follow-up)
Forge parses only the flat
cache_read_input_tokens/cache_creation_input_tokens(forge-core/llm/providers/anthropic.go:348-349). The user's example also carries the newer nested breakdown:i.e. the extended-cache TTL split (5-minute vs 1-hour). Forge does not parse
cache_creation.*at all, so even after the span fix the 5m/1h split can't be surfaced. Decide whether v1 stops at the flat counts (recommended — matches the audit event) or also parses the nested TTL breakdown into newUsageInfofields + span attributes.Acceptance
gen_ai.usage.cache_read_input_tokensandgen_ai.usage.cache_creation_input_tokenswhen non-zero.llm_callaudit event's cache fields for the same call (trace↔audit consistency restored).loop_spans_test.goasserts the new attributes on a cache-heavy call (extend the existing attr-map assertions at:132-134).docs/core-concepts/observability-tracing.mdspan-attribute table + thedocs/security/audit-logging.mdtrace-link note updated.Files
forge-core/observability/attrs.go:53-54neighborhood).forge-core/runtime/loop.go:531-533).forge-core/runtime/loop_spans_test.go:132-134).docs/core-concepts/observability-tracing.md,docs/security/audit-logging.mdReferences
UsageInfo+ thellm_callaudit event +total_input_tokens. This issue closes the parallel span gap that was explicitly scoped out of that work.