Skip to content

feat(ai): emit cost + full usage on otel spans#747

Open
season179 wants to merge 1 commit into
TanStack:mainfrom
season179:feat/otel-full-usage-emission
Open

feat(ai): emit cost + full usage on otel spans#747
season179 wants to merge 1 commit into
TanStack:mainfrom
season179:feat/otel-full-usage-emission

Conversation

@season179

@season179 season179 commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #721.

otelMiddleware only emitted gen_ai.usage.input_tokens / gen_ai.usage.output_tokens, even though TokenUsage already carries provider-reported cost, total tokens, cache/reasoning breakdowns, and duration-based billing (the cost fields landed in #654). Backends like PostHog had to re-derive cost from tokens × their own price table, losing cache discounts and gateway markup (OpenRouter), and duration-billed activities had no cost signal at all.

What changed

A shared usageAttributes() helper now builds the full attribute set at all three emission sites (RUN_FINISHED chunk, onUsage, onFinish rollup). Every field is guarded, so spans are unchanged when a provider doesn't report it:

Attribute Source Convention
gen_ai.usage.total_tokens totalTokens de-facto (PostHog/LiteLLM consume directly)
gen_ai.usage.cost cost de-facto (PostHog consumes directly)
gen_ai.usage.cache_read.input_tokens promptTokensDetails.cachedTokens official GenAI semconv
gen_ai.usage.cache_creation.input_tokens promptTokensDetails.cacheWriteTokens official GenAI semconv
gen_ai.usage.reasoning.output_tokens completionTokensDetails.reasoningTokens official GenAI semconv
tanstack.ai.usage.duration_seconds durationSeconds TanStack-namespaced (no semconv)
tanstack.ai.usage.upstream_cost / _input_cost / _output_cost costDetails TanStack-namespaced (no semconv)

Deliberately out of scope: unitsBilled and per-modality token breakdowns (media activities don't flow through chat middleware — that's #720), and providerUsageDetails (provider-shaped bag, unsafe to spread onto spans).

Tests

  • Unit: 5 new cases in packages/ai/tests/middlewares/otel.test.ts covering all three emission sites, absent-field omission, and empty detail objects.
  • E2E: new /api/otel-usage route drives the existing openai-usage-details (cache/reasoning/totals) and openrouter-cost (cost/cost split) aimock mounts through otelMiddleware with an in-memory capture tracer; middleware.spec.ts asserts the attributes land on both iteration and root spans.
  • Docs: attribute table + naming-convention note in docs/advanced/otel.md.

pnpm test:pr and the full middleware E2E suite pass locally.

Summary by CodeRabbit

  • New Features

    • OpenTelemetry spans now emit comprehensive usage attributes when providers supply them: total tokens, cost (including upstream/gateway splits), cache/read/write and reasoning token details, and duration-based billing.
  • Documentation

    • Expanded OpenTelemetry docs to describe the new usage attributes, naming conventions, and conditional emission based on provider support.
  • Tests / E2E

    • Added end-to-end route and tests validating full usage emission and aggregation across iteration and root spans.

@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b9ca787f-bda8-4131-a04f-748c16ae5a70

📥 Commits

Reviewing files that changed from the base of the PR and between 10d8c42 and c7df2a3.

📒 Files selected for processing (8)
  • .changeset/otel-full-usage-emission.md
  • docs/advanced/otel.md
  • docs/config.json
  • packages/ai/src/middlewares/otel.ts
  • packages/ai/tests/middlewares/otel.test.ts
  • testing/e2e/src/routeTree.gen.ts
  • testing/e2e/src/routes/api.otel-usage.ts
  • testing/e2e/tests/middleware.spec.ts
✅ Files skipped from review due to trivial changes (3)
  • docs/config.json
  • docs/advanced/otel.md
  • testing/e2e/src/routeTree.gen.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • .changeset/otel-full-usage-emission.md
  • packages/ai/tests/middlewares/otel.test.ts
  • testing/e2e/tests/middleware.spec.ts
  • packages/ai/src/middlewares/otel.ts
  • testing/e2e/src/routes/api.otel-usage.ts

📝 Walkthrough

Walkthrough

otelMiddleware now emits complete provider-reported token usage, cost, cache/reasoning token breakdowns, and duration-based billing attributes to OpenTelemetry spans. A new usageAttributes() helper centralizes attribute construction and is called at three span lifecycle points. Unit tests and E2E validation cover OpenAI and OpenRouter semantics.

Changes

OpenTelemetry full usage span attributes

Layer / File(s) Summary
Full usage attributes helper
packages/ai/src/middlewares/otel.ts
New usageAttributes(TokenUsage) helper builds complete gen_ai.usage.* span attributes including optional cost, total tokens, cache/reasoning breakdowns, duration, and upstream cost fields. Integrated at three points: onChunk (RUN_FINISHED), onUsage, and onFinish.
Unit test coverage for full usage
packages/ai/tests/middlewares/otel.test.ts
New test suite with fullUsage fixture and expectFullUsageAttrs assertion helper. Tests emit full attributes from both onChunk and onUsage, validate root span aggregation on onFinish, and assert optional attributes are omitted when providers do not report them.
E2E API endpoint and tracer capture
testing/e2e/src/routes/api.otel-usage.ts, testing/e2e/src/routeTree.gen.ts
New POST /api/otel-usage endpoint with provider selection (OpenAI/OpenRouter). Custom in-memory span capture tracer records start/end/attribute events for assertion. Route tree generated with full registration across type interfaces and module augmentations.
End-to-end span emission tests
testing/e2e/tests/middleware.spec.ts
E2E tests validate OpenAI (total, cached, reasoning tokens) and OpenRouter (cost, upstream cost breakdown) span attributes on iteration and root spans.
Documentation and metadata
.changeset/otel-full-usage-emission.md, docs/advanced/otel.md, docs/config.json
Changeset, expanded attribute reference table, conditional emission clarification, and config timestamp update.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • TanStack/ai#654: Establishes provider-reported usage.cost and usage.costDetails normalization that this PR emits to spans via the new usageAttributes helper.

Suggested reviewers

  • tombeckenham
  • AlemTuzlak

Poem

🐰 Spans now bloom with tokens and cost,
Cache discounts no longer lost,
From OpenAI dreams to Router streams—
Each usage detail flows like moonbeams!
The tracer captures, tests will dance,
Full usage given its rightful chance. 🌙

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(ai): emit cost + full usage on otel spans' clearly and concisely summarizes the main change: extending otelMiddleware to emit additional usage attributes beyond input/output tokens.
Description check ✅ Passed The PR description thoroughly covers what changed, provides a detailed attribute mapping table, explains the motivation, documents tests (unit, E2E, docs), and includes implementation details. It follows a well-organized format.
Linked Issues check ✅ Passed The PR fully addresses issue #721 by implementing all proposed emission points: cost, costDetails, totalTokens, cache/reasoning details, and durationSeconds. All optional fields are guarded, matching the issue's requirements.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing full usage emission on otel spans. Deliberately out-of-scope items (unitsBilled, per-modality breakdowns, providerUsageDetails) are explicitly excluded. Documentation and test updates support the core feature.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install timed out. The project may have too many dependencies for the sandbox.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@season179 season179 marked this pull request as draft June 11, 2026 15:33
…g 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 TanStack#721
@season179 season179 force-pushed the feat/otel-full-usage-emission branch from 10d8c42 to c7df2a3 Compare June 11, 2026 15:45
@season179 season179 marked this pull request as ready for review June 11, 2026 15:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(otel): emit cost + full usage on spans (gen_ai.usage.cost, total_tokens, cache/reasoning details, duration)

1 participant