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