Skip to content

fix(ai): report finalization-call usage on synthesized RUN_FINISHED#640

Open
tombeckenham wants to merge 1 commit into
mainfrom
fix/structured-output-finalization-usage
Open

fix(ai): report finalization-call usage on synthesized RUN_FINISHED#640
tombeckenham wants to merge 1 commit into
mainfrom
fix/structured-output-finalization-usage

Conversation

@tombeckenham
Copy link
Copy Markdown
Contributor

@tombeckenham tombeckenham commented May 26, 2026

Summary

Follow-up to #609. Adapters without a native structuredOutputStream (Anthropic, Gemini, Ollama, OpenRouter) run agentic structured output through the legacy finalization round-trip, but that extra call's token usage never reached the chat middleware onUsage hook — any cost-tracking middleware silently under-counted by exactly one call per structured-output request.

  • Each affected adapter now returns usage from structuredOutput(), sourced from the provider's response (Anthropic usage.input_tokens/output_tokens, Gemini usageMetadata, Ollama prompt_eval_count/eval_count, OpenRouter normalized usage).
  • fallbackStructuredOutputStream forwards usage onto the synthesized RUN_FINISHED event so the chat middleware accounts for the finalization call.
  • StructuredOutputResult gains an optional usage: { promptTokens, completionTokens, totalTokens } field. When the provider doesn't return tokens (or the call fails before usage is known) the field stays undefined, which the engine already treats as "no usage to report" — no consumer-visible change beyond accurate onUsage totals.

Test plan

  • pnpm --filter @tanstack/ai --filter @tanstack/ai-anthropic --filter @tanstack/ai-gemini --filter @tanstack/ai-ollama --filter @tanstack/ai-openrouter test:types (verified locally)
  • pnpm test:lib for the affected packages
  • Manual: run a structured-output request through Anthropic/Gemini/Ollama/OpenRouter with an onUsage middleware and confirm the finalization call's tokens are included

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Token usage metrics are now properly reported in structured output operations across all supported AI providers (Anthropic, Gemini, Ollama, and OpenRouter).
    • Chat middleware onUsage hooks now receive accurate token count totals, eliminating previous under-reporting in certain adapter paths.
    • Usage data is now consistently surfaced in structured output results, enabling proper cost tracking and usage analytics.

Review Change Stack

Adapters without a native `structuredOutputStream` (Anthropic, Gemini,
Ollama, OpenRouter) ran agentic structured output through the legacy
finalization round-trip, and that extra call's token usage never reached
the chat middleware `onUsage` hook — any cost-tracking middleware
silently under-counted by exactly one call per request.

Adapters now return `usage` from `structuredOutput()`, and
`fallbackStructuredOutputStream` forwards it onto the synthesized
RUN_FINISHED event. `StructuredOutputResult` gains an optional
`usage` field; adapters that can't report it leave it undefined,
which the engine treats as "no usage to report" — same as before.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 26, 2026

📝 Walkthrough

Walkthrough

Token usage from provider APIs is now exposed in the non-streaming structuredOutput() path. Four provider adapters extract token counts from their respective responses, the core interface gains an optional usage field, and the streaming fallback forwards that usage into the RUN_FINISHED event so middleware receives accurate totals.

Changes

Structured Output Token Usage

Layer / File(s) Summary
StructuredOutputResult contract extension
packages/typescript/ai/src/activities/chat/adapter.ts
Interface adds optional usage?: { promptTokens: number; completionTokens: number; totalTokens: number } field to surface provider-reported token counts from structured output calls.
Provider adapter usage extraction
packages/typescript/ai-anthropic/src/adapters/text.ts, packages/typescript/ai-gemini/src/adapters/text.ts, packages/typescript/ai-ollama/src/adapters/text.ts, packages/typescript/ai-openrouter/src/adapters/text.ts
Each adapter's structuredOutput() now extracts token counts from provider response (Anthropic: input_tokens/output_tokens; Gemini: usageMetadata.*TokenCount; Ollama: prompt_eval_count/eval_count; OpenRouter: usage) and populates the usage field with normalized promptTokens, completionTokens, and totalTokens.
Streaming fallback usage forwarding
packages/typescript/ai/src/activities/chat/index.ts
fallbackStructuredOutputStream now expands the result type to include optional usage and forwards result.usage into the synthetic RUN_FINISHED event emission, ensuring chat middleware onUsage hook receives correct totals instead of being under-counted for adapters using the legacy finalization path.
Release documentation
.changeset/structured-output-finalization-usage.md
Changeset documents patch releases and the behavioral fix for correct onUsage accounting from the structured output path.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

  • TanStack/ai#600: Both PRs touch the chat structured-output finalization/streaming path—retrieved PR changes the structured-output middleware lifecycle and RUN_STARTED/RUN_FINISHED suppression/onUsage semantics, and the main PR further fixes fallbackStructuredOutputStream to forward adapter StructuredOutputResult.usage into the synthesized RUN_FINISHED so onUsage totals are correct.

