fix(ai): forward token usage on the structured-output fallback path#789
Conversation
`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.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughFixes ChangesStructured-output fallback usage fix and tests
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Possibly related issues
Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
|
View your CI Pipeline Execution ↗ for commit fc9fc33
☁️ Nx Cloud last updated this comment at |
|
View your CI Pipeline Execution ↗ for commit fc9fc33 ☁️ Nx Cloud last updated this comment at |
@tanstack/ai
@tanstack/ai-angular
@tanstack/ai-anthropic
@tanstack/ai-client
@tanstack/ai-code-mode
@tanstack/ai-code-mode-skills
@tanstack/ai-devtools-core
@tanstack/ai-elevenlabs
@tanstack/ai-event-client
@tanstack/ai-fal
@tanstack/ai-gemini
@tanstack/ai-grok
@tanstack/ai-groq
@tanstack/ai-isolate-cloudflare
@tanstack/ai-isolate-node
@tanstack/ai-isolate-quickjs
@tanstack/ai-mcp
@tanstack/ai-ollama
@tanstack/ai-openai
@tanstack/ai-openrouter
@tanstack/ai-preact
@tanstack/ai-react
@tanstack/ai-react-ui
@tanstack/ai-solid
@tanstack/ai-solid-ui
@tanstack/ai-svelte
@tanstack/ai-utils
@tanstack/ai-vue
@tanstack/ai-vue-ui
@tanstack/openai-base
@tanstack/preact-ai-devtools
@tanstack/react-ai-devtools
@tanstack/solid-ai-devtools
commit: |
Summary
Fixes #758. Token usage was being dropped on the structured-output fallback path, so
RUN_FINISHED.usagecame backundefinedand the engine'srunOnUsagemiddleware 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:structuredOutputStream, or the combined tools+schema path. These emitusageon theirRUN_FINISHED.fallbackStructuredOutputStream, used when neither is available. It wraps the non-streamingstructuredOutput()and synthesizes the AG-UI lifecycle.The fallback's
structuredOutput()result does carryusage, but the synthesizedRUN_FINISHEDnever copied it over: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 declaresusage?) and forward it ontoRUN_FINISHEDwith a conditional spread so adapters that don't report usage don't get a spurioususage: undefined:This brings the fallback path to parity with the native streaming path (e.g.
openai-basealready emitsusageonRUN_FINISHEDthe same way).Testing
packages/ai/tests/chat-structured-output-stream.test.ts): a regression test assertingRUN_FINISHED.usageis forwarded, and a companion test asserting the key is omitted when the adapter reports no usage.testing/e2e/.../anthropic-structured-usage): drives the Anthropic adapter (claude-opus-4-1, a pre-combined model, to force the fallback) throughchat({ outputSchema, stream: true })against an aimock mount whose tool-forcedstructured_outputresponse carriesinput_tokens/output_tokens/cache_read_input_tokens, and asserts the normalized usage reachesRUN_FINISHED.usageend-to-end.@tanstack/aiunit suite (1045) and the e2e spec pass locally; types and lint are clean.A changeset is included (
@tanstack/aipatch).Summary by CodeRabbit