@juspay/neurolink 8.26.0 → 8.26.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 (92) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +47 -25
  3. package/dist/adapters/providerImageAdapter.js +11 -0
  4. package/dist/cli/commands/config.js +16 -23
  5. package/dist/cli/commands/setup-anthropic.js +3 -26
  6. package/dist/cli/commands/setup-azure.js +3 -22
  7. package/dist/cli/commands/setup-bedrock.js +3 -26
  8. package/dist/cli/commands/setup-google-ai.js +3 -22
  9. package/dist/cli/commands/setup-mistral.js +3 -31
  10. package/dist/cli/commands/setup-openai.js +3 -22
  11. package/dist/cli/factories/commandFactory.js +32 -0
  12. package/dist/cli/factories/ollamaCommandFactory.js +5 -17
  13. package/dist/cli/loop/optionsSchema.d.ts +1 -1
  14. package/dist/cli/loop/optionsSchema.js +13 -0
  15. package/dist/config/modelSpecificPrompts.d.ts +9 -0
  16. package/dist/config/modelSpecificPrompts.js +38 -0
  17. package/dist/constants/enums.d.ts +8 -0
  18. package/dist/constants/enums.js +8 -0
  19. package/dist/constants/tokens.d.ts +25 -0
  20. package/dist/constants/tokens.js +18 -0
  21. package/dist/core/analytics.js +7 -28
  22. package/dist/core/baseProvider.js +1 -0
  23. package/dist/core/constants.d.ts +1 -0
  24. package/dist/core/constants.js +1 -0
  25. package/dist/core/modules/GenerationHandler.js +43 -5
  26. package/dist/core/streamAnalytics.d.ts +1 -0
  27. package/dist/core/streamAnalytics.js +8 -16
  28. package/dist/lib/adapters/providerImageAdapter.js +11 -0
  29. package/dist/lib/config/modelSpecificPrompts.d.ts +9 -0
  30. package/dist/lib/config/modelSpecificPrompts.js +39 -0
  31. package/dist/lib/constants/enums.d.ts +8 -0
  32. package/dist/lib/constants/enums.js +8 -0
  33. package/dist/lib/constants/tokens.d.ts +25 -0
  34. package/dist/lib/constants/tokens.js +18 -0
  35. package/dist/lib/core/analytics.js +7 -28
  36. package/dist/lib/core/baseProvider.js +1 -0
  37. package/dist/lib/core/constants.d.ts +1 -0
  38. package/dist/lib/core/constants.js +1 -0
  39. package/dist/lib/core/modules/GenerationHandler.js +43 -5
  40. package/dist/lib/core/streamAnalytics.d.ts +1 -0
  41. package/dist/lib/core/streamAnalytics.js +8 -16
  42. package/dist/lib/providers/googleAiStudio.d.ts +15 -0
  43. package/dist/lib/providers/googleAiStudio.js +659 -3
  44. package/dist/lib/providers/googleVertex.d.ts +25 -0
  45. package/dist/lib/providers/googleVertex.js +978 -3
  46. package/dist/lib/types/analytics.d.ts +4 -0
  47. package/dist/lib/types/cli.d.ts +16 -0
  48. package/dist/lib/types/conversation.d.ts +72 -4
  49. package/dist/lib/types/conversation.js +30 -0
  50. package/dist/lib/types/generateTypes.d.ts +135 -0
  51. package/dist/lib/types/groundingTypes.d.ts +231 -0
  52. package/dist/lib/types/groundingTypes.js +12 -0
  53. package/dist/lib/types/providers.d.ts +29 -0
  54. package/dist/lib/types/streamTypes.d.ts +54 -0
  55. package/dist/lib/utils/analyticsUtils.js +22 -2
  56. package/dist/lib/utils/modelChoices.d.ts +82 -0
  57. package/dist/lib/utils/modelChoices.js +402 -0
  58. package/dist/lib/utils/modelDetection.d.ts +9 -0
  59. package/dist/lib/utils/modelDetection.js +81 -0
  60. package/dist/lib/utils/schemaConversion.d.ts +12 -0
  61. package/dist/lib/utils/schemaConversion.js +90 -0
  62. package/dist/lib/utils/thinkingConfig.d.ts +108 -0
  63. package/dist/lib/utils/thinkingConfig.js +105 -0
  64. package/dist/lib/utils/tokenUtils.d.ts +124 -0
  65. package/dist/lib/utils/tokenUtils.js +240 -0
  66. package/dist/lib/utils/transformationUtils.js +15 -26
  67. package/dist/providers/googleAiStudio.d.ts +15 -0
  68. package/dist/providers/googleAiStudio.js +659 -3
  69. package/dist/providers/googleVertex.d.ts +25 -0
  70. package/dist/providers/googleVertex.js +978 -3
  71. package/dist/types/analytics.d.ts +4 -0
  72. package/dist/types/cli.d.ts +16 -0
  73. package/dist/types/conversation.d.ts +72 -4
  74. package/dist/types/conversation.js +30 -0
  75. package/dist/types/generateTypes.d.ts +135 -0
  76. package/dist/types/groundingTypes.d.ts +231 -0
  77. package/dist/types/groundingTypes.js +11 -0
  78. package/dist/types/providers.d.ts +29 -0
  79. package/dist/types/streamTypes.d.ts +54 -0
  80. package/dist/utils/analyticsUtils.js +22 -2
  81. package/dist/utils/modelChoices.d.ts +82 -0
  82. package/dist/utils/modelChoices.js +401 -0
  83. package/dist/utils/modelDetection.d.ts +9 -0
  84. package/dist/utils/modelDetection.js +80 -0
  85. package/dist/utils/schemaConversion.d.ts +12 -0
  86. package/dist/utils/schemaConversion.js +90 -0
  87. package/dist/utils/thinkingConfig.d.ts +108 -0
  88. package/dist/utils/thinkingConfig.js +104 -0
  89. package/dist/utils/tokenUtils.d.ts +124 -0
  90. package/dist/utils/tokenUtils.js +239 -0
  91. package/dist/utils/transformationUtils.js +15 -26
  92. package/package.json +4 -3
