@tanstack/ai-gemini 0.0.1

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.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +131 -0
  3. package/dist/esm/gemini-adapter.d.ts +71 -0
  4. package/dist/esm/gemini-adapter.js +375 -0
  5. package/dist/esm/gemini-adapter.js.map +1 -0
  6. package/dist/esm/index.d.ts +6 -0
  7. package/dist/esm/index.js +7 -0
  8. package/dist/esm/index.js.map +1 -0
  9. package/dist/esm/message-types.d.ts +84 -0
  10. package/dist/esm/model-meta.d.ts +189 -0
  11. package/dist/esm/model-meta.js +43 -0
  12. package/dist/esm/model-meta.js.map +1 -0
  13. package/dist/esm/text/text-provider-options.d.ts +184 -0
  14. package/dist/esm/tools/code-execution-tool.d.ts +7 -0
  15. package/dist/esm/tools/code-execution-tool.js +9 -0
  16. package/dist/esm/tools/code-execution-tool.js.map +1 -0
  17. package/dist/esm/tools/computer-use-tool.d.ts +10 -0
  18. package/dist/esm/tools/computer-use-tool.js +13 -0
  19. package/dist/esm/tools/computer-use-tool.js.map +1 -0
  20. package/dist/esm/tools/file-search-tool.d.ts +7 -0
  21. package/dist/esm/tools/file-search-tool.js +10 -0
  22. package/dist/esm/tools/file-search-tool.js.map +1 -0
  23. package/dist/esm/tools/function-declaration-tool.d.ts +5 -0
  24. package/dist/esm/tools/google-maps-tool.d.ts +7 -0
  25. package/dist/esm/tools/google-maps-tool.js +10 -0
  26. package/dist/esm/tools/google-maps-tool.js.map +1 -0
  27. package/dist/esm/tools/google-search-retriveal-tool.d.ts +7 -0
  28. package/dist/esm/tools/google-search-retriveal-tool.js +10 -0
  29. package/dist/esm/tools/google-search-retriveal-tool.js.map +1 -0
  30. package/dist/esm/tools/google-search-tool.d.ts +7 -0
  31. package/dist/esm/tools/google-search-tool.js +10 -0
  32. package/dist/esm/tools/google-search-tool.js.map +1 -0
  33. package/dist/esm/tools/index.d.ts +9 -0
  34. package/dist/esm/tools/tool-converter.d.ts +22 -0
  35. package/dist/esm/tools/tool-converter.js +64 -0
  36. package/dist/esm/tools/tool-converter.js.map +1 -0
  37. package/dist/esm/tools/url-context-tool.d.ts +7 -0
  38. package/dist/esm/tools/url-context-tool.js +9 -0
  39. package/dist/esm/tools/url-context-tool.js.map +1 -0
  40. package/package.json +53 -0
  41. package/src/gemini-adapter.ts +577 -0
  42. package/src/index.ts +22 -0
  43. package/src/message-types.ts +125 -0
  44. package/src/model-meta.ts +887 -0
  45. package/src/text/text-provider-options.ts +246 -0
  46. package/src/tools/code-execution-tool.ts +17 -0
  47. package/src/tools/computer-use-tool.ts +25 -0
  48. package/src/tools/file-search-tool.ts +19 -0
  49. package/src/tools/function-declaration-tool.ts +34 -0
  50. package/src/tools/google-maps-tool.ts +19 -0
  51. package/src/tools/google-search-retriveal-tool.ts +21 -0
  52. package/src/tools/google-search-tool.ts +19 -0
  53. package/src/tools/index.ts +18 -0
  54. package/src/tools/tool-converter.ts +99 -0
  55. package/src/tools/url-context-tool.ts +17 -0