Suggested reviewers

  • AlemTuzlak

Poem

🐰 A token's tale of usage true,
From adapters four, the counts shine through,
Now streaming falls with numbers bright,
The onUsage hook sees totals right! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed Title clearly summarizes the main fix: reporting finalization-call usage in synthesized RUN_FINISHED events for structured output adapters.
Description check ✅ Passed Description covers changes comprehensively with context, detailed implementation notes, and test plan, though some test items are unchecked.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/structured-output-finalization-usage

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 failed: lockfile failed supply-chain policy check. Run pnpm install locally to update the lockfile.


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.

@github-actions
Copy link
Copy Markdown
Contributor

🚀 Changeset Version Preview

11 package(s) bumped directly, 16 bumped as dependents.

🟥 Major bumps

Package Version Reason
@tanstack/ai-elevenlabs 0.2.11 → 1.0.0 Dependent
@tanstack/ai-openai 0.10.1 → 1.0.0 Dependent
@tanstack/ai-react-ui 0.8.1 → 1.0.0 Dependent
@tanstack/ai-solid-ui 0.7.1 → 1.0.0 Dependent

🟨 Minor bumps

Package Version Reason
@tanstack/ai-client 0.11.8 → 0.12.0 Changeset
@tanstack/ai-react 0.11.8 → 0.12.0 Changeset

🟩 Patch bumps

Package Version Reason
@tanstack/ai 0.22.0 → 0.22.1 Changeset
@tanstack/ai-anthropic 0.11.0 → 0.11.1 Changeset
@tanstack/ai-gemini 0.11.0 → 0.11.1 Changeset
@tanstack/ai-ollama 0.6.22 → 0.6.23 Changeset
@tanstack/ai-openrouter 0.9.8 → 0.9.9 Changeset
@tanstack/ai-preact 0.6.33 → 0.6.34 Changeset
@tanstack/ai-solid 0.10.8 → 0.10.9 Changeset
@tanstack/ai-svelte 0.10.8 → 0.10.9 Changeset
@tanstack/ai-vue 0.10.9 → 0.10.10 Changeset
@tanstack/ai-code-mode 0.1.21 → 0.1.22 Dependent
@tanstack/ai-code-mode-skills 0.1.21 → 0.1.22 Dependent
@tanstack/ai-devtools-core 0.3.38 → 0.3.39 Dependent
@tanstack/ai-event-client 0.3.11 → 0.3.12 Dependent
@tanstack/ai-fal 0.7.14 → 0.7.15 Dependent
@tanstack/ai-isolate-cloudflare 0.2.12 → 0.2.13 Dependent
@tanstack/ai-isolate-node 0.1.21 → 0.1.22 Dependent
@tanstack/ai-isolate-quickjs 0.1.21 → 0.1.22 Dependent
@tanstack/ai-vue-ui 0.2.4 → 0.2.5 Dependent
@tanstack/preact-ai-devtools 0.1.42 → 0.1.43 Dependent
@tanstack/react-ai-devtools 0.2.42 → 0.2.43 Dependent
@tanstack/solid-ai-devtools 0.2.42 → 0.2.43 Dependent

@nx-cloud
Copy link
Copy Markdown

nx-cloud Bot commented May 26, 2026

View your CI Pipeline Execution ↗ for commit 554d31e

Command Status Duration Result
nx run-many --targets=build --exclude=examples/... ✅ Succeeded 1m 6s View ↗

☁️ Nx Cloud last updated this comment at 2026-05-26 04:24:25 UTC

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 26, 2026

Open in StackBlitz

@tanstack/ai

npm i https://pkg.pr.new/@tanstack/ai@640

@tanstack/ai-anthropic

npm i https://pkg.pr.new/@tanstack/ai-anthropic@640

@tanstack/ai-client

npm i https://pkg.pr.new/@tanstack/ai-client@640