@@ -1,12 +1,16 @@
1
1
  import { createGoogleGenerativeAI } from "@ai-sdk/google";
2
2
  import { streamText } from "ai";
3
- import { AIProviderName, GoogleAIModels } from "../constants/enums.js";
3
+ import { AIProviderName, GoogleAIModels, ErrorCategory, ErrorSeverity, } from "../constants/enums.js";
4
+ import { NeuroLinkError, ERROR_CODES } from "../utils/errorHandling.js";
4
5
  import { BaseProvider } from "../core/baseProvider.js";
5
6
  import { logger } from "../utils/logger.js";
6
7
  import { createTimeoutController, TimeoutError } from "../utils/timeout.js";
7
8
  import { AuthenticationError, NetworkError, ProviderError, RateLimitError, } from "../types/errors.js";
8
- import { DEFAULT_MAX_STEPS } from "../core/constants.js";
9
+ import { DEFAULT_MAX_STEPS, DEFAULT_TOOL_MAX_RETRIES, } from "../core/constants.js";
9
10
  import { streamAnalyticsCollector } from "../core/streamAnalytics.js";
11
+ import { isGemini3Model } from "../utils/modelDetection.js";
12
+ import { convertZodToJsonSchema, inlineJsonSchema, isZodSchema, } from "../utils/schemaConversion.js";
13
+ import { createNativeThinkingConfig } from "../utils/thinkingConfig.js";
10
14
  // Google AI Live API types now imported from ../types/providerSpecific.js
11
15
  // Import proper types for multimodal message handling
12
16
  // Create Google GenAI client
