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
OpenRouterTextAdapter.structuredOutput() receives a full chat completion from the SDK (including response.usage with token counts and OpenRouter’s per-request cost), but returns only { data, rawText }. The optional usage field on StructuredOutputResult is never set.
Downstream:
chat({ outputSchema, stream: false }) → agentic/final structured path → consumers/middleware onFinish / onUsage see no usage
Billing/observability layers that rely on TokenUsage.cost (or even token counts) charge $0 or report missing cost after a successful structured call
This is not the same as #758 (engine dropped adapter usage). That was fixed. Here the adapter never returns usage.
Environment
@tanstack/ai@0.42.0
@tanstack/ai-openrouter@0.15.10
Non-stream structured call: chat({ adapter, outputSchema, stream: false, ... }) (or any path that ends in adapter.structuredOutput())
Reproduction
import{chat}from'@tanstack/ai'import{openRouterText}from'@tanstack/ai-openrouter'import{z}from'zod'constschema=z.object({title: z.string()})// Capture middlewareletusageFromFinish: unknownconstmiddleware=[{onFinish(_ctx: unknown,info: {usage?: unknown}){usageFromFinish=info.usage},onUsage(_ctx: unknown,usage: unknown){console.log('onUsage',usage)},},]constresult=awaitchat({adapter: openRouterText('openai/gpt-4o-mini',{apiKey: process.env.OPENROUTER_API_KEY!}),messages: [{role: 'user',content: 'Return a short title as JSON.'}],outputSchema: schema,stream: false,
middleware,})console.log('result',result)console.log('onFinish usage',usageFromFinish)// undefined / no cost
Also verify inside the adapter (or via network): the raw OpenRouter response includes usage.prompt_tokens, usage.completion_tokens, and often usage.cost, while the return value of structuredOutput does not.
Root cause
In @tanstack/ai-openrouteradapters/text (structuredOutput):
constresponse=awaitthis.orClient.chat.send({chatRequest: { ... stream: false,responseFormat: {type: 'json_schema', ... }}})// ...return{data: transformed,
rawText,// response.usage is never mapped onto StructuredOutputResult.usage}
Contrast with streaming helpers in the same adapter, which do:
That matches the stream path and the contract used by fallbackStructuredOutputStream after #758.
Worth auditing other providers’ structuredOutput() for the same drop.
Impact
Any app that bills or traces from middleware usage after chat({ outputSchema, stream: false }) (or non-stream agentic structured finalization) sees successful completions with no token/cost signal. Streaming structured output can still work when RUN_FINISHED.usage is set; the gap is specifically the non-stream structuredOutput return.
StructuredOutputResult.usage is populated from response.usage (tokens + OpenRouter cost / costDetails when present), and is available to middleware onFinish / onUsage and to any stream fallback that forwards result.usage.
Summary
OpenRouterTextAdapter.structuredOutput()receives a full chat completion from the SDK (includingresponse.usagewith token counts and OpenRouter’s per-requestcost), but returns only{ data, rawText }. The optionalusagefield onStructuredOutputResultis never set.Downstream:
chat({ outputSchema, stream: false })→ agentic/final structured path → consumers/middlewareonFinish/onUsagesee no usagefallbackStructuredOutputStream(core) now forwardsresult.usagewhen present (fallbackStructuredOutputStream drops usage from structured-output.complete and RUN_FINISHED chunks #758), but OpenRouter never populates it, soRUN_FINISHED.usagestays missing on that path tooTokenUsage.cost(or even token counts) charge $0 or report missing cost after a successful structured callThis is not the same as #758 (engine dropped adapter usage). That was fixed. Here the adapter never returns usage.
Environment
@tanstack/ai@0.42.0@tanstack/ai-openrouter@0.15.10chat({ adapter, outputSchema, stream: false, ... })(or any path that ends inadapter.structuredOutput())Reproduction
Also verify inside the adapter (or via network): the raw OpenRouter response includes
usage.prompt_tokens,usage.completion_tokens, and oftenusage.cost, while the return value ofstructuredOutputdoes not.Root cause
In
@tanstack/ai-openrouteradapters/text(structuredOutput):Contrast with streaming helpers in the same adapter, which do:
StructuredOutputResultalready documents the field:Suggested fix
In
OpenRouterTextAdapter.structuredOutput(and any other providerstructuredOutputthat omits it):That matches the stream path and the contract used by
fallbackStructuredOutputStreamafter #758.Worth auditing other providers’
structuredOutput()for the same drop.Impact
Any app that bills or traces from middleware usage after
chat({ outputSchema, stream: false })(or non-stream agentic structured finalization) sees successful completions with no token/cost signal. Streaming structured output can still work whenRUN_FINISHED.usageis set; the gap is specifically the non-streamstructuredOutputreturn.Related
result.usage(fixed); does not cover adapter not returning usagebillingUnitfield to TokenUsage so billed quantities are self-describing #816 (open) —billingUnitonTokenUsage(orthogonal shape work)Expected
StructuredOutputResult.usageis populated fromresponse.usage(tokens + OpenRoutercost/costDetailswhen present), and is available to middlewareonFinish/onUsageand to any stream fallback that forwardsresult.usage.