@tanstack/ai-code-mode

npm i https://pkg.pr.new/@tanstack/ai-code-mode@640

@tanstack/ai-code-mode-skills

npm i https://pkg.pr.new/@tanstack/ai-code-mode-skills@640

@tanstack/ai-devtools-core

npm i https://pkg.pr.new/@tanstack/ai-devtools-core@640

@tanstack/ai-elevenlabs

npm i https://pkg.pr.new/@tanstack/ai-elevenlabs@640

@tanstack/ai-event-client

npm i https://pkg.pr.new/@tanstack/ai-event-client@640

@tanstack/ai-fal

npm i https://pkg.pr.new/@tanstack/ai-fal@640

@tanstack/ai-gemini

npm i https://pkg.pr.new/@tanstack/ai-gemini@640

@tanstack/ai-grok

npm i https://pkg.pr.new/@tanstack/ai-grok@640

@tanstack/ai-groq

npm i https://pkg.pr.new/@tanstack/ai-groq@640

@tanstack/ai-isolate-cloudflare

npm i https://pkg.pr.new/@tanstack/ai-isolate-cloudflare@640

@tanstack/ai-isolate-node

npm i https://pkg.pr.new/@tanstack/ai-isolate-node@640

@tanstack/ai-isolate-quickjs

npm i https://pkg.pr.new/@tanstack/ai-isolate-quickjs@640

@tanstack/ai-ollama

npm i https://pkg.pr.new/@tanstack/ai-ollama@640

@tanstack/ai-openai

npm i https://pkg.pr.new/@tanstack/ai-openai@640

@tanstack/ai-openrouter

npm i https://pkg.pr.new/@tanstack/ai-openrouter@640

@tanstack/ai-preact

npm i https://pkg.pr.new/@tanstack/ai-preact@640

@tanstack/ai-react

npm i https://pkg.pr.new/@tanstack/ai-react@640

@tanstack/ai-react-ui

npm i https://pkg.pr.new/@tanstack/ai-react-ui@640

@tanstack/ai-solid

npm i https://pkg.pr.new/@tanstack/ai-solid@640

@tanstack/ai-solid-ui

npm i https://pkg.pr.new/@tanstack/ai-solid-ui@640

@tanstack/ai-svelte

npm i https://pkg.pr.new/@tanstack/ai-svelte@640

@tanstack/ai-utils

npm i https://pkg.pr.new/@tanstack/ai-utils@640

@tanstack/ai-vue

npm i https://pkg.pr.new/@tanstack/ai-vue@640

@tanstack/ai-vue-ui

npm i https://pkg.pr.new/@tanstack/ai-vue-ui@640

@tanstack/openai-base

npm i https://pkg.pr.new/@tanstack/openai-base@640

@tanstack/preact-ai-devtools

npm i https://pkg.pr.new/@tanstack/preact-ai-devtools@640

@tanstack/react-ai-devtools

npm i https://pkg.pr.new/@tanstack/react-ai-devtools@640

@tanstack/solid-ai-devtools

npm i https://pkg.pr.new/@tanstack/solid-ai-devtools@640

commit: 554d31e