@@ -14,7 +18,14 @@ async function createGoogleGenAIClient(apiKey) {
14
18
  const mod = await import("@google/genai");
15
19
  const ctor = mod.GoogleGenAI;
16
20
  if (!ctor) {
17
- throw new Error("@google/genai does not export GoogleGenAI");
21
+ throw new NeuroLinkError({
22
+ code: ERROR_CODES.INVALID_CONFIGURATION,
23
+ message: "@google/genai does not export GoogleGenAI",
24
+ category: ErrorCategory.CONFIGURATION,
25
+ severity: ErrorSeverity.CRITICAL,
26
+ retriable: false,
27
+ context: { module: "@google/genai", expectedExport: "GoogleGenAI" },
28
+ });
18
29
  }
19
30
  const Ctor = ctor;
20
31
  return new Ctor({ apiKey });
@@ -99,6 +110,29 @@ export class GoogleAIStudioProvider extends BaseProvider {
99
110
  }
100
111
  // executeGenerate removed - BaseProvider handles all generation with tools
101
112
  async executeStream(options, _analysisSchema) {
113
+ // Check if this is a Gemini 3 model with tools - use native SDK for thought_signature
114
+ const gemini3CheckModelName = options.model || this.modelName;
115
+ // Check for tools from options AND from SDK (MCP tools)
116
+ // Need to check early if we should route to native SDK
117
+ const gemini3CheckShouldUseTools = !options.disableTools && this.supportsTools();
118
+ const optionTools = options.tools || {};
119
+ const sdkTools = gemini3CheckShouldUseTools ? await this.getAllTools() : {};
120
+ const combinedToolCount = Object.keys(optionTools).length + Object.keys(sdkTools).length;
121
+ const hasTools = gemini3CheckShouldUseTools && combinedToolCount > 0;
122
+ if (isGemini3Model(gemini3CheckModelName) && hasTools) {
123
+ // Merge SDK tools into options for native SDK path
124
+ const mergedOptions = {
125
+ ...options,
126
+ tools: { ...sdkTools, ...optionTools },
127
+ };
128
+ logger.info("[GoogleAIStudio] Routing Gemini 3 to native SDK for tool calling", {
129
+ model: gemini3CheckModelName,
130
+ optionToolCount: Object.keys(optionTools).length,
131
+ sdkToolCount: Object.keys(sdkTools).length,
132
+ totalToolCount: combinedToolCount,
133
+ });
134
+ return this.executeNativeGemini3Stream(mergedOptions);
135
+ }
102
136
  // Phase 1: if audio input present, bridge to Gemini Live (Studio) using @google/genai
103
137
  if (options.input?.audio) {
104
138
  return await this.executeAudioStreamViaGeminiLive(options);
@@ -130,6 +164,24 @@ export class GoogleAIStudioProvider extends BaseProvider {
130
164
  toolChoice: shouldUseTools ? "auto" : "none",
131
165
  abortSignal: timeoutController?.controller.signal,
132
166
  experimental_telemetry: this.telemetryHandler.getTelemetryConfig(options),
167
+ // Gemini 3: use thinkingLevel via providerOptions
168
+ // Gemini 2.5: use thinkingBudget via providerOptions
169
+ ...(options.thinkingConfig?.enabled && {
170
+ providerOptions: {
171
+ google: {
172
+ thinkingConfig: {
173
+ ...(options.thinkingConfig.thinkingLevel && {
174
+ thinkingLevel: options.thinkingConfig.thinkingLevel,
175
+ }),
176
+ ...(options.thinkingConfig.budgetTokens &&
177
+ !options.thinkingConfig.thinkingLevel && {
178
+ thinkingBudget: options.thinkingConfig.budgetTokens,
179
+ }),
180
+ includeThoughts: true,
181
+ },
182
+ },
183
+ },
184
+ }),
133
185
  onStepFinish: ({ toolCalls, toolResults }) => {
134
186
  this.handleToolExecutionStorage(toolCalls, toolResults, options, new Date()).catch((error) => {
135
187
  logger.warn("[GoogleAiStudioProvider] Failed to store tool executions", {
@@ -163,6 +215,610 @@ export class GoogleAIStudioProvider extends BaseProvider {
163
215
  throw this.handleProviderError(error);
164
216
  }
165
217
  }
218
+ /**
219
+ * Execute stream using native @google/genai SDK for Gemini 3 models
220
+ * This bypasses @ai-sdk/google to properly handle thought_signature
221
+ */
222
+ async executeNativeGemini3Stream(options) {
223
+ const startTime = Date.now();
224
+ const timeout = this.getTimeout(options);
225
+ const timeoutController = createTimeoutController(timeout, this.providerName, "stream");
226
+ const apiKey = this.getApiKey();
227
+ const client = await createGoogleGenAIClient(apiKey);
228
+ const modelName = options.model || this.modelName;
229
+ logger.debug("[GoogleAIStudio] Using native @google/genai for Gemini 3", {
230
+ model: modelName,
231
+ hasTools: !!options.tools && Object.keys(options.tools).length > 0,
232
+ });
233
+ // Build contents from input
234
+ const contents = [];
235
+ contents.push({
236
+ role: "user",
237
+ parts: [{ text: options.input.text }],
238
+ });
239
+ let tools;
240
+ const executeMap = new Map();
241
+ if (options.tools &&
242
+ Object.keys(options.tools).length > 0 &&
243
+ !options.disableTools) {
244
+ const functionDeclarations = [];
245
+ for (const [name, tool] of Object.entries(options.tools)) {
246
+ const decl = {
247
+ name,
248
+ description: tool.description || `Tool: ${name}`,
249
+ };
250
+ if (tool.parameters) {
251
+ let rawSchema;
252
+ if (isZodSchema(tool.parameters)) {
253
+ // It's a Zod schema - convert it
254
+ rawSchema = convertZodToJsonSchema(tool.parameters);
255
+ }
256
+ else if (typeof tool.parameters === "object") {
257
+ // Already JSON schema (jsonSchema() wrapper) - use directly
258
+ rawSchema = tool.parameters;
259
+ }
260
+ else {
261
+ rawSchema = { type: "object", properties: {} };
262
+ }
263
+ decl.parametersJsonSchema = inlineJsonSchema(rawSchema);
264
+ // Remove $schema if present - @google/genai doesn't need it
265
+ if (decl.parametersJsonSchema.$schema) {
266
+ delete decl.parametersJsonSchema.$schema;
267
+ }
268
+ }
269
+ functionDeclarations.push(decl);
270
+ if (tool.execute) {
271
+ executeMap.set(name, tool.execute);
272
+ }
273
+ }
274
+ tools = [{ functionDeclarations }];
275
+ logger.debug("[GoogleAIStudio] Converted tools for native SDK", {
276
+ toolCount: functionDeclarations.length,
277
+ toolNames: functionDeclarations.map((t) => t.name),
278
+ });
279
+ }
280
+ // Build config
281
+ const config = {
282
+ temperature: options.temperature ?? 1.0, // Gemini 3 requires 1.0 for tool calling
283
+ maxOutputTokens: options.maxTokens,
284
+ };
285
+ if (tools) {
286
+ config.tools = tools;
287
+ }
288
+ if (options.systemPrompt) {
289
+ config.systemInstruction = options.systemPrompt;
290
+ }
291
+ // Add thinking config for Gemini 3
292
+ const nativeThinkingConfig = createNativeThinkingConfig(options.thinkingConfig);
293
+ if (nativeThinkingConfig) {
294
+ config.thinkingConfig = nativeThinkingConfig;
295
+ }
296
+ // Ensure maxSteps is a valid positive integer to prevent infinite loops
297
+ const rawMaxSteps = options.maxSteps || DEFAULT_MAX_STEPS;
298
+ const maxSteps = Number.isFinite(rawMaxSteps) && rawMaxSteps > 0
299
+ ? Math.min(Math.floor(rawMaxSteps), 100) // Cap at 100 for safety
300
+ : Math.min(DEFAULT_MAX_STEPS, 100);
301
+ const currentContents = [...contents];
302
+ let finalText = "";
303
+ let lastStepText = ""; // Track text from last step for maxSteps termination
304
+ let totalInputTokens = 0;
305
+ let totalOutputTokens = 0;
306
+ const allToolCalls = [];
307
+ let step = 0;
308
+ // Track failed tools to prevent infinite retry loops
309
+ // Key: tool name, Value: { count: retry attempts, lastError: error message }
310
+ const failedTools = new Map();
311
+ // Agentic loop for tool calling
312
+ while (step < maxSteps) {
313
+ step++;
314
+ logger.debug(`[GoogleAIStudio] Native SDK step ${step}/${maxSteps}`);
315
+ try {
316
+ const stream = await client.models.generateContentStream({
317
+ model: modelName,
318
+ contents: currentContents,
319
+ config,
320
+ });
321
+ const stepFunctionCalls = [];
322
+ // Capture all raw parts including thoughtSignature for history
323
+ const rawResponseParts = [];
324
+ for await (const chunk of stream) {
325
+ // Extract raw parts from candidates FIRST
326
+ // This avoids using chunk.text which triggers SDK warning when
327
+ // non-text parts (thoughtSignature, functionCall) are present
328
+ const chunkRecord = chunk;
329
+ const candidates = chunkRecord.candidates;
330
+ const firstCandidate = candidates?.[0];
331
+ const chunkContent = firstCandidate?.content;
332
+ if (chunkContent && Array.isArray(chunkContent.parts)) {
333
+ rawResponseParts.push(...chunkContent.parts);
334
+ }
335
+ if (chunk.functionCalls) {
336
+ stepFunctionCalls.push(...chunk.functionCalls);
337
+ }
338
+ // Accumulate usage metadata from chunks
339
+ const usage = chunkRecord.usageMetadata;
340
+ if (usage) {
341
+ totalInputTokens = Math.max(totalInputTokens, usage.promptTokenCount || 0);
342
+ totalOutputTokens = Math.max(totalOutputTokens, usage.candidatesTokenCount || 0);
343
+ }
344
+ }
345
+ // Extract text from raw parts after stream completes
346
+ // This avoids SDK warning about non-text parts (thoughtSignature, functionCall)
347
+ const stepText = rawResponseParts
348
+ .filter((part) => typeof part.text === "string")
349
+ .map((part) => part.text)
350
+ .join("");
351
+ // If no function calls, we're done
352
+ if (stepFunctionCalls.length === 0) {
353
+ finalText = stepText;
354
+ break;
355
+ }
356
+ // Track the last step text for maxSteps termination
357
+ lastStepText = stepText;
358
+ // Execute function calls
359
+ logger.debug(`[GoogleAIStudio] Executing ${stepFunctionCalls.length} function calls`);
360
+ // Add model response with ALL parts (including thoughtSignature) to history
361
+ currentContents.push({
362
+ role: "model",
363
+ parts: rawResponseParts.length > 0
364
+ ? rawResponseParts
365
+ : stepFunctionCalls.map((fc) => ({
366
+ functionCall: fc,
367
+ })),
368
+ });
369
+ // Execute each function and collect responses
370
+ const functionResponses = [];
371
+ for (const call of stepFunctionCalls) {
372
+ allToolCalls.push({ toolName: call.name, args: call.args });
373
+ // Check if this tool has already exceeded retry limit
374
+ const failedInfo = failedTools.get(call.name);
375
+ if (failedInfo && failedInfo.count >= DEFAULT_TOOL_MAX_RETRIES) {
376
+ logger.warn(`[GoogleAIStudio] Tool "${call.name}" has exceeded retry limit (${DEFAULT_TOOL_MAX_RETRIES}), skipping execution`);
377
+ functionResponses.push({
378
+ functionResponse: {
379
+ name: call.name,
380
+ response: {
381
+ error: `TOOL_PERMANENTLY_FAILED: The tool "${call.name}" has failed ${failedInfo.count} times and will not be retried. Last error: ${failedInfo.lastError}. Please proceed without using this tool or inform the user that this functionality is unavailable.`,
382
+ status: "permanently_failed",
383
+ do_not_retry: true,
384
+ },
385
+ },
386
+ });
387
+ continue;
388
+ }
389
+ const execute = executeMap.get(call.name);
390
+ if (execute) {
391
+ try {
392
+ // AI SDK Tool execute requires (args, options) - provide minimal options
393
+ const toolOptions = {
394
+ toolCallId: `${call.name}-${Date.now()}`,
395
+ messages: [],
396
+ abortSignal: undefined,
397
+ };
398
+ const result = await execute(call.args, toolOptions);
399
+ functionResponses.push({
400
+ functionResponse: { name: call.name, response: { result } },
401
+ });
402
+ }
403
+ catch (error) {
404
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
405
+ // Track this failure
406
+ const currentFailInfo = failedTools.get(call.name) || {
407
+ count: 0,
408
+ lastError: "",
409
+ };
410
+ currentFailInfo.count++;
411
+ currentFailInfo.lastError = errorMessage;
412
+ failedTools.set(call.name, currentFailInfo);
413
+ logger.warn(`[GoogleAIStudio] Tool "${call.name}" failed (attempt ${currentFailInfo.count}/${DEFAULT_TOOL_MAX_RETRIES}): ${errorMessage}`);
414
+ // Determine if this is a permanent failure
415
+ const isPermanentFailure = currentFailInfo.count >= DEFAULT_TOOL_MAX_RETRIES;
416
+ functionResponses.push({
417
+ functionResponse: {
418
+ name: call.name,
419
+ response: {
420
+ error: isPermanentFailure
421
+ ? `TOOL_PERMANENTLY_FAILED: The tool "${call.name}" has failed ${currentFailInfo.count} times with error: ${errorMessage}. This tool will not be retried. Please proceed without using this tool or inform the user that this functionality is unavailable.`
422
+ : `TOOL_EXECUTION_ERROR: ${errorMessage}. Retry attempt ${currentFailInfo.count}/${DEFAULT_TOOL_MAX_RETRIES}.`,
423
+ status: isPermanentFailure
424
+ ? "permanently_failed"
425
+ : "failed",
426
+ do_not_retry: isPermanentFailure,
427
+ retry_count: currentFailInfo.count,
428
+ max_retries: DEFAULT_TOOL_MAX_RETRIES,
429
+ },
430
+ },
431
+ });
432
+ }
433
+ }
434
+ else {
435
+ // Tool not found is a permanent error
436
+ functionResponses.push({
437
+ functionResponse: {
438
+ name: call.name,
439
+ response: {
440
+ error: `TOOL_NOT_FOUND: The tool "${call.name}" does not exist. Do not attempt to call this tool again.`,
441
+ status: "permanently_failed",
442
+ do_not_retry: true,
443
+ },
444
+ },
445
+ });
446
+ }
447
+ }
448
+ // Add function responses to history
449
+ currentContents.push({
450
+ role: "function",
451
+ parts: functionResponses,
452
+ });
453
+ }
454
+ catch (error) {
455
+ logger.error("[GoogleAIStudio] Native SDK error", error);
456
+ throw this.handleProviderError(error);
457
+ }
458
+ }
459
+ timeoutController?.cleanup();
460
+ // Handle maxSteps termination - if we exited the loop due to maxSteps being reached
461
+ if (step >= maxSteps && !finalText) {
462
+ logger.warn(`[GoogleAIStudio] Tool call loop terminated after reaching maxSteps (${maxSteps}). ` +
463
+ `Model was still calling tools. Using accumulated text from last step.`);
464
+ finalText =
465
+ lastStepText ||
466
+ `[Tool execution limit reached after ${maxSteps} steps. The model continued requesting tool calls beyond the limit.]`;
467
+ }
468
+ const responseTime = Date.now() - startTime;
469
+ // Create async iterable for streaming result
470
+ async function* createTextStream() {
471
+ yield { content: finalText };
472
+ }
473
+ return {
474
+ stream: createTextStream(),
475
+ provider: this.providerName,
476
+ model: modelName,
477
+ toolCalls: allToolCalls.map((tc) => ({
478
+ toolName: tc.toolName,
479
+ args: tc.args,
480
+ })),
481
+ analytics: Promise.resolve({
482
+ provider: this.providerName,
483
+ model: modelName,
484
+ tokenUsage: {
485
+ input: totalInputTokens,
486
+ output: totalOutputTokens,
487
+ total: totalInputTokens + totalOutputTokens,
488
+ },
489
+ requestDuration: responseTime,
490
+ timestamp: new Date().toISOString(),
491
+ }),
492
+ metadata: {
493
+ streamId: `native-${Date.now()}`,
494
+ startTime,
495
+ responseTime,
496
+ totalToolExecutions: allToolCalls.length,
497
+ },
498
+ };
499
+ }
500
+ /**
501
+ * Execute generate using native @google/genai SDK for Gemini 3 models
502
+ * This bypasses @ai-sdk/google to properly handle thought_signature
503
+ */
504
+ async executeNativeGemini3Generate(options) {
505
+ const apiKey = this.getApiKey();
506
+ const client = await createGoogleGenAIClient(apiKey);
507
+ const modelName = options.model || this.modelName;
508
+ logger.debug("[GoogleAIStudio] Using native @google/genai for Gemini 3 generate", {
509
+ model: modelName,
510
+ hasTools: !!options.tools && Object.keys(options.tools).length > 0,
511
+ });
512
+ // Build contents from input
513
+ const contents = [];
514
+ const promptText = options.prompt || options.input?.text || "";
515
+ contents.push({
516
+ role: "user",
517
+ parts: [{ text: promptText }],
518
+ });
519
+ let tools;
520
+ const executeMap = new Map();
521
+ const allToolsForResult = {};
522
+ // Merge SDK tools with options.tools
523
+ const shouldUseTools = !options.disableTools;
524
+ if (shouldUseTools) {
525
+ const sdkTools = await this.getAllTools();
526
+ const mergedTools = { ...sdkTools, ...(options.tools || {}) };
527
+ if (Object.keys(mergedTools).length > 0) {
528
+ const functionDeclarations = [];
529
+ for (const [name, tool] of Object.entries(mergedTools)) {
530
+ allToolsForResult[name] = tool;
531
+ const decl = {
532
+ name,
533
+ description: tool.description || `Tool: ${name}`,
534
+ };
535
+ if (tool.parameters) {
536
+ let rawSchema;
537
+ if (isZodSchema(tool.parameters)) {
538
+ // It's a Zod schema - convert it
539
+ rawSchema = convertZodToJsonSchema(tool.parameters);
540
+ }
541
+ else if (typeof tool.parameters === "object") {
542
+ // Already JSON schema (jsonSchema() wrapper) - use directly
543
+ rawSchema = tool.parameters;
544
+ }
545
+ else {
546
+ rawSchema = { type: "object", properties: {} };
547
+ }
548
+ decl.parametersJsonSchema = inlineJsonSchema(rawSchema);
549
+ // Remove $schema if present - @google/genai doesn't need it
550
+ if (decl.parametersJsonSchema.$schema) {
551
+ delete decl.parametersJsonSchema.$schema;
552
+ }
553
+ }
554
+ functionDeclarations.push(decl);
555
+ if (tool.execute) {
556
+ executeMap.set(name, tool.execute);
557
+ }
558
+ }
559
+ tools = [{ functionDeclarations }];
560
+ logger.debug("[GoogleAIStudio] Converted tools for native SDK generate", {
561
+ toolCount: functionDeclarations.length,
562
+ toolNames: functionDeclarations.map((t) => t.name),
563
+ });
564
+ }
565
+ }
566
+ // Build config
567
+ const config = {
568
+ temperature: options.temperature ?? 1.0, // Gemini 3 requires 1.0 for tool calling
569
+ maxOutputTokens: options.maxTokens,
570
+ };
571
+ if (tools) {
572
+ config.tools = tools;
573
+ }
574
+ if (options.systemPrompt) {
575
+ config.systemInstruction = options.systemPrompt;
576
+ }
577
+ const startTime = Date.now();
578
+ // Ensure maxSteps is a valid positive integer to prevent infinite loops
579
+ const rawMaxSteps = options.maxSteps || DEFAULT_MAX_STEPS;
580
+ const maxSteps = Number.isFinite(rawMaxSteps) && rawMaxSteps > 0
581
+ ? Math.min(Math.floor(rawMaxSteps), 100) // Cap at 100 for safety
582
+ : Math.min(DEFAULT_MAX_STEPS, 100);
583
+ const currentContents = [...contents];
584
+ let finalText = "";
585
+ let lastStepText = ""; // Track text from last step for maxSteps termination
586
+ let totalInputTokens = 0;
587
+ let totalOutputTokens = 0;
588
+ const allToolCalls = [];
589
+ const toolExecutions = [];
590
+ let step = 0;
591
+ // Track failed tools to prevent infinite retry loops
592
+ // Key: tool name, Value: { count: retry attempts, lastError: error message }
593
+ const failedTools = new Map();
594
+ // Agentic loop for tool calling
595
+ while (step < maxSteps) {
596
+ step++;
597
+ logger.debug(`[GoogleAIStudio] Native SDK generate step ${step}/${maxSteps}`);
598
+ try {
599
+ const stream = await client.models.generateContentStream({
600
+ model: modelName,
601
+ contents: currentContents,
602
+ config,
603
+ });
604
+ const stepFunctionCalls = [];
605
+ // Capture all raw parts including thoughtSignature for history
606
+ const rawResponseParts = [];
607
+ for await (const chunk of stream) {
608
+ // Extract raw parts from candidates FIRST
609
+ // This avoids using chunk.text which triggers SDK warning when
610
+ // non-text parts (thoughtSignature, functionCall) are present
611
+ const chunkRecord = chunk;
612
+ const candidates = chunkRecord.candidates;
613
+ const firstCandidate = candidates?.[0];
614
+ const chunkContent = firstCandidate?.content;
615
+ if (chunkContent && Array.isArray(chunkContent.parts)) {
616
+ rawResponseParts.push(...chunkContent.parts);
617
+ }
618
+ if (chunk.functionCalls) {
619
+ stepFunctionCalls.push(...chunk.functionCalls);
620
+ }
621
+ // Accumulate usage metadata from chunks
622
+ const usage = chunkRecord.usageMetadata;
623
+ if (usage) {
624
+ totalInputTokens = Math.max(totalInputTokens, usage.promptTokenCount || 0);
625
+ totalOutputTokens = Math.max(totalOutputTokens, usage.candidatesTokenCount || 0);
626
+ }
627
+ }
628
+ // Extract text from raw parts after stream completes
629
+ // This avoids SDK warning about non-text parts (thoughtSignature, functionCall)
630
+ const stepText = rawResponseParts
631
+ .filter((part) => typeof part.text === "string")
632
+ .map((part) => part.text)
633
+ .join("");
634
+ // If no function calls, we're done
635
+ if (stepFunctionCalls.length === 0) {
636
+ finalText = stepText;
637
+ break;
638
+ }
639
+ // Track the last step text for maxSteps termination
640
+ lastStepText = stepText;
641
+ // Execute function calls
642
+ logger.debug(`[GoogleAIStudio] Executing ${stepFunctionCalls.length} function calls in generate`);
643
+ // Add model response with ALL parts (including thoughtSignature) to history
644
+ // This is critical for Gemini 3 - it requires thought signatures in subsequent turns
645
+ currentContents.push({
646
+ role: "model",
647
+ parts: rawResponseParts.length > 0
648
+ ? rawResponseParts
649
+ : stepFunctionCalls.map((fc) => ({
650
+ functionCall: fc,
651
+ })),
652
+ });
653
+ // Execute each function and collect responses
654
+ const functionResponses = [];
655
+ for (const call of stepFunctionCalls) {
656
+ allToolCalls.push({ toolName: call.name, args: call.args });
657
+ // Check if this tool has already exceeded retry limit
658
+ const failedInfo = failedTools.get(call.name);
659
+ if (failedInfo && failedInfo.count >= DEFAULT_TOOL_MAX_RETRIES) {
660
+ logger.warn(`[GoogleAIStudio] Tool "${call.name}" has exceeded retry limit (${DEFAULT_TOOL_MAX_RETRIES}), skipping execution`);
661
+ const errorOutput = {
662
+ error: `TOOL_PERMANENTLY_FAILED: The tool "${call.name}" has failed ${failedInfo.count} times and will not be retried. Last error: ${failedInfo.lastError}. Please proceed without using this tool or inform the user that this functionality is unavailable.`,
663
+ status: "permanently_failed",
664
+ do_not_retry: true,
665
+ };
666
+ functionResponses.push({
667
+ functionResponse: {
668
+ name: call.name,
669
+ response: errorOutput,
670
+ },
671
+ });
672
+ toolExecutions.push({
673
+ name: call.name,
674
+ input: call.args,
675
+ output: errorOutput,
676
+ });
677
+ continue;
678
+ }
679
+ const execute = executeMap.get(call.name);
680
+ if (execute) {
681
+ try {
682
+ // AI SDK Tool execute requires (args, options) - provide minimal options
683
+ const toolOptions = {
684
+ toolCallId: `${call.name}-${Date.now()}`,
685
+ messages: [],
686
+ abortSignal: undefined,
687
+ };
688
+ const result = await execute(call.args, toolOptions);
689
+ functionResponses.push({
690
+ functionResponse: { name: call.name, response: { result } },
691
+ });
692
+ toolExecutions.push({
693
+ name: call.name,
694
+ input: call.args,
695
+ output: result,
696
+ });
697
+ }
698
+ catch (error) {
699
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
700
+ // Track this failure
701
+ const currentFailInfo = failedTools.get(call.name) || {
702
+ count: 0,
703
+ lastError: "",
704
+ };
705
+ currentFailInfo.count++;
706
+ currentFailInfo.lastError = errorMessage;
707
+ failedTools.set(call.name, currentFailInfo);
708
+ logger.warn(`[GoogleAIStudio] Tool "${call.name}" failed (attempt ${currentFailInfo.count}/${DEFAULT_TOOL_MAX_RETRIES}): ${errorMessage}`);
709
+ // Determine if this is a permanent failure
710
+ const isPermanentFailure = currentFailInfo.count >= DEFAULT_TOOL_MAX_RETRIES;
711
+ const errorOutput = {
712
+ error: isPermanentFailure
713
+ ? `TOOL_PERMANENTLY_FAILED: The tool "${call.name}" has failed ${currentFailInfo.count} times with error: ${errorMessage}. This tool will not be retried. Please proceed without using this tool or inform the user that this functionality is unavailable.`
714
+ : `TOOL_EXECUTION_ERROR: ${errorMessage}. Retry attempt ${currentFailInfo.count}/${DEFAULT_TOOL_MAX_RETRIES}.`,
715
+ status: isPermanentFailure ? "permanently_failed" : "failed",
716
+ do_not_retry: isPermanentFailure,
717
+ retry_count: currentFailInfo.count,
718
+ max_retries: DEFAULT_TOOL_MAX_RETRIES,
719
+ };
720
+ functionResponses.push({
721
+ functionResponse: {
722
+ name: call.name,
723
+ response: errorOutput,
724
+ },
725
+ });
726
+ toolExecutions.push({
727
+ name: call.name,
728
+ input: call.args,
729
+ output: errorOutput,
730
+ });
731
+ }
732
+ }
733
+ else {
734
+ // Tool not found is a permanent error
735
+ const errorOutput = {
736
+ error: `TOOL_NOT_FOUND: The tool "${call.name}" does not exist. Do not attempt to call this tool again.`,
737
+ status: "permanently_failed",
738
+ do_not_retry: true,
739
+ };
740
+ functionResponses.push({
741
+ functionResponse: {
742
+ name: call.name,
743
+ response: errorOutput,
744
+ },
745
+ });
746
+ toolExecutions.push({
747
+ name: call.name,
748
+ input: call.args,
749
+ output: errorOutput,
750
+ });
751
+ }
752
+ }
753
+ // Add function responses to history
754
+ currentContents.push({
755
+ role: "function",
756
+ parts: functionResponses,
757
+ });
758
+ }
759
+ catch (error) {
760
+ logger.error("[GoogleAIStudio] Native SDK generate error", error);
761
+ throw this.handleProviderError(error);
762
+ }
763
+ }
764
+ // Handle maxSteps termination - if we exited the loop due to maxSteps being reached
765
+ if (step >= maxSteps && !finalText) {
766
+ logger.warn(`[GoogleAIStudio] Generate tool call loop terminated after reaching maxSteps (${maxSteps}). ` +
767
+ `Model was still calling tools. Using accumulated text from last step.`);
768
+ finalText =
769
+ lastStepText ||
770
+ `[Tool execution limit reached after ${maxSteps} steps. The model continued requesting tool calls beyond the limit.]`;
771
+ }
772
+ const responseTime = Date.now() - startTime;
773
+ // Build EnhancedGenerateResult
774
+ return {
775
+ content: finalText,
776
+ provider: this.providerName,
777
+ model: modelName,
778
+ usage: {
779
+ input: totalInputTokens,
780
+ output: totalOutputTokens,
781
+ total: totalInputTokens + totalOutputTokens,
782
+ },
783
+ responseTime,
784
+ toolsUsed: allToolCalls.map((tc) => tc.toolName),
785
+ toolExecutions: toolExecutions,
786
+ enhancedWithTools: allToolCalls.length > 0,
787
+ };
788
+ }
789
+ /**
790
+ * Override generate to route Gemini 3 models with tools to native SDK
791
+ */
792
+ async generate(optionsOrPrompt) {
793
+ // Normalize options
794
+ const options = typeof optionsOrPrompt === "string"
795
+ ? { prompt: optionsOrPrompt }
796
+ : optionsOrPrompt;
797
+ const modelName = options.model || this.modelName;
798
+ // Check if we should use native SDK for Gemini 3 with tools
799
+ const shouldUseTools = !options.disableTools && this.supportsTools();
800
+ const sdkTools = shouldUseTools ? await this.getAllTools() : {};
801
+ const hasTools = shouldUseTools &&
802
+ (Object.keys(sdkTools).length > 0 ||
803
+ (options.tools && Object.keys(options.tools).length > 0));
804
+ if (isGemini3Model(modelName) && hasTools) {
805
+ // Merge SDK tools into options for native SDK path
806
+ const mergedOptions = {
807
+ ...options,
808
+ tools: { ...sdkTools, ...(options.tools || {}) },
809
+ };
810
+ logger.info("[GoogleAIStudio] Routing Gemini 3 generate to native SDK for tool calling", {
811
+ model: modelName,
812
+ sdkToolCount: Object.keys(sdkTools).length,
813
+ optionToolCount: Object.keys(options.tools || {}).length,
814
+ totalToolCount: Object.keys(sdkTools).length +
815
+ Object.keys(options.tools || {}).length,
816
+ });
817
+ return this.executeNativeGemini3Generate(mergedOptions);
818
+ }
819
+ // Fall back to BaseProvider implementation
820
+ return super.generate(optionsOrPrompt);
821
+ }
166
822
  // ===================
167
823
  // HELPER METHODS
168
824
  // ===================