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
feat(ai): emit full usage on otel spans (cost, totals, cache/reasoning details)
otelMiddleware only emitted gen_ai.usage.input_tokens/output_tokens even
though TokenUsage already carries provider-reported cost, total tokens,
cache/reasoning breakdowns, and duration-based billing. Backends like
PostHog had to re-derive cost from their own price tables, losing cache
discounts and gateway markup (OpenRouter), and duration-billed activities
had no cost signal at all.
A shared usageAttributes() helper now builds the full guarded attribute
set at all three emission sites (RUN_FINISHED chunk, onUsage, onFinish
rollup):
- gen_ai.usage.total_tokens / gen_ai.usage.cost (de-facto extensions
consumed directly by PostHog and LiteLLM-style backends)
- gen_ai.usage.cache_read.input_tokens, cache_creation.input_tokens,
reasoning.output_tokens (official GenAI semconv names)
- tanstack.ai.usage.duration_seconds and the upstream cost split
(no semconv equivalent exists)
E2E: new /api/otel-usage route drives the existing openai-usage-details
and openrouter-cost aimock mounts through otelMiddleware with a local
capture tracer; middleware.spec.ts asserts the attributes land on
iteration and root spans.
Fixes#721
`otelMiddleware` now emits the rest of the reported `TokenUsage` on spans instead of only input/output tokens (#721). When the provider reports them, spans carry `gen_ai.usage.total_tokens`, `gen_ai.usage.cost` (provider-reported cost — cache discounts and gateway markup included, so backends like PostHog no longer re-derive cost from price tables), the official semconv cache/reasoning breakdowns (`gen_ai.usage.cache_read.input_tokens`, `gen_ai.usage.cache_creation.input_tokens`, `gen_ai.usage.reasoning.output_tokens`), and TanStack-namespaced attributes for duration-based billing (`tanstack.ai.usage.duration_seconds`) and the upstream cost split (`tanstack.ai.usage.upstream_cost` / `upstream_input_cost` / `upstream_output_cost`). All attributes are guarded — spans stay unchanged when a provider doesn't report a field. Media-oriented fields (`unitsBilled`, per-modality token breakdowns) and the provider-shaped `providerUsageDetails` bag are intentionally not emitted; media-activity observability is tracked in #720.
Usage attributes beyond input/output tokens are emitted only when the provider reports them, so spans stay clean otherwise. Cache and reasoning breakdowns use the official GenAI semconv names; `gen_ai.usage.cost` and `gen_ai.usage.total_tokens` are de-facto extensions consumed directly by backends like PostHog — without them, backends re-derive cost from their own price tables and lose cache discounts and gateway markup. Fields with no established convention (duration-based billing, the upstream cost split) are TanStack-namespaced.
0 commit comments