@tanstack/ai-gemini 0.12.1 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/usage.ts ADDED
@@ -0,0 +1,207 @@
1
+ import { buildBaseUsage } from '@tanstack/ai'
2
+ import type { TokenUsage } from '@tanstack/ai'
3
+ import type {
4
+ GenerateContentResponseUsageMetadata,
5
+ ModalityTokenCount,
6
+ } from '@google/genai'
7
+
8
+ /**
9
+ * Flattened modality token counts for normalized usage reporting.
10
+ * Maps Gemini's ModalityTokenCount array to individual fields.
11
+ */
12
+ export interface FlattenedModalityTokens {
13
+ /** Text tokens */
14
+ textTokens?: number
15
+ /** Image tokens */
16
+ imageTokens?: number
17
+ /** Audio tokens */
18
+ audioTokens?: number
19
+ /** Video tokens */
20
+ videoTokens?: number
21
+ /** Document tokens (e.g. PDF inputs) */
22
+ documentTokens?: number
23
+ }
24
+
25
+ /**
26
+ * Flattens Gemini's ModalityTokenCount array into individual token fields.
27
+ * Extracts TEXT, IMAGE, AUDIO, VIDEO, DOCUMENT modality counts into a
28
+ * normalized structure.
29
+ */
30
+ export function flattenModalityTokenCounts(
31
+ modalities?: Array<ModalityTokenCount>,
32
+ ): FlattenedModalityTokens {
33
+ if (!modalities || modalities.length === 0) {
34
+ return {}
35
+ }
36
+
37
+ const result: FlattenedModalityTokens = {}
38
+
39
+ for (const item of modalities) {
40
+ if (!item.modality || item.tokenCount === undefined) {
41
+ continue
42
+ }
43
+
44
+ const modality = item.modality.toUpperCase()
45
+ const count = item.tokenCount
46
+
47
+ switch (modality) {
48
+ case 'TEXT':
49
+ result.textTokens = (result.textTokens ?? 0) + count
50
+ break
51
+ case 'IMAGE':
52
+ result.imageTokens = (result.imageTokens ?? 0) + count
53
+ break
54
+ case 'AUDIO':
55
+ result.audioTokens = (result.audioTokens ?? 0) + count
56
+ break
57
+ case 'VIDEO':
58
+ result.videoTokens = (result.videoTokens ?? 0) + count
59
+ break
60
+ case 'DOCUMENT':
61
+ result.documentTokens = (result.documentTokens ?? 0) + count
62
+ break
63
+ }
64
+ }
65
+
66
+ return result
67
+ }
68
+
69
+ /**
70
+ * Checks if a FlattenedModalityTokens object has any values set.
71
+ */
72
+ export function hasModalityTokens(tokens: FlattenedModalityTokens): boolean {
73
+ return (
74
+ tokens.textTokens !== undefined ||
75
+ tokens.imageTokens !== undefined ||
76
+ tokens.audioTokens !== undefined ||
77
+ tokens.videoTokens !== undefined ||
78
+ tokens.documentTokens !== undefined
79
+ )
80
+ }
81
+
82
+ /**
83
+ * Gemini-specific provider usage details.
84
+ * These fields are unique to Gemini and placed in providerUsageDetails.
85
+ */
86
+ export type GeminiProviderUsageDetails = {
87
+ /**
88
+ * The traffic type for this request.
89
+ * Can indicate whether request was handled by different service tiers.
90
+ */
91
+ trafficType?: string
92
+ /**
93
+ * Number of tokens in the results from tool executions,
94
+ * which are provided back to the model as input.
95
+ */
96
+ toolUsePromptTokenCount?: number
97
+ /**
98
+ * Detailed breakdown by modality of the token counts from
99
+ * the results of tool executions.
100
+ */
101
+ toolUsePromptTokensDetails?: Array<{
102
+ modality: string
103
+ tokenCount: number
104
+ }>
105
+ /**
106
+ * Detailed breakdown of cache tokens by modality.
107
+ * More granular than the normalized cachedTokens field.
108
+ */
109
+ cacheTokensDetails?: Array<{
110
+ modality: string
111
+ tokenCount: number
112
+ }>
113
+ }
114
+
115
+ /**
116
+ * Build normalized TokenUsage from Gemini's usageMetadata.
117
+ * Handles modality breakdowns and thinking tokens. Returns `undefined` when the
118
+ * provider reported no usage metadata, so callers omit the field rather than
119
+ * fabricating zeroed totals.
120
+ */
121
+ export function buildGeminiUsage(
122
+ usageMetadata: GenerateContentResponseUsageMetadata | undefined | null,
123
+ ): TokenUsage<GeminiProviderUsageDetails> | undefined {
124
+ if (!usageMetadata) return undefined
125
+
126
+ const promptTokens = usageMetadata.promptTokenCount ?? 0
127
+ const completionTokens = usageMetadata.candidatesTokenCount ?? 0
128
+
129
+ const result = buildBaseUsage<GeminiProviderUsageDetails>({
130
+ promptTokens: promptTokens,
131
+ completionTokens: completionTokens,
132
+ totalTokens:
133
+ usageMetadata.totalTokenCount ?? promptTokens + completionTokens,
134
+ })
135
+
136
+ // Add prompt token details
137
+ // Flatten modality breakdown for prompt
138
+ const promptModalities = flattenModalityTokenCounts(
139
+ usageMetadata.promptTokensDetails,
140
+ )
141
+ const cachedTokens = usageMetadata.cachedContentTokenCount
142
+
143
+ const promptTokensDetails = {
144
+ ...(hasModalityTokens(promptModalities) ? promptModalities : {}),
145
+ ...(cachedTokens !== undefined && cachedTokens > 0 ? { cachedTokens } : {}),
146
+ }
147
+
148
+ // Add completion token details
149
+ // Flatten modality breakdown for candidates (output)
150
+ const completionModalities = flattenModalityTokenCounts(
151
+ usageMetadata.candidatesTokensDetails,
152
+ )
153
+ const thoughtsTokens = usageMetadata.thoughtsTokenCount
154
+
155
+ const completionTokensDetails = {
156
+ ...(hasModalityTokens(completionModalities) ? completionModalities : {}),
157
+ // Map thoughtsTokenCount to reasoningTokens for consistency with OpenAI
158
+ ...(thoughtsTokens !== undefined && thoughtsTokens > 0
159
+ ? { reasoningTokens: thoughtsTokens }
160
+ : {}),
161
+ }
162
+
163
+ // Add provider-specific details
164
+ const providerDetails: GeminiProviderUsageDetails = {
165
+ ...(usageMetadata.trafficType
166
+ ? { trafficType: usageMetadata.trafficType }
167
+ : {}),
168
+ ...(usageMetadata.toolUsePromptTokenCount !== undefined &&
169
+ usageMetadata.toolUsePromptTokenCount > 0
170
+ ? { toolUsePromptTokenCount: usageMetadata.toolUsePromptTokenCount }
171
+ : {}),
172
+ ...(usageMetadata.toolUsePromptTokensDetails &&
173
+ usageMetadata.toolUsePromptTokensDetails.length > 0
174
+ ? {
175
+ toolUsePromptTokensDetails:
176
+ usageMetadata.toolUsePromptTokensDetails.map((item) => ({
177
+ modality: item.modality || 'UNKNOWN',
178
+ tokenCount: item.tokenCount ?? 0,
179
+ })),
180
+ }
181
+ : {}),
182
+ ...(usageMetadata.cacheTokensDetails &&
183
+ usageMetadata.cacheTokensDetails.length > 0
184
+ ? {
185
+ cacheTokensDetails: usageMetadata.cacheTokensDetails.map((item) => ({
186
+ modality: item.modality || 'UNKNOWN',
187
+ tokenCount: item.tokenCount ?? 0,
188
+ })),
189
+ }
190
+ : {}),
191
+ }
192
+
193
+ // Add prompt token details if available
194
+ if (Object.keys(promptTokensDetails).length > 0) {
195
+ result.promptTokensDetails = promptTokensDetails
196
+ }
197
+ // Add provider details if available
198
+ if (Object.keys(providerDetails).length > 0) {
199
+ result.providerUsageDetails = providerDetails
200
+ }
201
+ // Add completion token details if available
202
+ if (Object.keys(completionTokensDetails).length > 0) {
203
+ result.completionTokensDetails = completionTokensDetails
204
+ }
205
+
206
+ return result
207
+ }