@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/README.md +1 -1
- package/dist/esm/adapters/audio.js +5 -1
- package/dist/esm/adapters/audio.js.map +1 -1
- package/dist/esm/adapters/image.js +6 -10
- package/dist/esm/adapters/image.js.map +1 -1
- package/dist/esm/adapters/text.js +53 -13
- package/dist/esm/adapters/text.js.map +1 -1
- package/dist/esm/adapters/tts.js +6 -2
- package/dist/esm/adapters/tts.js.map +1 -1
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/usage.d.ts +67 -0
- package/dist/esm/usage.js +94 -0
- package/dist/esm/usage.js.map +1 -0
- package/package.json +3 -3
- package/src/adapters/audio.ts +6 -0
- package/src/adapters/image.ts +8 -10
- package/src/adapters/text.ts +55 -13
- package/src/adapters/tts.ts +10 -0
- package/src/index.ts +3 -0
- package/src/usage.ts +207 -0
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
|
+
}
|