@tanstack/ai
Version:
Type-safe TypeScript AI SDK for streaming chat, tool calling, agents, structured outputs, and multimodal generation.
236 lines (217 loc) • 7.19 kB
text/typescript
import type { TokenUsage as SpecTokenUsage } from '@ag-ui/core'
import type { TokenUsage } from '../types'
/** AG-UI spec `usage[]` item (provider/model labels + token counts only). */
export type { TokenUsage as SpecTokenUsage } from '@ag-ui/core'
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
}
if (promptTokensDetails?.cacheWriteTokens !== undefined) {
spec.cacheWriteInputTokens = promptTokensDetails.cacheWriteTokens
}
const reasoningTokens = completionTokensDetails?.reasoningTokens
if (reasoningTokens !== undefined) {
spec.reasoningTokens = reasoningTokens
}
// cacheWriteTokens stays in the leftover too, so clients built before
// cacheWriteInputTokens existed still rebuild it.
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 }
: {}),
}),
}
}
function sumNumbers<T extends object>(
current: T | undefined,
next: T | undefined,
): T | undefined {
if (!current) return next
if (!next) return current
const result = { ...current }
for (const key of Object.keys(next) as Array<keyof T>) {
const value = next[key]
if (typeof value !== 'number') continue
const previous = current[key]
result[key] = ((typeof previous === 'number' ? previous : 0) +
value) as T[keyof T]
}
return result
}
function sumOptional(current?: number, next?: number): number | undefined {
if (current === undefined) return next
if (next === undefined) return current
return current + next
}
/**
* Add two usage totals, as for a parent run and its children. Numbers add
* up. `billed` adds up only in the same unit. `providerUsageDetails` is
* opaque, so the latest one stays. `@tanstack/ai-persistence` sums per-run
* usage with the same rules.
*/
export function addTokenUsage(
current: TokenUsage,
next: TokenUsage,
): TokenUsage {
const promptTokensDetails = sumNumbers(
current.promptTokensDetails,
next.promptTokensDetails,
)
const completionTokensDetails = sumNumbers(
current.completionTokensDetails,
next.completionTokensDetails,
)
const costDetails = sumNumbers(current.costDetails, next.costDetails)
const cost = sumOptional(current.cost, next.cost)
const durationSeconds = sumOptional(
current.durationSeconds,
next.durationSeconds,
)
const unitsBilled = sumOptional(current.unitsBilled, next.unitsBilled)
const billed =
current.billed && next.billed && current.billed.unit === next.billed.unit
? {
quantity: current.billed.quantity + next.billed.quantity,
unit: current.billed.unit,
}
: (next.billed ?? current.billed)
const providerUsageDetails =
next.providerUsageDetails ?? current.providerUsageDetails
return {
...current,
...next,
promptTokens: current.promptTokens + next.promptTokens,
completionTokens: current.completionTokens + next.completionTokens,
totalTokens: current.totalTokens + next.totalTokens,
...(promptTokensDetails && { promptTokensDetails }),
...(completionTokensDetails && { completionTokensDetails }),
...(cost !== undefined && { cost }),
...(costDetails && { costDetails }),
...(durationSeconds !== undefined && { durationSeconds }),
...(unitsBilled !== undefined && { unitsBilled }),
...(billed && { billed }),
...(providerUsageDetails && { providerUsageDetails }),
}
}
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?.reduce<SpecTokenUsage>((total, entry) => {
for (const key of [
'inputTokens',
'outputTokens',
'totalTokens',
'cachedInputTokens',
'cacheWriteInputTokens',
'reasoningTokens',
] as const) {
if (entry[key] !== undefined) total[key] = (total[key] ?? 0) + entry[key]
}
return total
}, {})
if ((usage === undefined || usage.length === 0) && leftover == null) {
return undefined
}
const {
promptTokensDetails: leftoverPromptDetails,
completionTokensDetails: leftoverCompletionDetails,
...leftoverRest
} = leftover ?? {}
const promptTokensDetails = definedDetails({
...(spec?.cachedInputTokens !== undefined
? { cachedTokens: spec.cachedInputTokens }
: {}),
...(spec?.cacheWriteInputTokens !== undefined
? { cacheWriteTokens: spec.cacheWriteInputTokens }
: {}),
...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 }
: {}),
}
}