@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.
- package/LICENSE +21 -0
- package/README.md +131 -0
- package/dist/esm/gemini-adapter.d.ts +71 -0
- package/dist/esm/gemini-adapter.js +375 -0
- package/dist/esm/gemini-adapter.js.map +1 -0
- package/dist/esm/index.d.ts +6 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/message-types.d.ts +84 -0
- package/dist/esm/model-meta.d.ts +189 -0
- package/dist/esm/model-meta.js +43 -0
- package/dist/esm/model-meta.js.map +1 -0
- package/dist/esm/text/text-provider-options.d.ts +184 -0
- package/dist/esm/tools/code-execution-tool.d.ts +7 -0
- package/dist/esm/tools/code-execution-tool.js +9 -0
- package/dist/esm/tools/code-execution-tool.js.map +1 -0
- package/dist/esm/tools/computer-use-tool.d.ts +10 -0
- package/dist/esm/tools/computer-use-tool.js +13 -0
- package/dist/esm/tools/computer-use-tool.js.map +1 -0
- package/dist/esm/tools/file-search-tool.d.ts +7 -0
- package/dist/esm/tools/file-search-tool.js +10 -0
- package/dist/esm/tools/file-search-tool.js.map +1 -0
- package/dist/esm/tools/function-declaration-tool.d.ts +5 -0
- package/dist/esm/tools/google-maps-tool.d.ts +7 -0
- package/dist/esm/tools/google-maps-tool.js +10 -0
- package/dist/esm/tools/google-maps-tool.js.map +1 -0
- package/dist/esm/tools/google-search-retriveal-tool.d.ts +7 -0
- package/dist/esm/tools/google-search-retriveal-tool.js +10 -0
- package/dist/esm/tools/google-search-retriveal-tool.js.map +1 -0
- package/dist/esm/tools/google-search-tool.d.ts +7 -0
- package/dist/esm/tools/google-search-tool.js +10 -0
- package/dist/esm/tools/google-search-tool.js.map +1 -0
- package/dist/esm/tools/index.d.ts +9 -0
- package/dist/esm/tools/tool-converter.d.ts +22 -0
- package/dist/esm/tools/tool-converter.js +64 -0
- package/dist/esm/tools/tool-converter.js.map +1 -0
- package/dist/esm/tools/url-context-tool.d.ts +7 -0
- package/dist/esm/tools/url-context-tool.js +9 -0
- package/dist/esm/tools/url-context-tool.js.map +1 -0
- package/package.json +53 -0
- package/src/gemini-adapter.ts +577 -0
- package/src/index.ts +22 -0
- package/src/message-types.ts +125 -0
- package/src/model-meta.ts +887 -0
- package/src/text/text-provider-options.ts +246 -0
- package/src/tools/code-execution-tool.ts +17 -0
- package/src/tools/computer-use-tool.ts +25 -0
- package/src/tools/file-search-tool.ts +19 -0
- package/src/tools/function-declaration-tool.ts +34 -0
- package/src/tools/google-maps-tool.ts +19 -0
- package/src/tools/google-search-retriveal-tool.ts +21 -0
- package/src/tools/google-search-tool.ts +19 -0
- package/src/tools/index.ts +18 -0
- package/src/tools/tool-converter.ts +99 -0
- 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'
|