@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.
- package/CHANGELOG.md +6 -0
- package/README.md +47 -25
- package/dist/adapters/providerImageAdapter.js +11 -0
- package/dist/cli/commands/config.js +16 -23
- package/dist/cli/commands/setup-anthropic.js +3 -26
- package/dist/cli/commands/setup-azure.js +3 -22
- package/dist/cli/commands/setup-bedrock.js +3 -26
- package/dist/cli/commands/setup-google-ai.js +3 -22
- package/dist/cli/commands/setup-mistral.js +3 -31
- package/dist/cli/commands/setup-openai.js +3 -22
- package/dist/cli/factories/commandFactory.js +32 -0
- package/dist/cli/factories/ollamaCommandFactory.js +5 -17
- package/dist/cli/loop/optionsSchema.d.ts +1 -1
- package/dist/cli/loop/optionsSchema.js +13 -0
- package/dist/config/modelSpecificPrompts.d.ts +9 -0
- package/dist/config/modelSpecificPrompts.js +38 -0
- package/dist/constants/enums.d.ts +8 -0
- package/dist/constants/enums.js +8 -0
- package/dist/constants/tokens.d.ts +25 -0
- package/dist/constants/tokens.js +18 -0
- package/dist/core/analytics.js +7 -28
- package/dist/core/baseProvider.js +1 -0
- package/dist/core/constants.d.ts +1 -0
- package/dist/core/constants.js +1 -0
- package/dist/core/modules/GenerationHandler.js +43 -5
- package/dist/core/streamAnalytics.d.ts +1 -0
- package/dist/core/streamAnalytics.js +8 -16
- package/dist/lib/adapters/providerImageAdapter.js +11 -0
- package/dist/lib/config/modelSpecificPrompts.d.ts +9 -0
- package/dist/lib/config/modelSpecificPrompts.js +39 -0
- package/dist/lib/constants/enums.d.ts +8 -0
- package/dist/lib/constants/enums.js +8 -0
- package/dist/lib/constants/tokens.d.ts +25 -0
- package/dist/lib/constants/tokens.js +18 -0
- package/dist/lib/core/analytics.js +7 -28
- package/dist/lib/core/baseProvider.js +1 -0
- package/dist/lib/core/constants.d.ts +1 -0
- package/dist/lib/core/constants.js +1 -0
- package/dist/lib/core/modules/GenerationHandler.js +43 -5
- package/dist/lib/core/streamAnalytics.d.ts +1 -0
- package/dist/lib/core/streamAnalytics.js +8 -16
- package/dist/lib/providers/googleAiStudio.d.ts +15 -0
- package/dist/lib/providers/googleAiStudio.js +659 -3
- package/dist/lib/providers/googleVertex.d.ts +25 -0
- package/dist/lib/providers/googleVertex.js +978 -3
- package/dist/lib/types/analytics.d.ts +4 -0
- package/dist/lib/types/cli.d.ts +16 -0
- package/dist/lib/types/conversation.d.ts +72 -4
- package/dist/lib/types/conversation.js +30 -0
- package/dist/lib/types/generateTypes.d.ts +135 -0
- package/dist/lib/types/groundingTypes.d.ts +231 -0
- package/dist/lib/types/groundingTypes.js +12 -0
- package/dist/lib/types/providers.d.ts +29 -0
- package/dist/lib/types/streamTypes.d.ts +54 -0
- package/dist/lib/utils/analyticsUtils.js +22 -2
- package/dist/lib/utils/modelChoices.d.ts +82 -0
- package/dist/lib/utils/modelChoices.js +402 -0
- package/dist/lib/utils/modelDetection.d.ts +9 -0
- package/dist/lib/utils/modelDetection.js +81 -0
- package/dist/lib/utils/schemaConversion.d.ts +12 -0
- package/dist/lib/utils/schemaConversion.js +90 -0
- package/dist/lib/utils/thinkingConfig.d.ts +108 -0
- package/dist/lib/utils/thinkingConfig.js +105 -0
- package/dist/lib/utils/tokenUtils.d.ts +124 -0
- package/dist/lib/utils/tokenUtils.js +240 -0
- package/dist/lib/utils/transformationUtils.js +15 -26
- package/dist/providers/googleAiStudio.d.ts +15 -0
- package/dist/providers/googleAiStudio.js +659 -3
- package/dist/providers/googleVertex.d.ts +25 -0
- package/dist/providers/googleVertex.js +978 -3
- package/dist/types/analytics.d.ts +4 -0
- package/dist/types/cli.d.ts +16 -0
- package/dist/types/conversation.d.ts +72 -4
- package/dist/types/conversation.js +30 -0
- package/dist/types/generateTypes.d.ts +135 -0
- package/dist/types/groundingTypes.d.ts +231 -0
- package/dist/types/groundingTypes.js +11 -0
- package/dist/types/providers.d.ts +29 -0
- package/dist/types/streamTypes.d.ts +54 -0
- package/dist/utils/analyticsUtils.js +22 -2
- package/dist/utils/modelChoices.d.ts +82 -0
- package/dist/utils/modelChoices.js +401 -0
- package/dist/utils/modelDetection.d.ts +9 -0
- package/dist/utils/modelDetection.js +80 -0
- package/dist/utils/schemaConversion.d.ts +12 -0
- package/dist/utils/schemaConversion.js +90 -0
- package/dist/utils/thinkingConfig.d.ts +108 -0
- package/dist/utils/thinkingConfig.js +104 -0
- package/dist/utils/tokenUtils.d.ts +124 -0
- package/dist/utils/tokenUtils.js +239 -0
- package/dist/utils/transformationUtils.js +15 -26
- 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
|
|
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
|
// ===================
|