Thanks to visit codestin.com
Credit goes to github.com

Skip to content

fix(ai): forward token usage on the structured-output fallback path#789

Merged
AlemTuzlak merged 2 commits into
TanStack:mainfrom
season179:fix/758-fallback-structured-output-usage
Jun 19, 2026
Merged

fix(ai): forward token usage on the structured-output fallback path#789
AlemTuzlak merged 2 commits into
TanStack:mainfrom
season179:fix/758-fallback-structured-output-usage

Conversation

@season179

@season179 season179 commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #758. Token usage was being dropped on the structured-output fallback path, so RUN_FINISHED.usage came back undefined and the engine's runOnUsage middleware hook never fired — cost-tracking and observability layers reported zero tokens.

Root cause

chat({ outputSchema, stream: true }) resolves the schema in one of two ways:

  • Native — the adapter's structuredOutputStream, or the combined tools+schema path. These emit usage on their RUN_FINISHED.
  • FallbackfallbackStructuredOutputStream, used when neither is available. It wraps the non-streaming structuredOutput() and synthesizes the AG-UI lifecycle.

The fallback's structuredOutput() result does carry usage, but the synthesized RUN_FINISHED never copied it over:

let result: { data: unknown; rawText: string }   // narrowed type hid `usage`
// ...
yield { type: EventType.RUN_FINISHED, /* …no usage… */ }

This affects any provider/model that takes the fallback: Ollama, plus Anthropic and Gemini models that predate combined tools+schema support (Claude 4.5+ and Gemini 3.x use the native combined path and were unaffected).

Fix

Widen the local type to the adapter's real return type (StructuredOutputResult<unknown>, which already declares usage?) and forward it onto RUN_FINISHED with a conditional spread so adapters that don't report usage don't get a spurious usage: undefined:

let result: StructuredOutputResult<unknown>
// ...
yield {
  type: EventType.RUN_FINISHED,
  // …
  ...(result.usage ? { usage: result.usage } : {}),
}

This brings the fallback path to parity with the native streaming path (e.g. openai-base already emits usage on RUN_FINISHED the same way).

Testing

  • Unit (packages/ai/tests/chat-structured-output-stream.test.ts): a regression test asserting RUN_FINISHED.usage is forwarded, and a companion test asserting the key is omitted when the adapter reports no usage.
  • E2E (testing/e2e/.../anthropic-structured-usage): drives the Anthropic adapter (claude-opus-4-1, a pre-combined model, to force the fallback) through chat({ outputSchema, stream: true }) against an aimock mount whose tool-forced structured_output response carries input_tokens / output_tokens / cache_read_input_tokens, and asserts the normalized usage reaches RUN_FINISHED.usage end-to-end.
  • Full @tanstack/ai unit suite (1045) and the e2e spec pass locally; types and lint are clean.

A changeset is included (@tanstack/ai patch).

Summary by CodeRabbit

  • Bug Fixes
    • Fixed token usage tracking for structured output streaming with adapters that resolve schemas via non-streaming methods (e.g., Ollama, certain Anthropic/Gemini models). Token counts are now properly reported and available to observability middleware, resolving previously reported zero/undefined usage values.

`fallbackStructuredOutputStream` — used by `chat({ outputSchema, stream: true })`
whenever an adapter resolves the schema through the non-streaming
`structuredOutput()` rather than a native streaming or combined path (Ollama,
plus Anthropic and Gemini models that predate combined tools+schema support) —
wrapped `structuredOutput()` but dropped the `usage` from its result. Consumers
reading `RUN_FINISHED.usage` saw `undefined`, and the engine's `runOnUsage`
middleware hook (gated on `chunk.usage`) never fired, so cost-tracking and
observability layers reported zero token counts on that path.

The synthesized `RUN_FINISHED` now carries the adapter-reported `usage`,
matching the native streaming path. Adapters that don't report usage are
unaffected — the conditional spread omits the key entirely.

Adds unit coverage in chat-structured-output-stream.test.ts (usage forwarded;
omitted when absent) and an e2e regression (anthropic-structured-usage) that
drives the Anthropic adapter through the fallback against an aimock mount and
asserts usage reaches RUN_FINISHED.usage.
@coderabbitai

coderabbitai Bot commented Jun 19, 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: 8833cb92-c23e-4d6e-a862-e89e4df36b02

📥 Commits

Reviewing files that changed from the base of the PR and between d69f3a9 and fc9fc33.

📒 Files selected for processing (1)
  • packages/ai/tests/chat-structured-output-stream.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/ai/tests/chat-structured-output-stream.test.ts

📝 Walkthrough

Walkthrough

Fixes fallbackStructuredOutputStream in @tanstack/ai to forward adapter-reported usage onto the synthesized RUN_FINISHED event. Adds unit tests covering both the presence and absence of usage, plus an E2E regression suite with a new mock Anthropic endpoint and a dedicated route that streams structured output and captures RUN_FINISHED.usage.

Changes

Structured-output fallback usage fix and tests

