@tanstack/ai
Version:
Type-safe TypeScript AI SDK for streaming chat, tool calling, agents, structured outputs, and multimodal generation.
149 lines (133 loc) • 3.97 kB
text/typescript
import type { TokenUsage } from '../types'
/** AG-UI spec `usage[]` item (provider/model labels + token counts only). */
export interface SpecTokenUsage {
provider?: string
model?: string
inputTokens?: number
outputTokens?: number
totalTokens?: number
reasoningTokens?: number
cachedInputTokens?: number
}
export interface ToSpecTokenUsageOptions {
provider?: string
model?: string
}
/** TokenUsage fields that have no AG-UI `usage[]` equivalent. */
export type TokenUsageLeftover = Omit<
TokenUsage,
'promptTokens' | 'completionTokens' | 'totalTokens'
>
function definedDetails<T extends object>(value: T): T | undefined {
return Object.keys(value).length > 0 ? value : undefined
}
function withoutKey<T extends object, K extends keyof T>(
value: T,
key: K,
): Omit<T, K> {
const next = { ...value }
delete next[key]
return next
}
export function isTanstackUsage(usage: unknown): usage is TokenUsage {
return (
typeof usage === 'object' &&
usage != null &&
!Array.isArray(usage) &&
'promptTokens' in usage
)
}
export function toSpecTokenUsage(
usage: TokenUsage,
options?: ToSpecTokenUsageOptions,
): { usage: Array<SpecTokenUsage>; leftover?: TokenUsageLeftover } {
const {
promptTokens,
completionTokens,
totalTokens,
promptTokensDetails,
completionTokensDetails,
...rest
} = usage
const spec: SpecTokenUsage = {
...(options?.provider !== undefined ? { provider: options.provider } : {}),
...(options?.model !== undefined ? { model: options.model } : {}),
inputTokens: promptTokens,
outputTokens: completionTokens,
totalTokens,
}
const cachedInputTokens = promptTokensDetails?.cachedTokens
if (cachedInputTokens !== undefined) {
spec.cachedInputTokens = cachedInputTokens
}
const reasoningTokens = completionTokensDetails?.reasoningTokens
if (reasoningTokens !== undefined) {
spec.reasoningTokens = reasoningTokens
}
const leftoverPrompt = promptTokensDetails
? definedDetails(withoutKey(promptTokensDetails, 'cachedTokens'))
: undefined
const leftoverCompletion = completionTokensDetails
? definedDetails(withoutKey(completionTokensDetails, 'reasoningTokens'))
: undefined
return {
usage: [spec],
leftover: definedDetails({
...rest,
...(leftoverPrompt !== undefined
? { promptTokensDetails: leftoverPrompt }
: {}),
...(leftoverCompletion !== undefined
? { completionTokensDetails: leftoverCompletion }
: {}),
}),
}
}
export function rebuildTokenUsage(
usage: unknown,
leftover?: TokenUsageLeftover,
): TokenUsage | undefined {
if (isTanstackUsage(usage)) {
return usage
}
if (Array.isArray(usage)) {
return fromSpecTokenUsage(usage, leftover)
}
return fromSpecTokenUsage(undefined, leftover)
}
export function fromSpecTokenUsage(
usage: ReadonlyArray<SpecTokenUsage> | undefined,
leftover?: TokenUsageLeftover,
): TokenUsage | undefined {
const spec = usage?.[0]
if (spec == null && leftover == null) {
return undefined
}
const {
promptTokensDetails: leftoverPromptDetails,
completionTokensDetails: leftoverCompletionDetails,
...leftoverRest
} = leftover ?? {}
const promptTokensDetails = definedDetails({
...(spec?.cachedInputTokens !== undefined
? { cachedTokens: spec.cachedInputTokens }
: {}),
...leftoverPromptDetails,
})
const completionTokensDetails = definedDetails({
...(spec?.reasoningTokens !== undefined
? { reasoningTokens: spec.reasoningTokens }
: {}),
...leftoverCompletionDetails,
})
return {
promptTokens: spec?.inputTokens ?? 0,
completionTokens: spec?.outputTokens ?? 0,
totalTokens: spec?.totalTokens ?? 0,
...leftoverRest,
...(promptTokensDetails !== undefined ? { promptTokensDetails } : {}),
...(completionTokensDetails !== undefined
? { completionTokensDetails }
: {}),
}
}