@tanstack/ai-gemini 0.17.3 → 0.18.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 (81) hide show
  1. package/package.json +5 -4
  2. package/src/experimental/text-interactions/adapter.ts +304 -100
  3. package/src/experimental/text-interactions/events.ts +29 -23
  4. package/src/experimental/text-interactions/provider-options.ts +0 -1
  5. package/dist/esm/adapters/audio.d.ts +0 -92
  6. package/dist/esm/adapters/audio.js +0 -66
  7. package/dist/esm/adapters/audio.js.map +0 -1
  8. package/dist/esm/adapters/image.d.ts +0 -101
  9. package/dist/esm/adapters/image.js +0 -253
  10. package/dist/esm/adapters/image.js.map +0 -1
  11. package/dist/esm/adapters/summarize.d.ts +0 -33
  12. package/dist/esm/adapters/summarize.js +0 -18
  13. package/dist/esm/adapters/summarize.js.map +0 -1
  14. package/dist/esm/adapters/text.d.ts +0 -85
  15. package/dist/esm/adapters/text.js +0 -658
  16. package/dist/esm/adapters/text.js.map +0 -1
  17. package/dist/esm/adapters/tts.d.ts +0 -165
  18. package/dist/esm/adapters/tts.js +0 -192
  19. package/dist/esm/adapters/tts.js.map +0 -1
  20. package/dist/esm/adapters/video.d.ts +0 -108
  21. package/dist/esm/adapters/video.js +0 -227
  22. package/dist/esm/adapters/video.js.map +0 -1
  23. package/dist/esm/experimental/index.d.ts +0 -6
  24. package/dist/esm/experimental/index.js +0 -7
  25. package/dist/esm/experimental/index.js.map +0 -1
  26. package/dist/esm/experimental/text-interactions/adapter.d.ts +0 -75
  27. package/dist/esm/experimental/text-interactions/adapter.js +0 -899
  28. package/dist/esm/experimental/text-interactions/adapter.js.map +0 -1
  29. package/dist/esm/experimental/text-interactions/events.d.ts +0 -141
  30. package/dist/esm/experimental/text-interactions/provider-options.d.ts +0 -15
  31. package/dist/esm/image/image-provider-options.d.ts +0 -181
  32. package/dist/esm/image/image-provider-options.js +0 -67
  33. package/dist/esm/image/image-provider-options.js.map +0 -1
  34. package/dist/esm/index.d.ts +0 -33
  35. package/dist/esm/index.js +0 -38
  36. package/dist/esm/index.js.map +0 -1
  37. package/dist/esm/message-types.d.ts +0 -104
  38. package/dist/esm/model-meta.d.ts +0 -242
  39. package/dist/esm/model-meta.js +0 -159
  40. package/dist/esm/model-meta.js.map +0 -1
  41. package/dist/esm/text/text-provider-options.d.ts +0 -196
  42. package/dist/esm/tools/code-execution-tool.d.ts +0 -10
  43. package/dist/esm/tools/code-execution-tool.js +0 -18
  44. package/dist/esm/tools/code-execution-tool.js.map +0 -1
  45. package/dist/esm/tools/computer-use-tool.d.ts +0 -13
  46. package/dist/esm/tools/computer-use-tool.js +0 -33
  47. package/dist/esm/tools/computer-use-tool.js.map +0 -1
  48. package/dist/esm/tools/file-search-tool.d.ts +0 -10
  49. package/dist/esm/tools/file-search-tool.js +0 -19
  50. package/dist/esm/tools/file-search-tool.js.map +0 -1
  51. package/dist/esm/tools/function-declaration-tool.d.ts +0 -5
  52. package/dist/esm/tools/function-declaration-tool.js +0 -28
  53. package/dist/esm/tools/function-declaration-tool.js.map +0 -1
  54. package/dist/esm/tools/google-maps-tool.d.ts +0 -10
  55. package/dist/esm/tools/google-maps-tool.js +0 -19
  56. package/dist/esm/tools/google-maps-tool.js.map +0 -1
  57. package/dist/esm/tools/google-search-retriveal-tool.d.ts +0 -10
  58. package/dist/esm/tools/google-search-retriveal-tool.js +0 -19
  59. package/dist/esm/tools/google-search-retriveal-tool.js.map +0 -1
  60. package/dist/esm/tools/google-search-tool.d.ts +0 -10
  61. package/dist/esm/tools/google-search-tool.js +0 -19
  62. package/dist/esm/tools/google-search-tool.js.map +0 -1
  63. package/dist/esm/tools/index.d.ts +0 -18
  64. package/dist/esm/tools/index.js +0 -21
  65. package/dist/esm/tools/index.js.map +0 -1
  66. package/dist/esm/tools/tool-converter.d.ts +0 -22
  67. package/dist/esm/tools/tool-converter.js +0 -66
  68. package/dist/esm/tools/tool-converter.js.map +0 -1
  69. package/dist/esm/tools/url-context-tool.d.ts +0 -10
  70. package/dist/esm/tools/url-context-tool.js +0 -18
  71. package/dist/esm/tools/url-context-tool.js.map +0 -1
  72. package/dist/esm/usage.d.ts +0 -67
  73. package/dist/esm/usage.js +0 -94
  74. package/dist/esm/usage.js.map +0 -1
  75. package/dist/esm/utils/client.d.ts +0 -17
  76. package/dist/esm/utils/client.js +0 -30
  77. package/dist/esm/utils/client.js.map +0 -1
  78. package/dist/esm/utils/index.d.ts +0 -1
  79. package/dist/esm/video/video-provider-options.d.ts +0 -90
  80. package/dist/esm/video/video-provider-options.js +0 -15
  81. package/dist/esm/video/video-provider-options.js.map +0 -1