@tombeckenham tombeckenham requested a review from a team May 26, 2026 04:25
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/typescript/ai-anthropic/src/adapters/text.ts`:
- Around line 279-288: The code currently forces a usage object with zeros by
defaulting response.usage fields to 0; change it to only include the usage
property when response.usage is present so unknown usage remains unknown. In the
function returning { data: parsed, rawText, usage: ... }, check if
response.usage != null (or typeof response.usage !== 'undefined') and only then
compute inputTokens = response.usage.input_tokens, outputTokens =
response.usage.output_tokens and return usage:{ promptTokens: inputTokens,
completionTokens: outputTokens, totalTokens: inputTokens + outputTokens };
otherwise omit the usage field (or set it to undefined).

In `@packages/typescript/ai-ollama/src/adapters/text.ts`:
- Around line 204-214: The current return always sets usage with promptTokens
and completionTokens defaulted to 0, collapsing "not reported" into zero; change
the construction in the adapter (around where parsed/rawText are returned) to
preserve optional semantics by using the raw response fields
(response.prompt_eval_count and response.eval_count) without defaulting to 0 and
only include the usage object when at least one of those fields is defined—i.e.,
set promptTokens = response.prompt_eval_count (no ?? 0), completionTokens =
response.eval_count (no ?? 0) and omit or set usage to undefined when both are
undefined so middleware sees "not reported" instead of 0.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7b982479-33e3-4669-a73f-883798bcffdd

📥 Commits

Reviewing files that changed from the base of the PR and between ad23da9 and 554d31e.

📒 Files selected for processing (7)
  • .changeset/structured-output-finalization-usage.md
  • packages/typescript/ai-anthropic/src/adapters/text.ts
  • packages/typescript/ai-gemini/src/adapters/text.ts
  • packages/typescript/ai-ollama/src/adapters/text.ts
  • packages/typescript/ai-openrouter/src/adapters/text.ts
  • packages/typescript/ai/src/activities/chat/adapter.ts
  • packages/typescript/ai/src/activities/chat/index.ts

Comment on lines +279 to +288
const inputTokens = response.usage?.input_tokens ?? 0
const outputTokens = response.usage?.output_tokens ?? 0
return {
data: parsed,
rawText,
usage: {
promptTokens: inputTokens,
completionTokens: outputTokens,
totalTokens: inputTokens + outputTokens,
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid forcing zero-usage when provider usage is missing.

Line 279–Line 288 always emits a usage object by defaulting to 0, which changes “unknown usage” into “known zero usage.” That can incorrectly fire onUsage with zero totals.

Proposed fix
-      const inputTokens = response.usage?.input_tokens ?? 0
-      const outputTokens = response.usage?.output_tokens ?? 0
+      const inputTokens = response.usage?.input_tokens
+      const outputTokens = response.usage?.output_tokens
       return {
         data: parsed,
         rawText,
-        usage: {
-          promptTokens: inputTokens,
-          completionTokens: outputTokens,
-          totalTokens: inputTokens + outputTokens,
-        },
+        ...(inputTokens !== undefined && outputTokens !== undefined
+          ? {
+              usage: {
+                promptTokens: inputTokens,
+                completionTokens: outputTokens,
+                totalTokens: inputTokens + outputTokens,
+              },
+            }
+          : {}),
       }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/typescript/ai-anthropic/src/adapters/text.ts` around lines 279 -
288, The code currently forces a usage object with zeros by defaulting
response.usage fields to 0; change it to only include the usage property when
response.usage is present so unknown usage remains unknown. In the function
returning { data: parsed, rawText, usage: ... }, check if response.usage != null
(or typeof response.usage !== 'undefined') and only then compute inputTokens =
response.usage.input_tokens, outputTokens = response.usage.output_tokens and
return usage:{ promptTokens: inputTokens, completionTokens: outputTokens,
totalTokens: inputTokens + outputTokens }; otherwise omit the usage field (or
set it to undefined).

Comment on lines +204 to 214
const promptTokens = response.prompt_eval_count ?? 0
const completionTokens = response.eval_count ?? 0
return {
data: parsed,
rawText,
usage: {
promptTokens,
completionTokens,
totalTokens: promptTokens + completionTokens,
},
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve optional usage semantics instead of defaulting to zeros.

Line 204–Line 214 always returns usage, even when token counters are absent. That collapses “not reported” into 0 and can skew usage middleware behavior.

Proposed fix
-      const promptTokens = response.prompt_eval_count ?? 0
-      const completionTokens = response.eval_count ?? 0
+      const promptTokens = response.prompt_eval_count
+      const completionTokens = response.eval_count
       return {
         data: parsed,
         rawText,
-        usage: {
-          promptTokens,
-          completionTokens,
-          totalTokens: promptTokens + completionTokens,
-        },
+        ...(promptTokens !== undefined && completionTokens !== undefined
+          ? {
+              usage: {
+                promptTokens,
+                completionTokens,
+                totalTokens: promptTokens + completionTokens,
+              },
+            }
+          : {}),
       }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/typescript/ai-ollama/src/adapters/text.ts` around lines 204 - 214,
The current return always sets usage with promptTokens and completionTokens
defaulted to 0, collapsing "not reported" into zero; change the construction in
the adapter (around where parsed/rawText are returned) to preserve optional
semantics by using the raw response fields (response.prompt_eval_count and
response.eval_count) without defaulting to 0 and only include the usage object
when at least one of those fields is defined—i.e., set promptTokens =
response.prompt_eval_count (no ?? 0), completionTokens = response.eval_count (no
?? 0) and omit or set usage to undefined when both are undefined so middleware
sees "not reported" instead of 0.

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.

2 participants