@juspay/neurolink 8.26.0 → 8.27.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.
- package/CHANGELOG.md +12 -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/providers/sagemaker/language-model.d.ts +2 -2
- 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/errorHandling.d.ts +65 -0
- package/dist/lib/utils/errorHandling.js +268 -0
- 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/parameterValidation.d.ts +59 -1
- package/dist/lib/utils/parameterValidation.js +196 -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/errorHandling.d.ts +65 -0
- package/dist/utils/errorHandling.js +268 -0
- 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/parameterValidation.d.ts +59 -1
- package/dist/utils/parameterValidation.js +196 -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,18 +1,23 @@
|
|
|
1
1
|
import { createVertex, } from "@ai-sdk/google-vertex";
|
|
2
2
|
import { createVertexAnthropic, } from "@ai-sdk/google-vertex/anthropic";
|
|
3
3
|
import { streamText, Output, } from "ai";
|
|
4
|
-
import { AIProviderName } from "../constants/enums.js";
|
|
4
|
+
import { AIProviderName, ErrorCategory, ErrorSeverity, } from "../constants/enums.js";
|
|
5
|
+
import { NeuroLinkError, ERROR_CODES } from "../utils/errorHandling.js";
|
|
5
6
|
import { BaseProvider } from "../core/baseProvider.js";
|
|
6
7
|
import { logger } from "../utils/logger.js";
|
|
7
8
|
import { createTimeoutController, TimeoutError } from "../utils/timeout.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 { ModelConfigurationManager } from "../core/modelConfiguration.js";
|
|
10
11
|
import { validateApiKey, createVertexProjectConfig, createGoogleAuthConfig, } from "../utils/providerConfig.js";
|
|
12
|
+
import { isGemini3Model } from "../utils/modelDetection.js";
|
|
13
|
+
import { convertZodToJsonSchema, inlineJsonSchema, } from "../utils/schemaConversion.js";
|
|
14
|
+
import { createNativeThinkingConfig } from "../utils/thinkingConfig.js";
|
|
11
15
|
import fs from "fs";
|
|
12
16
|
import path from "path";
|
|
13
17
|
import os from "os";
|
|
14
18
|
import dns from "dns";
|
|
15
19
|
import { createProxyFetch } from "../proxy/proxyFetch.js";
|
|
20
|
+
import { FileDetector } from "../utils/fileDetector.js";
|
|
16
21
|
// Import proper types for multimodal message handling
|
|
17
22
|
// Enhanced Anthropic support with direct imports
|
|
18
23
|
// Using the dual provider architecture from Vercel AI SDK
|
|
@@ -60,6 +65,11 @@ const createVertexSettings = async (region) => {
|
|
|
60
65
|
// instead of {region}-aiplatform.googleapis.com
|
|
61
66
|
if (location === "global") {
|
|
62
67
|
baseSettings.baseURL = `https://aiplatform.googleapis.com/v1/projects/${project}/locations/global/publishers/google`;
|
|
68
|
+
logger.debug("[GoogleVertexProvider] Using global endpoint", {
|
|
69
|
+
baseURL: baseSettings.baseURL,
|
|
70
|
+
location,
|
|
71
|
+
project,
|
|
72
|
+
});
|
|
63
73
|
}
|
|
64
74
|
// 🎯 OPTION 2: Create credentials file from environment variables at runtime
|
|
65
75
|
// This solves the problem where GOOGLE_APPLICATION_CREDENTIALS exists in ZSHRC locally
|
|
@@ -293,6 +303,11 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
293
303
|
// Initialize Google Cloud configuration
|
|
294
304
|
this.projectId = getVertexProjectId();
|
|
295
305
|
this.location = region || getVertexLocation();
|
|
306
|
+
logger.debug("[GoogleVertexProvider] Constructor initialized", {
|
|
307
|
+
regionParam: region,
|
|
308
|
+
resolvedLocation: this.location,
|
|
309
|
+
projectId: this.projectId,
|
|
310
|
+
});
|
|
296
311
|
logger.debug("Google Vertex AI BaseProvider v2 initialized", {
|
|
297
312
|
modelName: this.modelName,
|
|
298
313
|
projectId: this.projectId,
|
|
@@ -629,6 +644,31 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
629
644
|
this.validateStreamOptions(options);
|
|
630
645
|
}
|
|
631
646
|
async executeStream(options, analysisSchema) {
|
|
647
|
+
// Check if this is a Gemini 3 model with tools - use native SDK for thought_signature
|
|
648
|
+
const gemini3CheckModelName = options.model || this.modelName || getDefaultVertexModel();
|
|
649
|
+
// Check for tools from options AND from SDK (MCP tools)
|
|
650
|
+
// Need to check early if we should route to native SDK
|
|
651
|
+
const gemini3CheckShouldUseTools = !options.disableTools && this.supportsTools();
|
|
652
|
+
const optionTools = options.tools || {};
|
|
653
|
+
const sdkTools = gemini3CheckShouldUseTools ? await this.getAllTools() : {};
|
|
654
|
+
const combinedToolCount = Object.keys(optionTools).length + Object.keys(sdkTools).length;
|
|
655
|
+
const hasTools = gemini3CheckShouldUseTools && combinedToolCount > 0;
|
|
656
|
+
if (isGemini3Model(gemini3CheckModelName) && hasTools) {
|
|
657
|
+
// Process CSV files before routing to native SDK (bypasses normal message builder)
|
|
658
|
+
const processedOptions = await this.processCSVFilesForNativeSDK(options);
|
|
659
|
+
// Merge SDK tools into options for native SDK path
|
|
660
|
+
const mergedOptions = {
|
|
661
|
+
...processedOptions,
|
|
662
|
+
tools: { ...sdkTools, ...optionTools },
|
|
663
|
+
};
|
|
664
|
+
logger.info("[GoogleVertex] Routing Gemini 3 to native SDK for tool calling", {
|
|
665
|
+
model: gemini3CheckModelName,
|
|
666
|
+
optionToolCount: Object.keys(optionTools).length,
|
|
667
|
+
sdkToolCount: Object.keys(sdkTools).length,
|
|
668
|
+
totalToolCount: combinedToolCount,
|
|
669
|
+
});
|
|
670
|
+
return this.executeNativeGemini3Stream(mergedOptions);
|
|
671
|
+
}
|
|
632
672
|
// Initialize stream execution tracking
|
|
633
673
|
const functionTag = "GoogleVertexProvider.executeStream";
|
|
634
674
|
let chunkCount = 0;
|
|
@@ -672,6 +712,24 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
672
712
|
}),
|
|
673
713
|
abortSignal: timeoutController?.controller.signal,
|
|
674
714
|
experimental_telemetry: this.telemetryHandler.getTelemetryConfig(options),
|
|
715
|
+
// Gemini 3: use thinkingLevel via providerOptions (Vertex AI)
|
|
716
|
+
// Gemini 2.5: use thinkingBudget via providerOptions
|
|
717
|
+
...(options.thinkingConfig?.enabled && {
|
|
718
|
+
providerOptions: {
|
|
719
|
+
vertex: {
|
|
720
|
+
thinkingConfig: {
|
|
721
|
+
...(options.thinkingConfig.thinkingLevel && {
|
|
722
|
+
thinkingLevel: options.thinkingConfig.thinkingLevel,
|
|
723
|
+
}),
|
|
724
|
+
...(options.thinkingConfig.budgetTokens &&
|
|
725
|
+
!options.thinkingConfig.thinkingLevel && {
|
|
726
|
+
thinkingBudget: options.thinkingConfig.budgetTokens,
|
|
727
|
+
}),
|
|
728
|
+
includeThoughts: true,
|
|
729
|
+
},
|
|
730
|
+
},
|
|
731
|
+
},
|
|
732
|
+
}),
|
|
675
733
|
onError: (event) => {
|
|
676
734
|
const error = event.error;
|
|
677
735
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
@@ -745,6 +803,893 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
745
803
|
throw this.handleProviderError(error);
|
|
746
804
|
}
|
|
747
805
|
}
|
|
806
|
+
/**
|
|
807
|
+
* Create @google/genai client configured for Vertex AI
|
|
808
|
+
*/
|
|
809
|
+
async createVertexGenAIClient(regionOverride) {
|
|
810
|
+
const project = getVertexProjectId();
|
|
811
|
+
const location = regionOverride || this.location || getVertexLocation();
|
|
812
|
+
const mod = await import("@google/genai");
|
|
813
|
+
const ctor = mod.GoogleGenAI;
|
|
814
|
+
if (!ctor) {
|
|
815
|
+
throw new NeuroLinkError({
|
|
816
|
+
code: ERROR_CODES.INVALID_CONFIGURATION,
|
|
817
|
+
message: "@google/genai does not export GoogleGenAI",
|
|
818
|
+
category: ErrorCategory.CONFIGURATION,
|
|
819
|
+
severity: ErrorSeverity.CRITICAL,
|
|
820
|
+
retriable: false,
|
|
821
|
+
context: { module: "@google/genai", expectedExport: "GoogleGenAI" },
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
const Ctor = ctor;
|
|
825
|
+
// Use vertexai mode with project and location
|
|
826
|
+
return new Ctor({
|
|
827
|
+
vertexai: true,
|
|
828
|
+
project,
|
|
829
|
+
location,
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
/**
|
|
833
|
+
* Execute stream using native @google/genai SDK for Gemini 3 models on Vertex AI
|
|
834
|
+
* This bypasses @ai-sdk/google-vertex to properly handle thought_signature
|
|
835
|
+
*/
|
|
836
|
+
async executeNativeGemini3Stream(options) {
|
|
837
|
+
const client = await this.createVertexGenAIClient(options.region);
|
|
838
|
+
const modelName = options.model || this.modelName || getDefaultVertexModel();
|
|
839
|
+
const effectiveLocation = options.region || this.location || getVertexLocation();
|
|
840
|
+
logger.debug("[GoogleVertex] Using native @google/genai for Gemini 3", {
|
|
841
|
+
model: modelName,
|
|
842
|
+
hasTools: !!options.tools && Object.keys(options.tools).length > 0,
|
|
843
|
+
project: this.projectId,
|
|
844
|
+
location: effectiveLocation,
|
|
845
|
+
});
|
|
846
|
+
const contents = [];
|
|
847
|
+
// Build user message parts - start with text
|
|
848
|
+
const userParts = [{ text: options.input.text }];
|
|
849
|
+
// Add PDF files as inlineData parts if present
|
|
850
|
+
// Cast input to access multimodal properties that may exist at runtime
|
|
851
|
+
const multimodalInput = options.input;
|
|
852
|
+
if (multimodalInput?.pdfFiles && multimodalInput.pdfFiles.length > 0) {
|
|
853
|
+
logger.debug(`[GoogleVertex] Processing ${multimodalInput.pdfFiles.length} PDF file(s) for native stream`);
|
|
854
|
+
for (const pdfFile of multimodalInput.pdfFiles) {
|
|
855
|
+
let pdfBuffer;
|
|
856
|
+
if (typeof pdfFile === "string") {
|
|
857
|
+
// Check if it's a file path
|
|
858
|
+
if (fs.existsSync(pdfFile)) {
|
|
859
|
+
pdfBuffer = fs.readFileSync(pdfFile);
|
|
860
|
+
}
|
|
861
|
+
else {
|
|
862
|
+
// Assume it's already base64 encoded
|
|
863
|
+
pdfBuffer = Buffer.from(pdfFile, "base64");
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
else {
|
|
867
|
+
pdfBuffer = pdfFile;
|
|
868
|
+
}
|
|
869
|
+
// Convert to base64 for the native SDK
|
|
870
|
+
const base64Data = pdfBuffer.toString("base64");
|
|
871
|
+
userParts.push({
|
|
872
|
+
inlineData: {
|
|
873
|
+
mimeType: "application/pdf",
|
|
874
|
+
data: base64Data,
|
|
875
|
+
},
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
// Add images as inlineData parts if present
|
|
880
|
+
if (multimodalInput?.images && multimodalInput.images.length > 0) {
|
|
881
|
+
logger.debug(`[GoogleVertex] Processing ${multimodalInput.images.length} image(s) for native stream`);
|
|
882
|
+
for (const image of multimodalInput.images) {
|
|
883
|
+
let imageBuffer;
|
|
884
|
+
let mimeType = "image/jpeg"; // Default
|
|
885
|
+
if (typeof image === "string") {
|
|
886
|
+
if (fs.existsSync(image)) {
|
|
887
|
+
imageBuffer = fs.readFileSync(image);
|
|
888
|
+
// Detect mime type from extension
|
|
889
|
+
const ext = path.extname(image).toLowerCase();
|
|
890
|
+
if (ext === ".png") {
|
|
891
|
+
mimeType = "image/png";
|
|
892
|
+
}
|
|
893
|
+
else if (ext === ".gif") {
|
|
894
|
+
mimeType = "image/gif";
|
|
895
|
+
}
|
|
896
|
+
else if (ext === ".webp") {
|
|
897
|
+
mimeType = "image/webp";
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
else if (image.startsWith("data:")) {
|
|
901
|
+
// Handle data URL
|
|
902
|
+
const matches = image.match(/^data:([^;]+);base64,(.+)$/);
|
|
903
|
+
if (matches) {
|
|
904
|
+
mimeType = matches[1];
|
|
905
|
+
imageBuffer = Buffer.from(matches[2], "base64");
|
|
906
|
+
}
|
|
907
|
+
else {
|
|
908
|
+
continue; // Skip invalid data URL
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
else {
|
|
912
|
+
// Assume base64 string
|
|
913
|
+
imageBuffer = Buffer.from(image, "base64");
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
else {
|
|
917
|
+
imageBuffer = image;
|
|
918
|
+
}
|
|
919
|
+
const base64Data = imageBuffer.toString("base64");
|
|
920
|
+
userParts.push({
|
|
921
|
+
inlineData: {
|
|
922
|
+
mimeType,
|
|
923
|
+
data: base64Data,
|
|
924
|
+
},
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
contents.push({
|
|
929
|
+
role: "user",
|
|
930
|
+
parts: userParts,
|
|
931
|
+
});
|
|
932
|
+
let tools;
|
|
933
|
+
const executeMap = new Map();
|
|
934
|
+
if (options.tools &&
|
|
935
|
+
Object.keys(options.tools).length > 0 &&
|
|
936
|
+
!options.disableTools) {
|
|
937
|
+
const functionDeclarations = [];
|
|
938
|
+
for (const [name, tool] of Object.entries(options.tools)) {
|
|
939
|
+
const decl = {
|
|
940
|
+
name,
|
|
941
|
+
description: tool.description || `Tool: ${name}`,
|
|
942
|
+
};
|
|
943
|
+
if (tool.parameters) {
|
|
944
|
+
// Convert and inline schema to resolve $ref/definitions
|
|
945
|
+
const rawSchema = convertZodToJsonSchema(tool.parameters);
|
|
946
|
+
decl.parametersJsonSchema = inlineJsonSchema(rawSchema);
|
|
947
|
+
// Remove $schema if present - @google/genai doesn't need it
|
|
948
|
+
if (decl.parametersJsonSchema.$schema) {
|
|
949
|
+
delete decl.parametersJsonSchema.$schema;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
functionDeclarations.push(decl);
|
|
953
|
+
if (tool.execute) {
|
|
954
|
+
executeMap.set(name, tool.execute);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
tools = [{ functionDeclarations }];
|
|
958
|
+
logger.debug("[GoogleVertex] Converted tools for native SDK", {
|
|
959
|
+
toolCount: functionDeclarations.length,
|
|
960
|
+
toolNames: functionDeclarations.map((t) => t.name),
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
// Build config
|
|
964
|
+
const config = {
|
|
965
|
+
temperature: options.temperature ?? 1.0, // Gemini 3 requires 1.0 for tool calling
|
|
966
|
+
maxOutputTokens: options.maxTokens,
|
|
967
|
+
};
|
|
968
|
+
if (tools) {
|
|
969
|
+
config.tools = tools;
|
|
970
|
+
}
|
|
971
|
+
if (options.systemPrompt) {
|
|
972
|
+
config.systemInstruction = options.systemPrompt;
|
|
973
|
+
}
|
|
974
|
+
// Add thinking config for Gemini 3
|
|
975
|
+
const nativeThinkingConfig = createNativeThinkingConfig(options.thinkingConfig);
|
|
976
|
+
if (nativeThinkingConfig) {
|
|
977
|
+
config.thinkingConfig = nativeThinkingConfig;
|
|
978
|
+
}
|
|
979
|
+
// Add JSON output format support for native SDK stream
|
|
980
|
+
// Note: Combining tools + schema may have limitations with Gemini models
|
|
981
|
+
const streamOptions = options;
|
|
982
|
+
if (streamOptions.output?.format === "json" || streamOptions.schema) {
|
|
983
|
+
config.responseMimeType = "application/json";
|
|
984
|
+
// Convert schema to JSON schema format for the native SDK
|
|
985
|
+
if (streamOptions.schema) {
|
|
986
|
+
const rawSchema = convertZodToJsonSchema(streamOptions.schema);
|
|
987
|
+
const inlinedSchema = inlineJsonSchema(rawSchema);
|
|
988
|
+
// Remove $schema if present - @google/genai doesn't need it
|
|
989
|
+
if (inlinedSchema.$schema) {
|
|
990
|
+
delete inlinedSchema.$schema;
|
|
991
|
+
}
|
|
992
|
+
config.responseSchema = inlinedSchema;
|
|
993
|
+
logger.debug("[GoogleVertex] Added responseSchema for JSON output (stream)", {
|
|
994
|
+
schemaKeys: Object.keys(inlinedSchema),
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
const startTime = Date.now();
|
|
999
|
+
// Ensure maxSteps is a valid positive integer to prevent infinite loops
|
|
1000
|
+
const rawMaxSteps = options.maxSteps || DEFAULT_MAX_STEPS;
|
|
1001
|
+
const maxSteps = Number.isFinite(rawMaxSteps) && rawMaxSteps > 0
|
|
1002
|
+
? Math.min(Math.floor(rawMaxSteps), 100) // Cap at 100 for safety
|
|
1003
|
+
: Math.min(DEFAULT_MAX_STEPS, 100);
|
|
1004
|
+
const currentContents = [...contents];
|
|
1005
|
+
let finalText = "";
|
|
1006
|
+
let lastStepText = ""; // Track text from last step for maxSteps termination
|
|
1007
|
+
const allToolCalls = [];
|
|
1008
|
+
let step = 0;
|
|
1009
|
+
// Track failed tools to prevent infinite retry loops
|
|
1010
|
+
// Key: tool name, Value: { count: retry attempts, lastError: error message }
|
|
1011
|
+
const failedTools = new Map();
|
|
1012
|
+
// Track token usage across all steps
|
|
1013
|
+
// promptTokenCount is typically in the final chunk, candidatesTokenCount accumulates
|
|
1014
|
+
let totalInputTokens = 0;
|
|
1015
|
+
let totalOutputTokens = 0;
|
|
1016
|
+
// Agentic loop for tool calling
|
|
1017
|
+
while (step < maxSteps) {
|
|
1018
|
+
step++;
|
|
1019
|
+
logger.debug(`[GoogleVertex] Native SDK step ${step}/${maxSteps}`);
|
|
1020
|
+
try {
|
|
1021
|
+
const stream = await client.models.generateContentStream({
|
|
1022
|
+
model: modelName,
|
|
1023
|
+
contents: currentContents,
|
|
1024
|
+
config,
|
|
1025
|
+
});
|
|
1026
|
+
const stepFunctionCalls = [];
|
|
1027
|
+
// Capture raw response parts including thoughtSignature
|
|
1028
|
+
const rawResponseParts = [];
|
|
1029
|
+
for await (const chunk of stream) {
|
|
1030
|
+
// Extract raw parts from candidates FIRST
|
|
1031
|
+
// This avoids using chunk.text which triggers SDK warning when
|
|
1032
|
+
// non-text parts (thoughtSignature, functionCall) are present
|
|
1033
|
+
const chunkRecord = chunk;
|
|
1034
|
+
const candidates = chunkRecord.candidates;
|
|
1035
|
+
const firstCandidate = candidates?.[0];
|
|
1036
|
+
const chunkContent = firstCandidate?.content;
|
|
1037
|
+
if (chunkContent && Array.isArray(chunkContent.parts)) {
|
|
1038
|
+
rawResponseParts.push(...chunkContent.parts);
|
|
1039
|
+
}
|
|
1040
|
+
if (chunk.functionCalls) {
|
|
1041
|
+
stepFunctionCalls.push(...chunk.functionCalls);
|
|
1042
|
+
}
|
|
1043
|
+
// Extract usage metadata from chunk
|
|
1044
|
+
// promptTokenCount is typically in the final chunk, candidatesTokenCount accumulates
|
|
1045
|
+
const usageMetadata = chunkRecord.usageMetadata;
|
|
1046
|
+
if (usageMetadata) {
|
|
1047
|
+
// Take the latest promptTokenCount (usually only in final chunk)
|
|
1048
|
+
if (usageMetadata.promptTokenCount !== undefined &&
|
|
1049
|
+
usageMetadata.promptTokenCount > 0) {
|
|
1050
|
+
totalInputTokens = usageMetadata.promptTokenCount;
|
|
1051
|
+
}
|
|
1052
|
+
// Take the latest candidatesTokenCount (accumulates through chunks)
|
|
1053
|
+
if (usageMetadata.candidatesTokenCount !== undefined &&
|
|
1054
|
+
usageMetadata.candidatesTokenCount > 0) {
|
|
1055
|
+
totalOutputTokens = usageMetadata.candidatesTokenCount;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
// Extract text from raw parts after stream completes
|
|
1060
|
+
// This avoids SDK warning about non-text parts (thoughtSignature, functionCall)
|
|
1061
|
+
const stepText = rawResponseParts
|
|
1062
|
+
.filter((part) => typeof part.text === "string")
|
|
1063
|
+
.map((part) => part.text)
|
|
1064
|
+
.join("");
|
|
1065
|
+
// If no function calls, we're done
|
|
1066
|
+
if (stepFunctionCalls.length === 0) {
|
|
1067
|
+
finalText = stepText;
|
|
1068
|
+
break;
|
|
1069
|
+
}
|
|
1070
|
+
// Track the last step text for maxSteps termination
|
|
1071
|
+
lastStepText = stepText;
|
|
1072
|
+
// Execute function calls
|
|
1073
|
+
logger.debug(`[GoogleVertex] Executing ${stepFunctionCalls.length} function calls`);
|
|
1074
|
+
// Add model response with ALL parts (including thoughtSignature) to history
|
|
1075
|
+
// This preserves the thought_signature which is required for Gemini 3 multi-turn tool calling
|
|
1076
|
+
currentContents.push({
|
|
1077
|
+
role: "model",
|
|
1078
|
+
parts: rawResponseParts.length > 0
|
|
1079
|
+
? rawResponseParts
|
|
1080
|
+
: stepFunctionCalls.map((fc) => ({
|
|
1081
|
+
functionCall: fc,
|
|
1082
|
+
})),
|
|
1083
|
+
});
|
|
1084
|
+
// Execute each function and collect responses
|
|
1085
|
+
const functionResponses = [];
|
|
1086
|
+
for (const call of stepFunctionCalls) {
|
|
1087
|
+
allToolCalls.push({ toolName: call.name, args: call.args });
|
|
1088
|
+
// Check if this tool has already exceeded retry limit
|
|
1089
|
+
const failedInfo = failedTools.get(call.name);
|
|
1090
|
+
if (failedInfo && failedInfo.count >= DEFAULT_TOOL_MAX_RETRIES) {
|
|
1091
|
+
logger.warn(`[GoogleVertex] Tool "${call.name}" has exceeded retry limit (${DEFAULT_TOOL_MAX_RETRIES}), skipping execution`);
|
|
1092
|
+
functionResponses.push({
|
|
1093
|
+
functionResponse: {
|
|
1094
|
+
name: call.name,
|
|
1095
|
+
response: {
|
|
1096
|
+
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.`,
|
|
1097
|
+
status: "permanently_failed",
|
|
1098
|
+
do_not_retry: true,
|
|
1099
|
+
},
|
|
1100
|
+
},
|
|
1101
|
+
});
|
|
1102
|
+
continue;
|
|
1103
|
+
}
|
|
1104
|
+
const execute = executeMap.get(call.name);
|
|
1105
|
+
if (execute) {
|
|
1106
|
+
try {
|
|
1107
|
+
// AI SDK Tool execute requires (args, options) - provide minimal options
|
|
1108
|
+
const toolOptions = {
|
|
1109
|
+
toolCallId: `${call.name}-${Date.now()}`,
|
|
1110
|
+
messages: [],
|
|
1111
|
+
abortSignal: undefined,
|
|
1112
|
+
};
|
|
1113
|
+
const result = await execute(call.args, toolOptions);
|
|
1114
|
+
functionResponses.push({
|
|
1115
|
+
functionResponse: { name: call.name, response: { result } },
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
catch (error) {
|
|
1119
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
1120
|
+
// Track this failure
|
|
1121
|
+
const currentFailInfo = failedTools.get(call.name) || {
|
|
1122
|
+
count: 0,
|
|
1123
|
+
lastError: "",
|
|
1124
|
+
};
|
|
1125
|
+
currentFailInfo.count++;
|
|
1126
|
+
currentFailInfo.lastError = errorMessage;
|
|
1127
|
+
failedTools.set(call.name, currentFailInfo);
|
|
1128
|
+
logger.warn(`[GoogleVertex] Tool "${call.name}" failed (attempt ${currentFailInfo.count}/${DEFAULT_TOOL_MAX_RETRIES}): ${errorMessage}`);
|
|
1129
|
+
// Determine if this is a permanent failure
|
|
1130
|
+
const isPermanentFailure = currentFailInfo.count >= DEFAULT_TOOL_MAX_RETRIES;
|
|
1131
|
+
functionResponses.push({
|
|
1132
|
+
functionResponse: {
|
|
1133
|
+
name: call.name,
|
|
1134
|
+
response: {
|
|
1135
|
+
error: isPermanentFailure
|
|
1136
|
+
? `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.`
|
|
1137
|
+
: `TOOL_EXECUTION_ERROR: ${errorMessage}. Retry attempt ${currentFailInfo.count}/${DEFAULT_TOOL_MAX_RETRIES}.`,
|
|
1138
|
+
status: isPermanentFailure
|
|
1139
|
+
? "permanently_failed"
|
|
1140
|
+
: "failed",
|
|
1141
|
+
do_not_retry: isPermanentFailure,
|
|
1142
|
+
retry_count: currentFailInfo.count,
|
|
1143
|
+
max_retries: DEFAULT_TOOL_MAX_RETRIES,
|
|
1144
|
+
},
|
|
1145
|
+
},
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
else {
|
|
1150
|
+
// Tool not found is a permanent error
|
|
1151
|
+
functionResponses.push({
|
|
1152
|
+
functionResponse: {
|
|
1153
|
+
name: call.name,
|
|
1154
|
+
response: {
|
|
1155
|
+
error: `TOOL_NOT_FOUND: The tool "${call.name}" does not exist. Do not attempt to call this tool again.`,
|
|
1156
|
+
status: "permanently_failed",
|
|
1157
|
+
do_not_retry: true,
|
|
1158
|
+
},
|
|
1159
|
+
},
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
// Add function responses to history
|
|
1164
|
+
currentContents.push({
|
|
1165
|
+
role: "function",
|
|
1166
|
+
parts: functionResponses,
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
catch (error) {
|
|
1170
|
+
logger.error("[GoogleVertex] Native SDK error", error);
|
|
1171
|
+
throw this.handleProviderError(error);
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
// Handle maxSteps termination - if we exited the loop due to maxSteps being reached
|
|
1175
|
+
if (step >= maxSteps && !finalText) {
|
|
1176
|
+
logger.warn(`[GoogleVertex] Tool call loop terminated after reaching maxSteps (${maxSteps}). ` +
|
|
1177
|
+
`Model was still calling tools. Using accumulated text from last step.`);
|
|
1178
|
+
finalText =
|
|
1179
|
+
lastStepText ||
|
|
1180
|
+
`[Tool execution limit reached after ${maxSteps} steps. The model continued requesting tool calls beyond the limit.]`;
|
|
1181
|
+
}
|
|
1182
|
+
const responseTime = Date.now() - startTime;
|
|
1183
|
+
// Create async iterable for streaming result
|
|
1184
|
+
async function* createTextStream() {
|
|
1185
|
+
yield { content: finalText };
|
|
1186
|
+
}
|
|
1187
|
+
return {
|
|
1188
|
+
stream: createTextStream(),
|
|
1189
|
+
provider: this.providerName,
|
|
1190
|
+
model: modelName,
|
|
1191
|
+
usage: {
|
|
1192
|
+
input: totalInputTokens,
|
|
1193
|
+
output: totalOutputTokens,
|
|
1194
|
+
total: totalInputTokens + totalOutputTokens,
|
|
1195
|
+
},
|
|
1196
|
+
toolCalls: allToolCalls.map((tc) => ({
|
|
1197
|
+
toolName: tc.toolName,
|
|
1198
|
+
args: tc.args,
|
|
1199
|
+
})),
|
|
1200
|
+
metadata: {
|
|
1201
|
+
streamId: `native-vertex-${Date.now()}`,
|
|
1202
|
+
startTime,
|
|
1203
|
+
responseTime,
|
|
1204
|
+
totalToolExecutions: allToolCalls.length,
|
|
1205
|
+
},
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
1208
|
+
/**
|
|
1209
|
+
* Execute generate using native @google/genai SDK for Gemini 3 models on Vertex AI
|
|
1210
|
+
* This bypasses @ai-sdk/google-vertex to properly handle thought_signature
|
|
1211
|
+
*/
|
|
1212
|
+
async executeNativeGemini3Generate(options) {
|
|
1213
|
+
const client = await this.createVertexGenAIClient(options.region);
|
|
1214
|
+
const modelName = options.model || this.modelName || getDefaultVertexModel();
|
|
1215
|
+
const effectiveLocation = options.region || this.location || getVertexLocation();
|
|
1216
|
+
logger.debug("[GoogleVertex] Using native @google/genai for Gemini 3 generate", {
|
|
1217
|
+
model: modelName,
|
|
1218
|
+
project: this.projectId,
|
|
1219
|
+
location: effectiveLocation,
|
|
1220
|
+
});
|
|
1221
|
+
// Build contents from input with multimodal support
|
|
1222
|
+
const inputText = options.prompt || options.input?.text || "Please respond.";
|
|
1223
|
+
const contents = [];
|
|
1224
|
+
// Build user message parts - start with text
|
|
1225
|
+
const userParts = [{ text: inputText }];
|
|
1226
|
+
// Add PDF files as inlineData parts if present
|
|
1227
|
+
// Cast input to access multimodal properties that may exist at runtime
|
|
1228
|
+
const multimodalInput = options.input;
|
|
1229
|
+
if (multimodalInput?.pdfFiles && multimodalInput.pdfFiles.length > 0) {
|
|
1230
|
+
logger.debug(`[GoogleVertex] Processing ${multimodalInput.pdfFiles.length} PDF file(s) for native generate`);
|
|
1231
|
+
for (const pdfFile of multimodalInput.pdfFiles) {
|
|
1232
|
+
let pdfBuffer;
|
|
1233
|
+
if (typeof pdfFile === "string") {
|
|
1234
|
+
// Check if it's a file path
|
|
1235
|
+
if (fs.existsSync(pdfFile)) {
|
|
1236
|
+
pdfBuffer = fs.readFileSync(pdfFile);
|
|
1237
|
+
}
|
|
1238
|
+
else {
|
|
1239
|
+
// Assume it's already base64 encoded
|
|
1240
|
+
pdfBuffer = Buffer.from(pdfFile, "base64");
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
else {
|
|
1244
|
+
pdfBuffer = pdfFile;
|
|
1245
|
+
}
|
|
1246
|
+
// Convert to base64 for the native SDK
|
|
1247
|
+
const base64Data = pdfBuffer.toString("base64");
|
|
1248
|
+
userParts.push({
|
|
1249
|
+
inlineData: {
|
|
1250
|
+
mimeType: "application/pdf",
|
|
1251
|
+
data: base64Data,
|
|
1252
|
+
},
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
// Add images as inlineData parts if present
|
|
1257
|
+
if (multimodalInput?.images && multimodalInput.images.length > 0) {
|
|
1258
|
+
logger.debug(`[GoogleVertex] Processing ${multimodalInput.images.length} image(s) for native generate`);
|
|
1259
|
+
for (const image of multimodalInput.images) {
|
|
1260
|
+
let imageBuffer;
|
|
1261
|
+
let mimeType = "image/jpeg"; // Default
|
|
1262
|
+
if (typeof image === "string") {
|
|
1263
|
+
if (fs.existsSync(image)) {
|
|
1264
|
+
imageBuffer = fs.readFileSync(image);
|
|
1265
|
+
// Detect mime type from extension
|
|
1266
|
+
const ext = path.extname(image).toLowerCase();
|
|
1267
|
+
if (ext === ".png") {
|
|
1268
|
+
mimeType = "image/png";
|
|
1269
|
+
}
|
|
1270
|
+
else if (ext === ".gif") {
|
|
1271
|
+
mimeType = "image/gif";
|
|
1272
|
+
}
|
|
1273
|
+
else if (ext === ".webp") {
|
|
1274
|
+
mimeType = "image/webp";
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
else if (image.startsWith("data:")) {
|
|
1278
|
+
// Handle data URL
|
|
1279
|
+
const matches = image.match(/^data:([^;]+);base64,(.+)$/);
|
|
1280
|
+
if (matches) {
|
|
1281
|
+
mimeType = matches[1];
|
|
1282
|
+
imageBuffer = Buffer.from(matches[2], "base64");
|
|
1283
|
+
}
|
|
1284
|
+
else {
|
|
1285
|
+
continue; // Skip invalid data URL
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
else {
|
|
1289
|
+
// Assume base64 string
|
|
1290
|
+
imageBuffer = Buffer.from(image, "base64");
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
else {
|
|
1294
|
+
imageBuffer = image;
|
|
1295
|
+
}
|
|
1296
|
+
const base64Data = imageBuffer.toString("base64");
|
|
1297
|
+
userParts.push({
|
|
1298
|
+
inlineData: {
|
|
1299
|
+
mimeType,
|
|
1300
|
+
data: base64Data,
|
|
1301
|
+
},
|
|
1302
|
+
});
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
contents.push({
|
|
1306
|
+
role: "user",
|
|
1307
|
+
parts: userParts,
|
|
1308
|
+
});
|
|
1309
|
+
// Get tools from SDK and options
|
|
1310
|
+
const shouldUseTools = !options.disableTools && this.supportsTools();
|
|
1311
|
+
const sdkTools = shouldUseTools ? await this.getAllTools() : {};
|
|
1312
|
+
const combinedTools = { ...sdkTools, ...(options.tools || {}) };
|
|
1313
|
+
let tools;
|
|
1314
|
+
const executeMap = new Map();
|
|
1315
|
+
if (Object.keys(combinedTools).length > 0) {
|
|
1316
|
+
const functionDeclarations = [];
|
|
1317
|
+
for (const [name, tool] of Object.entries(combinedTools)) {
|
|
1318
|
+
const decl = {
|
|
1319
|
+
name,
|
|
1320
|
+
description: tool.description || `Tool: ${name}`,
|
|
1321
|
+
};
|
|
1322
|
+
if (tool.parameters) {
|
|
1323
|
+
// Convert and inline schema to resolve $ref/definitions
|
|
1324
|
+
const rawSchema = convertZodToJsonSchema(tool.parameters);
|
|
1325
|
+
decl.parametersJsonSchema = inlineJsonSchema(rawSchema);
|
|
1326
|
+
// Remove $schema if present - @google/genai doesn't need it
|
|
1327
|
+
if (decl.parametersJsonSchema.$schema) {
|
|
1328
|
+
delete decl.parametersJsonSchema.$schema;
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
functionDeclarations.push(decl);
|
|
1332
|
+
if (tool.execute) {
|
|
1333
|
+
executeMap.set(name, tool.execute);
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
tools = [{ functionDeclarations }];
|
|
1337
|
+
logger.debug("[GoogleVertex] Converted tools for native SDK generate", {
|
|
1338
|
+
toolCount: functionDeclarations.length,
|
|
1339
|
+
toolNames: functionDeclarations.map((t) => t.name),
|
|
1340
|
+
});
|
|
1341
|
+
}
|
|
1342
|
+
// Build config
|
|
1343
|
+
const config = {
|
|
1344
|
+
temperature: options.temperature ?? 1.0, // Gemini 3 requires 1.0 for tool calling
|
|
1345
|
+
maxOutputTokens: options.maxTokens,
|
|
1346
|
+
};
|
|
1347
|
+
if (tools) {
|
|
1348
|
+
config.tools = tools;
|
|
1349
|
+
}
|
|
1350
|
+
if (options.systemPrompt) {
|
|
1351
|
+
config.systemInstruction = options.systemPrompt;
|
|
1352
|
+
}
|
|
1353
|
+
// Add thinking config for Gemini 3
|
|
1354
|
+
const nativeThinkingConfig2 = createNativeThinkingConfig(options.thinkingConfig);
|
|
1355
|
+
if (nativeThinkingConfig2) {
|
|
1356
|
+
config.thinkingConfig = nativeThinkingConfig2;
|
|
1357
|
+
}
|
|
1358
|
+
// Note: Schema/JSON output for Gemini 3 native SDK is complex due to $ref resolution issues
|
|
1359
|
+
// For now, schemas are handled via the AI SDK fallback path, not native SDK
|
|
1360
|
+
// TODO: Implement proper $ref resolution for complex nested schemas
|
|
1361
|
+
const startTime = Date.now();
|
|
1362
|
+
// Ensure maxSteps is a valid positive integer to prevent infinite loops
|
|
1363
|
+
const rawMaxSteps = options.maxSteps || DEFAULT_MAX_STEPS;
|
|
1364
|
+
const maxSteps = Number.isFinite(rawMaxSteps) && rawMaxSteps > 0
|
|
1365
|
+
? Math.min(Math.floor(rawMaxSteps), 100) // Cap at 100 for safety
|
|
1366
|
+
: Math.min(DEFAULT_MAX_STEPS, 100);
|
|
1367
|
+
const currentContents = [...contents];
|
|
1368
|
+
let finalText = "";
|
|
1369
|
+
let lastStepText = ""; // Track text from last step for maxSteps termination
|
|
1370
|
+
const allToolCalls = [];
|
|
1371
|
+
const toolExecutions = [];
|
|
1372
|
+
let step = 0;
|
|
1373
|
+
// Track failed tools to prevent infinite retry loops
|
|
1374
|
+
// Key: tool name, Value: { count: retry attempts, lastError: error message }
|
|
1375
|
+
const failedTools = new Map();
|
|
1376
|
+
// Track token usage across all steps
|
|
1377
|
+
// promptTokenCount is typically in the final chunk, candidatesTokenCount accumulates
|
|
1378
|
+
let totalInputTokens = 0;
|
|
1379
|
+
let totalOutputTokens = 0;
|
|
1380
|
+
// Agentic loop for tool calling
|
|
1381
|
+
while (step < maxSteps) {
|
|
1382
|
+
step++;
|
|
1383
|
+
logger.debug(`[GoogleVertex] Native SDK generate step ${step}/${maxSteps}`);
|
|
1384
|
+
try {
|
|
1385
|
+
// Use generateContentStream and collect all chunks (same as GoogleAIStudio)
|
|
1386
|
+
const stream = await client.models.generateContentStream({
|
|
1387
|
+
model: modelName,
|
|
1388
|
+
contents: currentContents,
|
|
1389
|
+
config,
|
|
1390
|
+
});
|
|
1391
|
+
const stepFunctionCalls = [];
|
|
1392
|
+
// Capture raw response parts including thoughtSignature
|
|
1393
|
+
const rawResponseParts = [];
|
|
1394
|
+
// Collect all chunks from stream
|
|
1395
|
+
for await (const chunk of stream) {
|
|
1396
|
+
// Extract raw parts from candidates FIRST
|
|
1397
|
+
// This avoids using chunk.text which triggers SDK warning when
|
|
1398
|
+
// non-text parts (thoughtSignature, functionCall) are present
|
|
1399
|
+
const chunkRecord = chunk;
|
|
1400
|
+
const candidates = chunkRecord.candidates;
|
|
1401
|
+
const firstCandidate = candidates?.[0];
|
|
1402
|
+
const chunkContent = firstCandidate?.content;
|
|
1403
|
+
if (chunkContent && Array.isArray(chunkContent.parts)) {
|
|
1404
|
+
rawResponseParts.push(...chunkContent.parts);
|
|
1405
|
+
}
|
|
1406
|
+
if (chunk.functionCalls) {
|
|
1407
|
+
stepFunctionCalls.push(...chunk.functionCalls);
|
|
1408
|
+
}
|
|
1409
|
+
// Extract usage metadata from chunk
|
|
1410
|
+
// promptTokenCount is typically in the final chunk, candidatesTokenCount accumulates
|
|
1411
|
+
const usageMetadata = chunkRecord.usageMetadata;
|
|
1412
|
+
if (usageMetadata) {
|
|
1413
|
+
// Take the latest promptTokenCount (usually only in final chunk)
|
|
1414
|
+
if (usageMetadata.promptTokenCount !== undefined &&
|
|
1415
|
+
usageMetadata.promptTokenCount > 0) {
|
|
1416
|
+
totalInputTokens = usageMetadata.promptTokenCount;
|
|
1417
|
+
}
|
|
1418
|
+
// Take the latest candidatesTokenCount (accumulates through chunks)
|
|
1419
|
+
if (usageMetadata.candidatesTokenCount !== undefined &&
|
|
1420
|
+
usageMetadata.candidatesTokenCount > 0) {
|
|
1421
|
+
totalOutputTokens = usageMetadata.candidatesTokenCount;
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
// Extract text from raw parts after stream completes
|
|
1426
|
+
// This avoids SDK warning about non-text parts (thoughtSignature, functionCall)
|
|
1427
|
+
const stepText = rawResponseParts
|
|
1428
|
+
.filter((part) => typeof part.text === "string")
|
|
1429
|
+
.map((part) => part.text)
|
|
1430
|
+
.join("");
|
|
1431
|
+
// If no function calls, we're done
|
|
1432
|
+
if (stepFunctionCalls.length === 0) {
|
|
1433
|
+
finalText = stepText;
|
|
1434
|
+
break;
|
|
1435
|
+
}
|
|
1436
|
+
// Track the last step text for maxSteps termination
|
|
1437
|
+
lastStepText = stepText;
|
|
1438
|
+
// Execute function calls
|
|
1439
|
+
logger.debug(`[GoogleVertex] Generate executing ${stepFunctionCalls.length} function calls`);
|
|
1440
|
+
// Add model response with ALL parts (including thoughtSignature) to history
|
|
1441
|
+
// This preserves the thought_signature which is required for Gemini 3 multi-turn tool calling
|
|
1442
|
+
currentContents.push({
|
|
1443
|
+
role: "model",
|
|
1444
|
+
parts: rawResponseParts.length > 0
|
|
1445
|
+
? rawResponseParts
|
|
1446
|
+
: stepFunctionCalls.map((fc) => ({
|
|
1447
|
+
functionCall: fc,
|
|
1448
|
+
})),
|
|
1449
|
+
});
|
|
1450
|
+
// Execute each function and collect responses
|
|
1451
|
+
const functionResponses = [];
|
|
1452
|
+
for (const call of stepFunctionCalls) {
|
|
1453
|
+
allToolCalls.push({ toolName: call.name, args: call.args });
|
|
1454
|
+
// Check if this tool has already exceeded retry limit
|
|
1455
|
+
const failedInfo = failedTools.get(call.name);
|
|
1456
|
+
if (failedInfo && failedInfo.count >= DEFAULT_TOOL_MAX_RETRIES) {
|
|
1457
|
+
logger.warn(`[GoogleVertex] Tool "${call.name}" has exceeded retry limit (${DEFAULT_TOOL_MAX_RETRIES}), skipping execution`);
|
|
1458
|
+
const errorOutput = {
|
|
1459
|
+
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.`,
|
|
1460
|
+
status: "permanently_failed",
|
|
1461
|
+
do_not_retry: true,
|
|
1462
|
+
};
|
|
1463
|
+
toolExecutions.push({
|
|
1464
|
+
name: call.name,
|
|
1465
|
+
input: call.args,
|
|
1466
|
+
output: errorOutput,
|
|
1467
|
+
});
|
|
1468
|
+
functionResponses.push({
|
|
1469
|
+
functionResponse: {
|
|
1470
|
+
name: call.name,
|
|
1471
|
+
response: errorOutput,
|
|
1472
|
+
},
|
|
1473
|
+
});
|
|
1474
|
+
continue;
|
|
1475
|
+
}
|
|
1476
|
+
const execute = executeMap.get(call.name);
|
|
1477
|
+
if (execute) {
|
|
1478
|
+
try {
|
|
1479
|
+
// AI SDK Tool execute requires (args, options) - provide minimal options
|
|
1480
|
+
const toolOptions = {
|
|
1481
|
+
toolCallId: `${call.name}-${Date.now()}`,
|
|
1482
|
+
messages: [],
|
|
1483
|
+
abortSignal: undefined,
|
|
1484
|
+
};
|
|
1485
|
+
const execResult = await execute(call.args, toolOptions);
|
|
1486
|
+
// Track execution
|
|
1487
|
+
toolExecutions.push({
|
|
1488
|
+
name: call.name,
|
|
1489
|
+
input: call.args,
|
|
1490
|
+
output: execResult,
|
|
1491
|
+
});
|
|
1492
|
+
functionResponses.push({
|
|
1493
|
+
functionResponse: {
|
|
1494
|
+
name: call.name,
|
|
1495
|
+
response: { result: execResult },
|
|
1496
|
+
},
|
|
1497
|
+
});
|
|
1498
|
+
}
|
|
1499
|
+
catch (error) {
|
|
1500
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
1501
|
+
// Track this failure
|
|
1502
|
+
const currentFailInfo = failedTools.get(call.name) || {
|
|
1503
|
+
count: 0,
|
|
1504
|
+
lastError: "",
|
|
1505
|
+
};
|
|
1506
|
+
currentFailInfo.count++;
|
|
1507
|
+
currentFailInfo.lastError = errorMessage;
|
|
1508
|
+
failedTools.set(call.name, currentFailInfo);
|
|
1509
|
+
logger.warn(`[GoogleVertex] Tool "${call.name}" failed (attempt ${currentFailInfo.count}/${DEFAULT_TOOL_MAX_RETRIES}): ${errorMessage}`);
|
|
1510
|
+
// Determine if this is a permanent failure
|
|
1511
|
+
const isPermanentFailure = currentFailInfo.count >= DEFAULT_TOOL_MAX_RETRIES;
|
|
1512
|
+
const errorOutput = {
|
|
1513
|
+
error: isPermanentFailure
|
|
1514
|
+
? `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.`
|
|
1515
|
+
: `TOOL_EXECUTION_ERROR: ${errorMessage}. Retry attempt ${currentFailInfo.count}/${DEFAULT_TOOL_MAX_RETRIES}.`,
|
|
1516
|
+
status: isPermanentFailure ? "permanently_failed" : "failed",
|
|
1517
|
+
do_not_retry: isPermanentFailure,
|
|
1518
|
+
retry_count: currentFailInfo.count,
|
|
1519
|
+
max_retries: DEFAULT_TOOL_MAX_RETRIES,
|
|
1520
|
+
};
|
|
1521
|
+
toolExecutions.push({
|
|
1522
|
+
name: call.name,
|
|
1523
|
+
input: call.args,
|
|
1524
|
+
output: errorOutput,
|
|
1525
|
+
});
|
|
1526
|
+
functionResponses.push({
|
|
1527
|
+
functionResponse: {
|
|
1528
|
+
name: call.name,
|
|
1529
|
+
response: errorOutput,
|
|
1530
|
+
},
|
|
1531
|
+
});
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
else {
|
|
1535
|
+
// Tool not found is a permanent error
|
|
1536
|
+
const errorOutput = {
|
|
1537
|
+
error: `TOOL_NOT_FOUND: The tool "${call.name}" does not exist. Do not attempt to call this tool again.`,
|
|
1538
|
+
status: "permanently_failed",
|
|
1539
|
+
do_not_retry: true,
|
|
1540
|
+
};
|
|
1541
|
+
toolExecutions.push({
|
|
1542
|
+
name: call.name,
|
|
1543
|
+
input: call.args,
|
|
1544
|
+
output: errorOutput,
|
|
1545
|
+
});
|
|
1546
|
+
functionResponses.push({
|
|
1547
|
+
functionResponse: {
|
|
1548
|
+
name: call.name,
|
|
1549
|
+
response: errorOutput,
|
|
1550
|
+
},
|
|
1551
|
+
});
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
// Add function responses to history
|
|
1555
|
+
currentContents.push({
|
|
1556
|
+
role: "function",
|
|
1557
|
+
parts: functionResponses,
|
|
1558
|
+
});
|
|
1559
|
+
}
|
|
1560
|
+
catch (error) {
|
|
1561
|
+
logger.error("[GoogleVertex] Native SDK generate error", error);
|
|
1562
|
+
throw this.handleProviderError(error);
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
// Handle maxSteps termination - if we exited the loop due to maxSteps being reached
|
|
1566
|
+
if (step >= maxSteps && !finalText) {
|
|
1567
|
+
logger.warn(`[GoogleVertex] Generate tool call loop terminated after reaching maxSteps (${maxSteps}). ` +
|
|
1568
|
+
`Model was still calling tools. Using accumulated text from last step.`);
|
|
1569
|
+
finalText =
|
|
1570
|
+
lastStepText ||
|
|
1571
|
+
`[Tool execution limit reached after ${maxSteps} steps. The model continued requesting tool calls beyond the limit.]`;
|
|
1572
|
+
}
|
|
1573
|
+
const responseTime = Date.now() - startTime;
|
|
1574
|
+
// Build EnhancedGenerateResult
|
|
1575
|
+
return {
|
|
1576
|
+
content: finalText,
|
|
1577
|
+
provider: this.providerName,
|
|
1578
|
+
model: modelName,
|
|
1579
|
+
usage: {
|
|
1580
|
+
input: totalInputTokens,
|
|
1581
|
+
output: totalOutputTokens,
|
|
1582
|
+
total: totalInputTokens + totalOutputTokens,
|
|
1583
|
+
},
|
|
1584
|
+
responseTime,
|
|
1585
|
+
toolsUsed: allToolCalls.map((tc) => tc.toolName),
|
|
1586
|
+
toolExecutions: toolExecutions,
|
|
1587
|
+
enhancedWithTools: allToolCalls.length > 0,
|
|
1588
|
+
};
|
|
1589
|
+
}
|
|
1590
|
+
/**
|
|
1591
|
+
* Process CSV files and append content to options.input.text
|
|
1592
|
+
* This ensures CSV data is available in the prompt for native Gemini 3 SDK calls
|
|
1593
|
+
* Returns a new options object with modified input (immutable pattern)
|
|
1594
|
+
*/
|
|
1595
|
+
async processCSVFilesForNativeSDK(options) {
|
|
1596
|
+
const input = options.input;
|
|
1597
|
+
if (!input?.csvFiles || input.csvFiles.length === 0) {
|
|
1598
|
+
return options;
|
|
1599
|
+
}
|
|
1600
|
+
logger.info(`[GoogleVertex] Processing ${input.csvFiles.length} CSV file(s) for native Gemini 3 SDK`);
|
|
1601
|
+
let modifiedText = input.text || "";
|
|
1602
|
+
for (let i = 0; i < input.csvFiles.length; i++) {
|
|
1603
|
+
const csvFile = input.csvFiles[i];
|
|
1604
|
+
try {
|
|
1605
|
+
const result = await FileDetector.detectAndProcess(csvFile, {
|
|
1606
|
+
allowedTypes: ["csv"],
|
|
1607
|
+
csvOptions: "csvOptions" in options
|
|
1608
|
+
? options.csvOptions
|
|
1609
|
+
: undefined,
|
|
1610
|
+
});
|
|
1611
|
+
// Extract filename for display
|
|
1612
|
+
const filename = typeof csvFile === "string"
|
|
1613
|
+
? path.basename(csvFile)
|
|
1614
|
+
: `csv_file_${i + 1}.csv`;
|
|
1615
|
+
let csvSection = `\n\n## CSV Data from "${filename}":\n`;
|
|
1616
|
+
// Add metadata if available
|
|
1617
|
+
if (result.metadata) {
|
|
1618
|
+
const meta = result.metadata;
|
|
1619
|
+
if (meta.rowCount || meta.columnCount || meta.columnNames) {
|
|
1620
|
+
csvSection += `**File Info:**\n`;
|
|
1621
|
+
if (meta.rowCount) {
|
|
1622
|
+
csvSection += `- Rows: ${meta.rowCount}\n`;
|
|
1623
|
+
}
|
|
1624
|
+
if (meta.columnCount) {
|
|
1625
|
+
csvSection += `- Columns: ${meta.columnCount}\n`;
|
|
1626
|
+
}
|
|
1627
|
+
if (meta.columnNames && Array.isArray(meta.columnNames)) {
|
|
1628
|
+
csvSection += `- Column Names: ${meta.columnNames.join(", ")}\n`;
|
|
1629
|
+
}
|
|
1630
|
+
csvSection += "\n";
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
// Add strong instructions to use the CSV data directly
|
|
1634
|
+
csvSection += `\n**CRITICAL INSTRUCTION**: The complete CSV data is included below. You MUST use this data directly from this prompt.\n`;
|
|
1635
|
+
csvSection += `DO NOT use any external tools (github, search_code, get_file_contents, etc.) to access this data.\n`;
|
|
1636
|
+
csvSection += `The data you need is right here in this message - read it carefully and answer based on it.\n\n`;
|
|
1637
|
+
csvSection += result.content;
|
|
1638
|
+
// Prepend CSV to ensure data appears before user's question
|
|
1639
|
+
modifiedText =
|
|
1640
|
+
csvSection + "\n\n---\n\n**USER QUESTION:**\n" + modifiedText;
|
|
1641
|
+
logger.info(`[GoogleVertex] ✅ Processed CSV: ${filename}`);
|
|
1642
|
+
}
|
|
1643
|
+
catch (error) {
|
|
1644
|
+
logger.error(`[GoogleVertex] ❌ Failed to process CSV file ${i + 1}:`, error);
|
|
1645
|
+
const filename = typeof csvFile === "string"
|
|
1646
|
+
? path.basename(csvFile)
|
|
1647
|
+
: `csv_file_${i + 1}.csv`;
|
|
1648
|
+
modifiedText += `\n\n## CSV Data Error: Failed to process "${filename}"\nReason: ${error instanceof Error ? error.message : "Unknown error"}`;
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
// Return new options with modified input (immutable pattern)
|
|
1652
|
+
// Preserve the full type of options.input by spreading options.input directly
|
|
1653
|
+
return {
|
|
1654
|
+
...options,
|
|
1655
|
+
input: { ...options.input, text: modifiedText },
|
|
1656
|
+
};
|
|
1657
|
+
}
|
|
1658
|
+
/**
|
|
1659
|
+
* Override generate to route Gemini 3 models with tools to native SDK
|
|
1660
|
+
*/
|
|
1661
|
+
async generate(optionsOrPrompt) {
|
|
1662
|
+
// Normalize options
|
|
1663
|
+
const options = typeof optionsOrPrompt === "string"
|
|
1664
|
+
? { prompt: optionsOrPrompt }
|
|
1665
|
+
: optionsOrPrompt;
|
|
1666
|
+
const modelName = options.model || this.modelName || getDefaultVertexModel();
|
|
1667
|
+
// Check if we should use native SDK for Gemini 3 with tools
|
|
1668
|
+
const shouldUseTools = !options.disableTools && this.supportsTools();
|
|
1669
|
+
const sdkTools = shouldUseTools ? await this.getAllTools() : {};
|
|
1670
|
+
const hasTools = shouldUseTools &&
|
|
1671
|
+
(Object.keys(sdkTools).length > 0 ||
|
|
1672
|
+
(options.tools && Object.keys(options.tools).length > 0));
|
|
1673
|
+
if (isGemini3Model(modelName) && hasTools) {
|
|
1674
|
+
// Process CSV files before routing to native SDK (bypasses normal message builder)
|
|
1675
|
+
const processedOptions = await this.processCSVFilesForNativeSDK(options);
|
|
1676
|
+
// Merge SDK tools into options for native SDK path
|
|
1677
|
+
const mergedOptions = {
|
|
1678
|
+
...processedOptions,
|
|
1679
|
+
tools: { ...sdkTools, ...(processedOptions.tools || {}) },
|
|
1680
|
+
};
|
|
1681
|
+
logger.info("[GoogleVertex] Routing Gemini 3 generate to native SDK for tool calling", {
|
|
1682
|
+
model: modelName,
|
|
1683
|
+
sdkToolCount: Object.keys(sdkTools).length,
|
|
1684
|
+
optionToolCount: Object.keys(processedOptions.tools || {}).length,
|
|
1685
|
+
totalToolCount: Object.keys(sdkTools).length +
|
|
1686
|
+
Object.keys(processedOptions.tools || {}).length,
|
|
1687
|
+
});
|
|
1688
|
+
return this.executeNativeGemini3Generate(mergedOptions);
|
|
1689
|
+
}
|
|
1690
|
+
// Fall back to BaseProvider implementation
|
|
1691
|
+
return super.generate(optionsOrPrompt);
|
|
1692
|
+
}
|
|
748
1693
|
handleProviderError(error) {
|
|
749
1694
|
const errorRecord = error;
|
|
750
1695
|
if (typeof errorRecord?.name === "string" &&
|
|
@@ -1150,15 +2095,45 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
1150
2095
|
async checkVertexRegionalSupport(region = "us-central1") {
|
|
1151
2096
|
// Based on Google Cloud documentation, these regions support Anthropic models
|
|
1152
2097
|
const supportedRegions = [
|
|
2098
|
+
// North America
|
|
1153
2099
|
"us-central1",
|
|
2100
|
+
"us-east1",
|
|
1154
2101
|
"us-east4",
|
|
1155
2102
|
"us-east5",
|
|
2103
|
+
"us-south1",
|
|
1156
2104
|
"us-west1",
|
|
1157
2105
|
"us-west4",
|
|
2106
|
+
"northamerica-northeast1",
|
|
2107
|
+
"northamerica-northeast2",
|
|
2108
|
+
// Europe
|
|
1158
2109
|
"europe-west1",
|
|
2110
|
+
"europe-west2",
|
|
2111
|
+
"europe-west3",
|
|
1159
2112
|
"europe-west4",
|
|
1160
|
-
"
|
|
2113
|
+
"europe-west6",
|
|
2114
|
+
"europe-west8",
|
|
2115
|
+
"europe-west9",
|
|
2116
|
+
"europe-north1",
|
|
2117
|
+
"europe-central2",
|
|
2118
|
+
"europe-southwest1",
|
|
2119
|
+
// Asia Pacific
|
|
2120
|
+
"asia-east1",
|
|
2121
|
+
"asia-east2",
|
|
1161
2122
|
"asia-northeast1",
|
|
2123
|
+
"asia-northeast2",
|
|
2124
|
+
"asia-northeast3",
|
|
2125
|
+
"asia-south1",
|
|
2126
|
+
"asia-southeast1",
|
|
2127
|
+
"asia-southeast2",
|
|
2128
|
+
"australia-southeast1",
|
|
2129
|
+
"australia-southeast2",
|
|
2130
|
+
// Middle East & Africa
|
|
2131
|
+
"me-west1",
|
|
2132
|
+
"me-central1",
|
|
2133
|
+
"africa-south1",
|
|
2134
|
+
// South America
|
|
2135
|
+
"southamerica-east1",
|
|
2136
|
+
"southamerica-west1",
|
|
1162
2137
|
];
|
|
1163
2138
|
return supportedRegions.includes(region);
|
|
1164
2139
|
}
|