@@ -1,658 +0,0 @@
1
- import { FinishReason } from "@google/genai";
2
- import { EventType, normalizeSystemPrompts } from "@tanstack/ai";
3
- import { toRunErrorRawEvent } from "@tanstack/ai/adapter-internals";
4
- import { BaseTextAdapter } from "@tanstack/ai/adapters";
5
- import { convertToolsToProviderFormat } from "../tools/tool-converter.js";
6
- import { buildGeminiUsage } from "../usage.js";
7
- import { createGeminiClient, generateId, getGeminiApiKeyFromEnv } from "../utils/client.js";
8
- import { GEMINI_COMBINED_TOOLS_AND_SCHEMA_MODELS } from "../model-meta.js";
9
- class GeminiTextAdapter extends BaseTextAdapter {
10
- kind = "text";
11
- name = "gemini";
12
- client;
13
- constructor(config, model) {
14
- super({}, model);
15
- this.client = createGeminiClient(config);
16
- }
17
- async *chatStream(options) {
18
- const mappedOptions = this.mapCommonOptionsToGemini(options);
19
- const { logger } = options;
20
- try {
21
- logger.request(
22
- `activity=chat provider=gemini model=${this.model} messages=${options.messages.length} tools=${options.tools?.length ?? 0} stream=true`,
23
- { provider: "gemini", model: this.model }
24
- );
25
- const result = await this.client.models.generateContentStream(mappedOptions);
26
- yield* this.processStreamChunks(result, options, logger);
27
- } catch (error) {
28
- const rawEvent = toRunErrorRawEvent(error);
29
- logger.errors("gemini.chatStream fatal", {
30
- error,
31
- source: "gemini.chatStream"
32
- });
33
- yield {
34
- type: EventType.RUN_ERROR,
35
- model: options.model,
36
- timestamp: Date.now(),
37
- message: error instanceof Error ? error.message : "An unknown error occurred during the chat stream.",
38
- // Forward the provider's structured error body when present (see
39
- // toRunErrorRawEvent); omitted otherwise.
40
- ...rawEvent !== void 0 && { rawEvent },
41
- error: {
42
- message: error instanceof Error ? error.message : "An unknown error occurred during the chat stream."
43
- }
44
- };
45
- }
46
- }
47
- /**
48
- * Generate structured output using Gemini's native JSON response format.
49
- * Uses responseMimeType: 'application/json' and responseSchema for structured output.
50
- * The outputSchema is already JSON Schema (converted in the ai layer).
51
- */
52
- async structuredOutput(options) {
53
- const { chatOptions, outputSchema } = options;
54
- const { logger } = chatOptions;
55
- const mappedOptions = this.mapCommonOptionsToGemini(chatOptions);
56
- try {
57
- logger.request(
58
- `activity=chat provider=gemini model=${this.model} messages=${chatOptions.messages.length} tools=${chatOptions.tools?.length ?? 0} stream=false`,
59
- { provider: "gemini", model: this.model }
60
- );
61
- const result = await this.client.models.generateContent({
62
- ...mappedOptions,
63
- config: {
64
- ...mappedOptions.config,
65
- responseMimeType: "application/json",
66
- responseSchema: outputSchema
67
- }
68
- });
69
- const rawText = this.extractTextFromResponse(result);
70
- let parsed;
71
- try {
72
- parsed = JSON.parse(rawText);
73
- } catch {
74
- throw new Error(
75
- `Failed to parse structured output as JSON. Content: ${rawText.slice(0, 200)}${rawText.length > 200 ? "..." : ""}`
76
- );
77
- }
78
- return {
79
- data: parsed,
80
- rawText,
81
- usage: result.usageMetadata ? buildGeminiUsage(result.usageMetadata) : void 0
82
- };
83
- } catch (error) {
84
- logger.errors("gemini.structuredOutput fatal", {
85
- error,
86
- source: "gemini.structuredOutput"
87
- });
88
- throw new Error(
89
- error instanceof Error ? error.message : "An unknown error occurred during structured output generation."
90
- );
91
- }
92
- }
93
- /**
94
- * Extract text content from a non-streaming response
95
- */
96
- extractTextFromResponse(response) {
97
- let textContent = "";
98
- if (response.candidates?.[0]?.content?.parts) {
99
- for (const part of response.candidates[0].content.parts) {
100
- if (part.text) {
101
- textContent += part.text;
102
- }
103
- }
104
- }
105
- return textContent;
106
- }
107
- async *processStreamChunks(result, options, logger) {
108
- const model = options.model;
109
- let accumulatedContent = "";
110
- let accumulatedThinking = "";
111
- const toolCallMap = /* @__PURE__ */ new Map();
112
- let nextToolIndex = 0;
113
- const runId = options.runId ?? generateId(this.name);
114
- const threadId = options.threadId ?? generateId(this.name);
115
- const messageId = generateId(this.name);
116
- let stepId = null;
117
- let reasoningMessageId = null;
118
- let hasClosedReasoning = false;
119
- let hasEmittedRunStarted = false;
120
- let hasEmittedTextMessageStart = false;
121
- let hasEmittedStepStarted = false;
122
- for await (const chunk of result) {
123
- logger.provider(`provider=gemini`, { chunk });
124
- if (!hasEmittedRunStarted) {
125
- hasEmittedRunStarted = true;
126
- yield {
127
- type: EventType.RUN_STARTED,
128
- runId,
129
- threadId,
130
- model,
131
- timestamp: Date.now(),
132
- parentRunId: options.parentRunId
133
- };
134
- }
135
- if (chunk.candidates?.[0]?.content?.parts) {
136
- const parts = chunk.candidates[0].content.parts;
137
- for (const part of parts) {
138
- if (part.text) {
139
- if (part.thought) {
140
- if (!hasEmittedStepStarted) {
141
- hasEmittedStepStarted = true;
142
- stepId = generateId(this.name);
143
- reasoningMessageId = generateId(this.name);
144
- yield {
145
- type: EventType.REASONING_START,
146
- messageId: reasoningMessageId,
147
- model,
148
- timestamp: Date.now()
149
- };
150
- yield {
151
- type: EventType.REASONING_MESSAGE_START,
152
- messageId: reasoningMessageId,
153
- role: "reasoning",
154
- model,
155
- timestamp: Date.now()
156
- };
157
- yield {
158
- type: EventType.STEP_STARTED,
159
- stepName: stepId,
160
- stepId,
161
- model,
162
- timestamp: Date.now(),
163
- stepType: "thinking"
164
- };
165
- }
166
- accumulatedThinking += part.text;
167
- if (!reasoningMessageId) continue;
168
- yield {
169
- type: EventType.REASONING_MESSAGE_CONTENT,
170
- messageId: reasoningMessageId,
171
- delta: part.text,
172
- model,
173
- timestamp: Date.now()
174
- };
175
- yield {
176
- type: EventType.STEP_FINISHED,
177
- stepName: stepId || generateId(this.name),
178
- stepId: stepId || generateId(this.name),
179
- model,
180
- timestamp: Date.now(),
181
- delta: part.text,
182
- content: accumulatedThinking
183
- };
184
- } else if (part.text.trim()) {
185
- if (reasoningMessageId && !hasClosedReasoning) {
186
- hasClosedReasoning = true;
187
- yield {
188
- type: EventType.REASONING_MESSAGE_END,
189
- messageId: reasoningMessageId,
190
- model,
191
- timestamp: Date.now()
192
- };
193
- yield {
194
- type: EventType.REASONING_END,
195
- messageId: reasoningMessageId,
196
- model,
197
- timestamp: Date.now()
198
- };
199
- }
200
- if (!hasEmittedTextMessageStart) {
201
- hasEmittedTextMessageStart = true;
202
- yield {
203
- type: EventType.TEXT_MESSAGE_START,
204
- messageId,
205
- model,
206
- timestamp: Date.now(),
207
- role: "assistant"
208
- };
209
- }
210
- accumulatedContent += part.text;
211
- yield {
212
- type: EventType.TEXT_MESSAGE_CONTENT,
213
- messageId,
214
- model,
215
- timestamp: Date.now(),
216
- delta: part.text,
217
- content: accumulatedContent
218
- };
219
- }
220
- }
221
- const functionCall = part.functionCall;
222
- if (functionCall) {
223
- const toolCallId = functionCall.id || `${functionCall.name}_${Date.now()}_${nextToolIndex}`;
224
- const functionArgs = functionCall.args || {};
225
- const partThoughtSignature = part.thoughtSignature || void 0;
226
- let toolCallData = toolCallMap.get(toolCallId);
227
- if (!toolCallData) {
228
- toolCallData = {
229
- name: functionCall.name || "",
230
- args: typeof functionArgs === "string" ? functionArgs : JSON.stringify(functionArgs),
231
- index: nextToolIndex++,
232
- started: false,
233
- // Only set thoughtSignature when present — under EOPT, the
234
- // optional field cannot accept an explicit `undefined`.
235
- ...partThoughtSignature !== void 0 && {
236
- thoughtSignature: partThoughtSignature
237
- }
238
- };
239
- toolCallMap.set(toolCallId, toolCallData);
240
- } else {
241
- if (!toolCallData.thoughtSignature && partThoughtSignature) {
242
- toolCallData.thoughtSignature = partThoughtSignature;
243
- }
244
- try {
245
- const existingArgs = JSON.parse(toolCallData.args);
246
- const newArgs = typeof functionArgs === "string" ? JSON.parse(functionArgs) : functionArgs;
247
- const mergedArgs = { ...existingArgs, ...newArgs };
248
- toolCallData.args = JSON.stringify(mergedArgs);
249
- } catch {
250
- toolCallData.args = typeof functionArgs === "string" ? functionArgs : JSON.stringify(functionArgs);
251
- }
252
- }
253
- if (!toolCallData.started) {
254
- toolCallData.started = true;
255
- yield {
256
- type: EventType.TOOL_CALL_START,
257
- toolCallId,
258
- toolCallName: toolCallData.name,
259
- toolName: toolCallData.name,
260
- parentMessageId: messageId,
261
- model,
262
- timestamp: Date.now(),
263
- index: toolCallData.index,
264
- ...toolCallData.thoughtSignature && {
265
- metadata: {
266
- thoughtSignature: toolCallData.thoughtSignature
267
- }
268
- }
269
- };
270
- }
271
- yield {
272
- type: EventType.TOOL_CALL_ARGS,
273
- toolCallId,
274
- model,
275
- timestamp: Date.now(),
276
- delta: toolCallData.args,
277
- args: toolCallData.args
278
- };
279
- }
280
- }
281
- } else if (chunk.data && chunk.data.trim()) {
282
- if (!hasEmittedTextMessageStart) {
283
- hasEmittedTextMessageStart = true;
284
- yield {
285
- type: EventType.TEXT_MESSAGE_START,
286
- messageId,
287
- model,
288
- timestamp: Date.now(),
289
- role: "assistant"
290
- };
291
- }
292
- accumulatedContent += chunk.data;
293
- yield {
294
- type: EventType.TEXT_MESSAGE_CONTENT,
295
- messageId,
296
- model,
297
- timestamp: Date.now(),
298
- delta: chunk.data,
299
- content: accumulatedContent
300
- };
301
- }
302
- if (chunk.candidates?.[0]?.finishReason) {
303
- const finishReason = chunk.candidates[0].finishReason;
304
- if (finishReason === FinishReason.UNEXPECTED_TOOL_CALL) {
305
- if (chunk.candidates[0].content?.parts) {
306
- for (const part of chunk.candidates[0].content.parts) {
307
- const functionCall = part.functionCall;
308
- if (functionCall) {
309
- const toolCallId = functionCall.id || `${functionCall.name}_${Date.now()}_${nextToolIndex}`;
310
- const functionArgs = functionCall.args || {};
311
- const argsString = typeof functionArgs === "string" ? functionArgs : JSON.stringify(functionArgs);
312
- toolCallMap.set(toolCallId, {
313
- name: functionCall.name || "",
314
- args: argsString,
315
- index: nextToolIndex++,
316
- started: true
317
- });
318
- yield {
319
- type: EventType.TOOL_CALL_START,
320
- toolCallId,
321
- toolCallName: functionCall.name || "",
322
- toolName: functionCall.name || "",
323
- parentMessageId: messageId,
324
- model,
325
- timestamp: Date.now(),
326
- index: nextToolIndex - 1
327
- };
328
- let parsedInput = {};
329
- try {
330
- const parsed = typeof functionArgs === "string" ? JSON.parse(functionArgs) : functionArgs;
331
- parsedInput = parsed && typeof parsed === "object" ? parsed : {};
332
- } catch {
333
- parsedInput = {};
334
- }
335
- yield {
336
- type: EventType.TOOL_CALL_END,
337
- toolCallId,
338
- toolCallName: functionCall.name || "",
339
- toolName: functionCall.name || "",
340
- model,
341
- timestamp: Date.now(),
342
- input: parsedInput
343
- };
344
- }
345
- }
346
- }
347
- }
348
- for (const [toolCallId, toolCallData] of toolCallMap.entries()) {
349
- let parsedInput = {};
350
- try {
351
- const parsed = JSON.parse(toolCallData.args);
352
- parsedInput = parsed && typeof parsed === "object" ? parsed : {};
353
- } catch {
354
- parsedInput = {};
355
- }
356
- yield {
357
- type: EventType.TOOL_CALL_END,
358
- toolCallId,
359
- toolCallName: toolCallData.name,
360
- toolName: toolCallData.name,
361
- model,
362
- timestamp: Date.now(),
363
- input: parsedInput
364
- };
365
- }
366
- if (toolCallMap.size > 0) {
367
- hasEmittedTextMessageStart = false;
368
- }
369
- if (finishReason === FinishReason.MAX_TOKENS) {
370
- yield {
371
- type: EventType.RUN_ERROR,
372
- runId,
373
- model,
374
- timestamp: Date.now(),
375
- message: "The response was cut off because the maximum token limit was reached.",
376
- code: "max_tokens",
377
- error: {
378
- message: "The response was cut off because the maximum token limit was reached.",
379
- code: "max_tokens"
380
- }
381
- };
382
- }
383
- if (reasoningMessageId && !hasClosedReasoning) {
384
- hasClosedReasoning = true;
385
- yield {
386
- type: EventType.REASONING_MESSAGE_END,
387
- messageId: reasoningMessageId,
388
- model,
389
- timestamp: Date.now()
390
- };
391
- yield {
392
- type: EventType.REASONING_END,
393
- messageId: reasoningMessageId,
394
- model,
395
- timestamp: Date.now()
396
- };
397
- }
398
- if (hasEmittedTextMessageStart) {
399
- yield {
400
- type: EventType.TEXT_MESSAGE_END,
401
- messageId,
402
- model,
403
- timestamp: Date.now()
404
- };
405
- }
406
- yield {
407
- type: EventType.RUN_FINISHED,
408
- runId,
409
- threadId,
410
- model,
411
- timestamp: Date.now(),
412
- finishReason: toolCallMap.size > 0 ? "tool_calls" : "stop",
413
- // RunFinishedEvent.usage is `usage?: {...}` (no `| undefined`) under
414
- // exactOptionalPropertyTypes; only include it when usageMetadata is
415
- // present rather than assigning an explicit `undefined`.
416
- ...chunk.usageMetadata && {
417
- usage: buildGeminiUsage(chunk.usageMetadata)
418
- }
419
- };
420
- }
421
- }
422
- }
423
- convertContentPartToGemini(part) {
424
- switch (part.type) {
425
- case "text":
426
- return { text: part.content };
427
- case "image":
428
- case "audio":
429
- case "video":
430
- case "document": {
431
- if (part.source.type === "data") {
432
- return {
433
- inlineData: {
434
- data: part.source.value,
435
- mimeType: part.source.mimeType
436
- }
437
- };
438
- } else {
439
- const defaultMimeType = {
440
- image: "image/jpeg",
441
- audio: "audio/mp3",
442
- video: "video/mp4",
443
- document: "application/pdf"
444
- }[part.type];
445
- return {
446
- fileData: {
447
- fileUri: part.source.value,
448
- mimeType: part.source.mimeType ?? defaultMimeType
449
- }
450
- };
451
- }
452
- }
453
- default: {
454
- const _exhaustiveCheck = part;
455
- throw new Error(
456
- `Unsupported content part type: ${_exhaustiveCheck.type}`
457
- );
458
- }
459
- }
460
- }
461
- formatMessages(messages) {
462
- const toolCallIdToName = /* @__PURE__ */ new Map();
463
- for (const msg of messages) {
464
- if (msg.role === "assistant" && msg.toolCalls) {
465
- for (const tc of msg.toolCalls) {
466
- toolCallIdToName.set(tc.id, tc.function.name);
467
- }
468
- }
469
- }
470
- const formatted = messages.map((msg) => {
471
- const role = msg.role === "assistant" ? "model" : "user";
472
- const parts = [];
473
- if (Array.isArray(msg.content)) {
474
- for (const contentPart of msg.content) {
475
- parts.push(this.convertContentPartToGemini(contentPart));
476
- }
477
- } else if (msg.content && msg.role !== "tool") {
478
- parts.push({ text: msg.content });
479
- }
480
- if (msg.role === "assistant" && msg.toolCalls?.length) {
481
- for (const toolCall of msg.toolCalls) {
482
- let parsedArgs = {};
483
- try {
484
- parsedArgs = toolCall.function.arguments ? JSON.parse(toolCall.function.arguments) : {};
485
- } catch {
486
- parsedArgs = {};
487
- }
488
- const thoughtSignature = toolCall.metadata?.thoughtSignature;
489
- const part = {
490
- functionCall: {
491
- id: toolCall.id,
492
- name: toolCall.function.name,
493
- args: parsedArgs
494
- }
495
- };
496
- if (thoughtSignature) {
497
- part.thoughtSignature = thoughtSignature;
498
- }
499
- parts.push(part);
500
- }
501
- }
502
- if (msg.role === "tool" && msg.toolCallId) {
503
- const functionName = toolCallIdToName.get(msg.toolCallId) || msg.toolCallId;
504
- const toolContent = msg.content;
505
- if (Array.isArray(toolContent)) {
506
- const textChunks = [];
507
- const mediaParts = [];
508
- for (const part of toolContent) {
509
- if (part.type === "text") {
510
- textChunks.push(part.content);
511
- } else if (part.source.type === "data") {
512
- mediaParts.push({
513
- inlineData: {
514
- data: part.source.value,
515
- mimeType: part.source.mimeType
516
- }
517
- });
518
- } else {
519
- const defaultMimeType = {
520
- image: "image/jpeg",
521
- audio: "audio/mp3",
522
- video: "video/mp4",
523
- document: "application/pdf"
524
- }[part.type];
525
- mediaParts.push({
526
- fileData: {
527
- fileUri: part.source.value,
528
- mimeType: part.source.mimeType ?? defaultMimeType
529
- }
530
- });
531
- }
532
- }
533
- parts.push({
534
- functionResponse: {
535
- id: msg.toolCallId,
536
- name: functionName,
537
- response: { content: textChunks.join("\n") },
538
- ...mediaParts.length > 0 && { parts: mediaParts }
539
- }
540
- });
541
- } else {
542
- parts.push({
543
- functionResponse: {
544
- id: msg.toolCallId,
545
- name: functionName,
546
- response: { content: toolContent || "" }
547
- }
548
- });
549
- }
550
- }
551
- return {
552
- role,
553
- parts: parts.length > 0 ? parts : [{ text: "" }]
554
- };
555
- });
556
- return this.mergeConsecutiveSameRoleMessages(formatted);
557
- }
558
- /**
559
- * Merge consecutive messages of the same role into a single message.
560
- * Gemini's API requires strictly alternating user/model roles.
561
- * Tool results are mapped to role:'user', which can collide with actual
562
- * user messages in multi-turn conversations.
563
- *
564
- * Also filters out empty model messages (e.g., from a previous failed request)
565
- * and deduplicates functionResponse parts with the same name (tool call ID).
566
- */
567
- mergeConsecutiveSameRoleMessages(messages) {
568
- const merged = [];
569
- for (const msg of messages) {
570
- const parts = msg.parts || [];
571
- if (msg.role === "model") {
572
- const hasContent = parts.length > 0 && !parts.every(
573
- (p) => "text" in p && p.text === ""
574
- );
575
- if (!hasContent) {
576
- continue;
577
- }
578
- }
579
- const prev = merged[merged.length - 1];
580
- if (prev && prev.role === msg.role) {
581
- prev.parts = [...prev.parts || [], ...parts];
582
- } else {
583
- merged.push({ ...msg, parts: [...parts] });
584
- }
585
- }
586
- for (const msg of merged) {
587
- if (!msg.parts) continue;
588
- const seenFunctionResponseNames = /* @__PURE__ */ new Set();
589
- msg.parts = msg.parts.filter((part) => {
590
- if ("functionResponse" in part && part.functionResponse?.name) {
591
- if (seenFunctionResponseNames.has(part.functionResponse.name)) {
592
- return false;
593
- }
594
- seenFunctionResponseNames.add(part.functionResponse.name);
595
- }
596
- return true;
597
- });
598
- }
599
- return merged;
600
- }
601
- mapCommonOptionsToGemini(options) {
602
- const { thinkingConfig, ...modelOpts } = options.modelOptions ?? {};
603
- const mappedThinkingConfig = thinkingConfig ? {
604
- ...thinkingConfig.includeThoughts !== void 0 && {
605
- includeThoughts: thinkingConfig.includeThoughts
606
- },
607
- ...thinkingConfig.thinkingBudget !== void 0 && {
608
- thinkingBudget: thinkingConfig.thinkingBudget
609
- },
610
- ...thinkingConfig.thinkingLevel ? {
611
- thinkingLevel: thinkingConfig.thinkingLevel
612
- } : {}
613
- } : void 0;
614
- const normalizedPrompts = normalizeSystemPrompts(options.systemPrompts);
615
- const systemInstruction = normalizedPrompts.length > 0 ? normalizedPrompts.map((p) => p.content).join("\n") : void 0;
616
- const combinedSchema = options.outputSchema;
617
- const combinedSchemaConfig = combinedSchema ? {
618
- responseMimeType: "application/json",
619
- responseSchema: combinedSchema
620
- } : void 0;
621
- const requestOptions = {
622
- model: options.model,
623
- contents: this.formatMessages(options.messages),
624
- config: {
625
- ...modelOpts,
626
- ...mappedThinkingConfig !== void 0 && {
627
- thinkingConfig: mappedThinkingConfig
628
- },
629
- ...systemInstruction !== void 0 && { systemInstruction },
630
- tools: convertToolsToProviderFormat(options.tools),
631
- ...combinedSchemaConfig ?? {}
632
- }
633
- };
634
- return requestOptions;
635
- }
636
- /**
637
- * Gemini 3.x natively combines `tools` + `responseSchema` in a single
638
- * streaming `generateContentStream` call (issue #605). Gemini 2.x is
639
- * documented as brittle for the combination and keeps the engine's
640
- * legacy finalization path.
641
- */
642
- supportsCombinedToolsAndSchema() {
643
- return GEMINI_COMBINED_TOOLS_AND_SCHEMA_MODELS.has(this.model);
644
- }
645
- }
646
- function createGeminiChat(model, apiKey, config) {
647
- return new GeminiTextAdapter({ apiKey, ...config }, model);
648
- }
649
- function geminiText(model, config) {
650
- const apiKey = getGeminiApiKeyFromEnv();
651
- return createGeminiChat(model, apiKey, config);
652
- }
653
- export {
654
- GeminiTextAdapter,
655
- createGeminiChat,
656
- geminiText
657
- };
658
- //# sourceMappingURL=text.js.map