Layer / File(s) Summary
Core usage forwarding in fallbackStructuredOutputStream
packages/ai/src/activities/chat/index.ts, .changeset/fallback-structured-output-usage.md
Widens result to StructuredOutputResult<unknown> and conditionally spreads result.usage onto the yielded RUN_FINISHED event; updates adapter type imports. Changeset entry documents the patch.
Unit regression tests for fallback usage
packages/ai/tests/chat-structured-output-stream.test.ts
Extends makeAdapter's structuredOutput return type with optional usage?: TokenUsage; adds two tests verifying RUN_FINISHED.usage is forwarded when present and the key is omitted entirely when absent.
E2E mock, route handler, route tree, and regression spec
testing/e2e/global-setup.ts, testing/e2e/src/routes/api.anthropic-structured-usage.ts, testing/e2e/src/routeTree.gen.ts, testing/e2e/tests/anthropic-structured-usage.spec.ts
Adds an Anthropic mock mount returning tool_use JSON with usage fields; a POST route that streams structured output and captures RUN_FINISHED.usage; generated route tree registrations; and an E2E spec asserting prompt/completion/cached token counts.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related issues

  • #758 — This PR directly fixes the described root cause: fallbackStructuredOutputStream now forwards result.usage to the RUN_FINISHED yield, resolving the undefined usage reported by consumers of the fallback streaming path.

Possibly related PRs

  • TanStack/ai#747: Adds OpenTelemetry middleware that reads chunk.usage from RUN_FINISHED to populate span attributes — exactly the consumer that was broken by the dropped usage on the fallback path this PR fixes.

Suggested reviewers

  • AlemTuzlak

Poem

🐇 Hop hop, the tokens were lost in the stream,
The fallback forgot them — a developer's bad dream.
Now result.usage rides RUN_FINISHED along,
No zero-count ghosts, and no tracking gone wrong.
The rabbit checks usage and wiggles with glee! 🎉

🚥 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 PR title clearly summarizes the main fix: forwarding token usage on the structured-output fallback path, which is the core change across multiple files.
Description check ✅ Passed The PR description thoroughly covers the problem (token usage dropped on fallback path), root cause (narrowed type hiding usage field), fix (widen type and forward usage), and testing approach (unit and e2e regression tests).
Linked Issues check ✅ Passed The PR fully addresses issue #758 by implementing the suggested fix: forwarding result.usage to both RUN_FINISHED and structured-output.complete events with proper type widening and conditional spread to avoid spurious undefined values.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the fallback structured output usage issue: implementation fix, unit tests, e2e test setup/route/test, changeset, and generated routing artifacts. No unrelated modifications detected.

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

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

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.

@nx-cloud

nx-cloud Bot commented Jun 19, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit fc9fc33

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

☁️ Nx Cloud last updated this comment at 2026-06-19 12:32:16 UTC

@nx-cloud

nx-cloud Bot commented Jun 19, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit fc9fc33


☁️ Nx Cloud last updated this comment at 2026-06-19 08:25:46 UTC

@pkg-pr-new

pkg-pr-new Bot commented Jun 19, 2026

Copy link
Copy Markdown

Open in StackBlitz

@tanstack/ai

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

@tanstack/ai-angular

npm i https://pkg.pr.new/@tanstack/ai-angular@789

@tanstack/ai-anthropic

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

@tanstack/ai-client

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

@tanstack/ai-code-mode

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

@tanstack/ai-code-mode-skills

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

@tanstack/ai-devtools-core

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

@tanstack/ai-elevenlabs

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

@tanstack/ai-event-client

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

@tanstack/ai-fal

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

@tanstack/ai-gemini

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

@tanstack/ai-grok

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

@tanstack/ai-groq

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

@tanstack/ai-isolate-cloudflare

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

@tanstack/ai-isolate-node

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

@tanstack/ai-isolate-quickjs

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

@tanstack/ai-mcp

npm i https://pkg.pr.new/@tanstack/ai-mcp@789

@tanstack/ai-ollama

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

@tanstack/ai-openai

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

@tanstack/ai-openrouter

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

@tanstack/ai-preact

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

@tanstack/ai-react

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

@tanstack/ai-react-ui

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

@tanstack/ai-solid

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

@tanstack/ai-solid-ui

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

@tanstack/ai-svelte

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

@tanstack/ai-utils

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

@tanstack/ai-vue

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

@tanstack/ai-vue-ui

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

@tanstack/openai-base

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

@tanstack/preact-ai-devtools

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

@tanstack/react-ai-devtools

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

@tanstack/solid-ai-devtools

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

commit: fc9fc33

@AlemTuzlak AlemTuzlak merged commit 18e5f4d into TanStack:main Jun 19, 2026
10 checks passed
@github-actions github-actions Bot mentioned this pull request Jun 19, 2026
@season179 season179 deleted the fix/758-fallback-structured-output-usage branch June 19, 2026 13:27
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.

fallbackStructuredOutputStream drops usage from structured-output.complete and RUN_FINISHED chunks

2 participants