@@ -0,0 +1,577 @@
1
+ import { FinishReason, GoogleGenAI } from '@google/genai'
2
+ import { BaseAdapter } from '@tanstack/ai'
3
+ import { GEMINI_EMBEDDING_MODELS, GEMINI_MODELS } from './model-meta'
4
+ import { convertToolsToProviderFormat } from './tools/tool-converter'
5
+ import type {
6
+ AIAdapterConfig,
7
+ ChatOptions,
8
+ ContentPart,
9
+ EmbeddingOptions,
10
+ EmbeddingResult,
11
+ ModelMessage,
12
+ StreamChunk,
13
+ SummarizationOptions,
14
+ SummarizationResult,
15
+ } from '@tanstack/ai'
16
+ import type {
17
+ GeminiChatModelProviderOptionsByName,
18
+ GeminiModelInputModalitiesByName,
19
+ } from './model-meta'
20
+ import type { ExternalTextProviderOptions } from './text/text-provider-options'
21
+ import type {
22
+ GenerateContentParameters,
23
+ GenerateContentResponse,
24
+ Part,
25
+ } from '@google/genai'
26
+ import type {
27
+ GeminiAudioMetadata,
28
+ GeminiDocumentMetadata,
29
+ GeminiImageMetadata,
30
+ GeminiMessageMetadataByModality,
31
+ GeminiVideoMetadata,
32
+ } from './message-types'
33
+
34
+ export interface GeminiAdapterConfig extends AIAdapterConfig {
35
+ apiKey: string
36
+ }
37
+
38
+ /**
39
+ * Gemini-specific provider options
40
+ * Based on Google Generative AI SDK
41
+ * @see https://ai.google.dev/api/rest/v1/GenerationConfig
42
+ */
43
+ export type GeminiProviderOptions = ExternalTextProviderOptions
44
+
45
+ export class GeminiAdapter extends BaseAdapter<
46
+ typeof GEMINI_MODELS,
47
+ typeof GEMINI_EMBEDDING_MODELS,
48
+ GeminiProviderOptions,
49
+ Record<string, any>,
50
+ GeminiChatModelProviderOptionsByName,
51
+ GeminiModelInputModalitiesByName,
52
+ GeminiMessageMetadataByModality
53
+ > {
54
+ name = 'gemini'
55
+ models = GEMINI_MODELS
56
+ embeddingModels = GEMINI_EMBEDDING_MODELS
57
+ declare _modelProviderOptionsByName: GeminiChatModelProviderOptionsByName
58
+ declare _modelInputModalitiesByName: GeminiModelInputModalitiesByName
59
+ declare _messageMetadataByModality: GeminiMessageMetadataByModality
60
+ private client: GoogleGenAI
61
+
62
+ constructor(config: GeminiAdapterConfig) {
63
+ super(config)
64
+ this.client = new GoogleGenAI({
65
+ apiKey: config.apiKey,
66
+ })
67
+ }
68
+
69
+ async *chatStream(
70
+ options: ChatOptions<string, GeminiProviderOptions>,
71
+ ): AsyncIterable<StreamChunk> {
72
+ // Map common options to Gemini format
73
+ const mappedOptions = this.mapCommonOptionsToGemini(options)
74
+
75
+ try {
76
+ const result =
77
+ await this.client.models.generateContentStream(mappedOptions)
78
+
79
+ yield* this.processStreamChunks(result, options.model)
80
+ } catch (error) {
81
+ const timestamp = Date.now()
82
+ yield {
83
+ type: 'error',
84
+ id: this.generateId(),
85
+ model: options.model,
86
+ timestamp,
87
+ error: {
88
+ message:
89
+ error instanceof Error
90
+ ? error.message
91
+ : 'An unknown error occurred during the chat stream.',
92
+ },
93
+ }
94
+ }
95
+ }
96
+
97
+ async summarize(options: SummarizationOptions): Promise<SummarizationResult> {
98
+ const prompt = this.buildSummarizationPrompt(options, options.text)
99
+
100
+ // Use models API like chatCompletion
101
+ const result = await this.client.models.generateContent({
102
+ model: options.model,
103
+ contents: [{ role: 'user', parts: [{ text: prompt }] }],
104
+ config: {
105
+ temperature: 0.3,
106
+ maxOutputTokens: options.maxLength || 500,
107
+ },
108
+ })
109
+
110
+ // Extract text from candidates or use .text() method
111
+ let summary = ''
112
+ if (result.candidates?.[0]?.content?.parts) {
113
+ const parts = result.candidates[0].content.parts
114
+ for (const part of parts) {
115
+ if (part.text) {
116
+ summary += part.text
117
+ }
118
+ }
119
+ }
120
+
121
+ if (!summary && typeof result.text === 'string') {
122
+ summary = result.text
123
+ }
124
+
125
+ const promptTokens = this.estimateTokens(prompt)
126
+ const completionTokens = this.estimateTokens(summary)
127
+
128
+ return {
129
+ id: this.generateId(),
130
+ model: options.model,
131
+ summary,
132
+ usage: {
133
+ promptTokens,
134
+ completionTokens,
135
+ totalTokens: promptTokens + completionTokens,
136
+ },
137
+ }
138
+ }
139
+
140
+ async createEmbeddings(options: EmbeddingOptions): Promise<EmbeddingResult> {
141
+ const inputs = Array.isArray(options.input)
142
+ ? options.input
143
+ : [options.input]
144
+
145
+ // According to docs: contents can be a string or array of strings
146
+ // Response has embeddings (plural) array with values property
147
+ const result = await this.client.models.embedContent({
148
+ model: options.model,
149
+ contents: inputs,
150
+ })
151
+
152
+ // Extract embeddings from result.embeddings array
153
+ const embeddings: Array<Array<number>> = []
154
+ if (result.embeddings && Array.isArray(result.embeddings)) {
155
+ for (const embedding of result.embeddings) {
156
+ if (embedding.values && Array.isArray(embedding.values)) {
157
+ embeddings.push(embedding.values)
158
+ } else if (Array.isArray(embedding)) {
159
+ embeddings.push(embedding)
160
+ }
161
+ }
162
+ }
163
+
164
+ const promptTokens = inputs.reduce(
165
+ (sum, input) => sum + this.estimateTokens(input),
166
+ 0,
167
+ )
168
+
169
+ return {
170
+ id: this.generateId(),
171
+ model: options.model || 'gemini-embedding-001',
172
+ embeddings,
173
+ usage: {
174
+ promptTokens,
175
+ totalTokens: promptTokens,
176
+ },
177
+ }
178
+ }
179
+
180
+ private buildSummarizationPrompt(
181
+ options: SummarizationOptions,
182
+ text: string,
183
+ ): string {
184
+ let prompt = 'You are a professional summarizer. '
185
+
186
+ switch (options.style) {
187
+ case 'bullet-points':
188
+ prompt += 'Provide a summary in bullet point format. '
189
+ break
190
+ case 'paragraph':
191
+ prompt += 'Provide a summary in paragraph format. '
192
+ break
193
+ case 'concise':
194
+ prompt += 'Provide a very concise summary in 1-2 sentences. '
195
+ break
196
+ default:
197
+ prompt += 'Provide a clear and concise summary. '
198
+ }
199
+
200
+ if (options.focus && options.focus.length > 0) {
201
+ prompt += `Focus on the following aspects: ${options.focus.join(', ')}. `
202
+ }
203
+
204
+ prompt += `\n\nText to summarize:\n${text}\n\nSummary:`
205
+
206
+ return prompt
207
+ }
208
+
209
+ private estimateTokens(text: string): number {
210
+ // Rough approximation: 1 token ≈ 4 characters
211
+ return Math.ceil(text.length / 4)
212
+ }
213
+
214
+ private async *processStreamChunks(
215
+ result: AsyncGenerator<GenerateContentResponse, any, any>,
216
+ model: string,
217
+ ): AsyncIterable<StreamChunk> {
218
+ const timestamp = Date.now()
219
+ let accumulatedContent = ''
220
+ const toolCallMap = new Map<
221
+ string,
222
+ { name: string; args: string; index: number }
223
+ >()
224
+ let nextToolIndex = 0
225
+ // Iterate over the stream result (it's already an AsyncGenerator)
226
+ for await (const chunk of result) {
227
+ // Extract content from candidates[0].content.parts
228
+ // Parts can contain text or functionCall
229
+ if (chunk.candidates?.[0]?.content?.parts) {
230
+ const parts = chunk.candidates[0].content.parts
231
+
232
+ for (const part of parts) {
233
+ // Handle text content
234
+ if (part.text) {
235
+ accumulatedContent += part.text
236
+ yield {
237
+ type: 'content',
238
+ id: this.generateId(),
239
+ model,
240
+ timestamp,
241
+ delta: part.text,
242
+ content: accumulatedContent,
243
+ role: 'assistant',
244
+ }
245
+ }
246
+
247
+ // Handle function calls (tool calls)
248
+ // Check both camelCase (SDK) and snake_case (direct API) formats
249
+ const functionCall = part.functionCall
250
+ if (functionCall) {
251
+ const toolCallId =
252
+ functionCall.name || `call_${Date.now()}_${nextToolIndex}`
253
+ const functionArgs = functionCall.args || {}
254
+
255
+ // Check if we've seen this tool call before (for streaming args)
256
+ let toolCallData = toolCallMap.get(toolCallId)
257
+ if (!toolCallData) {
258
+ toolCallData = {
259
+ name: functionCall.name || '',
260
+ args:
261
+ typeof functionArgs === 'string'
262
+ ? functionArgs
263
+ : JSON.stringify(functionArgs),
264
+ index: nextToolIndex++,
265
+ }
266
+ toolCallMap.set(toolCallId, toolCallData)
267
+ } else {
268
+ // Merge arguments if streaming
269
+
270
+ try {
271
+ const existingArgs = JSON.parse(toolCallData.args)
272
+ const newArgs =
273
+ typeof functionArgs === 'string'
274
+ ? JSON.parse(functionArgs)
275
+ : functionArgs
276
+ const mergedArgs = { ...existingArgs, ...newArgs }
277
+ toolCallData.args = JSON.stringify(mergedArgs)
278
+ } catch {
279
+ // If parsing fails, use new args
280
+ toolCallData.args =
281
+ typeof functionArgs === 'string'
282
+ ? functionArgs
283
+ : JSON.stringify(functionArgs)
284
+ }
285
+ }
286
+
287
+ yield {
288
+ type: 'tool_call',
289
+ id: this.generateId(),
290
+ model,
291
+ timestamp,
292
+ toolCall: {
293
+ id: toolCallId,
294
+ type: 'function',
295
+ function: {
296
+ name: toolCallData.name,
297
+ arguments: toolCallData.args,
298
+ },
299
+ },
300
+ index: toolCallData.index,
301
+ }
302
+ }
303
+ }
304
+ } else if (chunk.data) {
305
+ // Fallback to chunk.data if available
306
+ accumulatedContent += chunk.data
307
+ yield {
308
+ type: 'content',
309
+ id: this.generateId(),
310
+ model,
311
+ timestamp,
312
+ delta: chunk.data,
313
+ content: accumulatedContent,
314
+ role: 'assistant',
315
+ }
316
+ }
317
+
318
+ // Check for finish reason
319
+ if (chunk.candidates?.[0]?.finishReason) {
320
+ const finishReason = chunk.candidates[0].finishReason
321
+
322
+ // UNEXPECTED_TOOL_CALL means Gemini tried to call a function but it wasn't properly declared
323
+ // This typically means there's an issue with the tool declaration format
324
+ // We should map it to tool_calls to try to process it anyway
325
+ if (finishReason === FinishReason.UNEXPECTED_TOOL_CALL) {
326
+ // Try to extract function call from content.parts if available
327
+ if (chunk.candidates[0].content?.parts) {
328
+ for (const part of chunk.candidates[0].content.parts) {
329
+ const functionCall = part.functionCall
330
+ if (functionCall) {
331
+ // We found a function call - process it
332
+ const toolCallId =
333
+ functionCall.name || `call_${Date.now()}_${nextToolIndex}`
334
+ const functionArgs = functionCall.args || {}
335
+
336
+ toolCallMap.set(toolCallId, {
337
+ name: functionCall.name || '',
338
+ args:
339
+ typeof functionArgs === 'string'
340
+ ? functionArgs
341
+ : JSON.stringify(functionArgs),
342
+ index: nextToolIndex++,
343
+ })
344
+
345
+ yield {
346
+ type: 'tool_call',
347
+ id: this.generateId(),
348
+ model,
349
+ timestamp,
350
+ toolCall: {
351
+ id: toolCallId,
352
+ type: 'function',
353
+ function: {
354
+ name: functionCall.name || '',
355
+ arguments:
356
+ typeof functionArgs === 'string'
357
+ ? functionArgs
358
+ : JSON.stringify(functionArgs),
359
+ },
360
+ },
361
+ index: nextToolIndex - 1,
362
+ }
363
+ }
364
+ }
365
+ }
366
+ }
367
+ if (finishReason === FinishReason.MAX_TOKENS) {
368
+ yield {
369
+ type: 'error',
370
+ id: this.generateId(),
371
+ model,
372
+ timestamp,
373
+ error: {
374
+ message:
375
+ 'The response was cut off because the maximum token limit was reached.',
376
+ },
377
+ }
378
+ }
379
+
380
+ yield {
381
+ type: 'done',
382
+ id: this.generateId(),
383
+ model,
384
+ timestamp,
385
+ finishReason: toolCallMap.size > 0 ? 'tool_calls' : 'stop',
386
+ usage: chunk.usageMetadata
387
+ ? {
388
+ promptTokens: chunk.usageMetadata.promptTokenCount ?? 0,
389
+ completionTokens: chunk.usageMetadata.thoughtsTokenCount ?? 0,
390
+ totalTokens: chunk.usageMetadata.totalTokenCount ?? 0,
391
+ }
392
+ : undefined,
393
+ }
394
+ }
395
+ }
396
+ }
397
+
398
+ private convertContentPartToGemini(part: ContentPart): Part {
399
+ switch (part.type) {
400
+ case 'text':
401
+ return { text: part.content }
402
+ case 'image':
403
+ case 'audio':
404
+ case 'video':
405
+ case 'document': {
406
+ const metadata = part.metadata as
407
+ | GeminiDocumentMetadata
408
+ | GeminiImageMetadata
409
+ | GeminiVideoMetadata
410
+ | GeminiAudioMetadata
411
+ | undefined
412
+ // Gemini uses inlineData for base64 and fileData for URLs
413
+ if (part.source.type === 'data') {
414
+ return {
415
+ inlineData: {
416
+ data: part.source.value,
417
+ mimeType: metadata?.mimeType ?? 'image/jpeg',
418
+ },
419
+ }
420
+ } else {
421
+ return {
422
+ fileData: {
423
+ fileUri: part.source.value,
424
+ mimeType: metadata?.mimeType ?? 'image/jpeg',
425
+ },
426
+ }
427
+ }
428
+ }
429
+ default: {
430
+ // Exhaustive check - this should never happen with known types
431
+ const _exhaustiveCheck: never = part
432
+ throw new Error(
433
+ `Unsupported content part type: ${(_exhaustiveCheck as ContentPart).type}`,
434
+ )
435
+ }
436
+ }
437
+ }
438
+
439
+ private formatMessages(
440
+ messages: Array<ModelMessage>,
441
+ ): GenerateContentParameters['contents'] {
442
+ return messages.map((msg) => {
443
+ const role: 'user' | 'model' = msg.role === 'assistant' ? 'model' : 'user'
444
+ const parts: Array<Part> = []
445
+
446
+ // Handle multimodal content (array of ContentPart)
447
+ if (Array.isArray(msg.content)) {
448
+ for (const contentPart of msg.content) {
449
+ parts.push(this.convertContentPartToGemini(contentPart))
450
+ }
451
+ } else if (msg.content) {
452
+ // Handle string content (backward compatibility)
453
+ parts.push({ text: msg.content })
454
+ }
455
+
456
+ // Handle tool calls (from assistant)
457
+ if (msg.role === 'assistant' && msg.toolCalls?.length) {
458
+ for (const toolCall of msg.toolCalls) {
459
+ let parsedArgs: Record<string, unknown> = {}
460
+ try {
461
+ parsedArgs = toolCall.function.arguments
462
+ ? JSON.parse(toolCall.function.arguments)
463
+ : {}
464
+ } catch {
465
+ parsedArgs = toolCall.function.arguments as any
466
+ }
467
+
468
+ parts.push({
469
+ functionCall: {
470
+ name: toolCall.function.name,
471
+ args: parsedArgs,
472
+ },
473
+ })
474
+ }
475
+ }
476
+
477
+ // Handle tool results (from tool role)
478
+ if (msg.role === 'tool' && msg.toolCallId) {
479
+ parts.push({
480
+ functionResponse: {
481
+ name: msg.toolCallId, // Gemini uses function name here
482
+ response: {
483
+ content: msg.content || '',
484
+ },
485
+ },
486
+ })
487
+ }
488
+
489
+ return {
490
+ role,
491
+ parts: parts.length > 0 ? parts : [{ text: '' }],
492
+ }
493
+ })
494
+ }
495
+ /**
496
+ * Maps common options to Gemini-specific format
497
+ * Handles translation of normalized options to Gemini's API format
498
+ */
499
+ private mapCommonOptionsToGemini(options: ChatOptions) {
500
+ const providerOpts = options.providerOptions
501
+ const requestOptions: GenerateContentParameters = {
502
+ model: options.model,
503
+ contents: this.formatMessages(options.messages),
504
+ config: {
505
+ ...providerOpts,
506
+ temperature: options.options?.temperature,
507
+ topP: options.options?.topP,
508
+ maxOutputTokens: options.options?.maxTokens,
509
+ systemInstruction: options.systemPrompts?.join('\n'),
510
+ ...providerOpts?.generationConfig,
511
+ tools: convertToolsToProviderFormat(options.tools),
512
+ },
513
+ }
514
+
515
+ return requestOptions
516
+ }
517
+ }
518
+
519
+ /**
520
+ * Creates a Gemini adapter with simplified configuration
521
+ * @param apiKey - Your Google API key
522
+ * @returns A fully configured Gemini adapter instance
523
+ *
524
+ * @example
525
+ * ```typescript
526
+ * const gemini = createGemini("AIza...");
527
+ *
528
+ * const ai = new AI({
529
+ * adapters: {
530
+ * gemini,
531
+ * }
532
+ * });
533
+ * ```
534
+ */
535
+ export function createGemini(
536
+ apiKey: string,
537
+ config?: Omit<GeminiAdapterConfig, 'apiKey'>,
538
+ ): GeminiAdapter {
539
+ return new GeminiAdapter({ apiKey, ...config })
540
+ }
541
+
542
+ /**
543
+ * Create a Gemini adapter with automatic API key detection from environment variables.
544
+ *
545
+ * Looks for `GOOGLE_API_KEY` or `GEMINI_API_KEY` in:
546
+ * - `process.env` (Node.js)
547
+ * - `window.env` (Browser with injected env)
548
+ *
549
+ * @param config - Optional configuration (excluding apiKey which is auto-detected)
550
+ * @returns Configured Gemini adapter instance
551
+ * @throws Error if API key is not found in environment
552
+ *
553
+ * @example
554
+ * ```typescript
555
+ * // Automatically uses GOOGLE_API_KEY or GEMINI_API_KEY from environment
556
+ * const aiInstance = ai(gemini());
557
+ * ```
558
+ */
559
+ export function gemini(
560
+ config?: Omit<GeminiAdapterConfig, 'apiKey'>,
561
+ ): GeminiAdapter {
562
+ const env =
563
+ typeof globalThis !== 'undefined' && (globalThis as any).window?.env
564
+ ? (globalThis as any).window.env
565
+ : typeof process !== 'undefined'
566
+ ? process.env
567
+ : undefined
568
+ const key = env?.GOOGLE_API_KEY || env?.GEMINI_API_KEY
569
+
570
+ if (!key) {
571
+ throw new Error(
572
+ 'GOOGLE_API_KEY or GEMINI_API_KEY is required. Please set it in your environment variables or use createGemini(apiKey, config) instead.',
573
+ )
574
+ }
575
+
576
+ return createGemini(key, config)
577
+ }
package/src/index.ts ADDED
@@ -0,0 +1,22 @@
1
+ export { GeminiAdapter, createGemini, gemini } from './gemini-adapter'
2
+ export type { GeminiAdapterConfig } from './gemini-adapter'
3
+ export type {
4
+ GeminiChatModelProviderOptionsByName,
5
+ GeminiModelInputModalitiesByName,
6
+ } from './model-meta'
7
+ export type {
8
+ GeminiStructuredOutputOptions,
9
+ GeminiThinkingOptions,
10
+ } from './text/text-provider-options'
11
+ export type { GoogleGeminiTool } from './tools/index'
12
+ export type {
13
+ GeminiImageMetadata,
14
+ GeminiAudioMetadata,
15
+ GeminiVideoMetadata,
16
+ GeminiDocumentMetadata,
17
+ GeminiImageMimeType,
18
+ GeminiAudioMimeType,
19
+ GeminiVideoMimeType,
20
+ GeminiDocumentMimeType,
21
+ GeminiMessageMetadataByModality,
22
+ } from './message-types'