@juspay/neurolink
Version:
Universal AI Development Platform with working MCP integration, multi-provider support, and professional CLI. Built-in tools operational, 58+ external MCP servers discoverable. Connect to filesystem, GitHub, database operations, and more. Build, test, and
592 lines (591 loc) • 26.7 kB
JavaScript
import { createVertex, } from "@ai-sdk/google-vertex";
import { streamText, Output, } from "ai";
import { BaseProvider } from "../core/baseProvider.js";
import { logger } from "../utils/logger.js";
import { createTimeoutController, TimeoutError, } from "../utils/timeout.js";
import { DEFAULT_MAX_TOKENS, DEFAULT_MAX_STEPS } from "../core/constants.js";
import { ModelConfigurationManager } from "../core/modelConfiguration.js";
import { validateApiKey, createVertexProjectConfig, createGoogleAuthConfig, } from "../utils/providerConfig.js";
import { buildMessagesArray } from "../utils/messageBuilder.js";
import { createProxyFetch } from "../proxy/proxyFetch.js";
// Cache for anthropic module to avoid repeated imports
let _createVertexAnthropic = null;
let _anthropicImportAttempted = false;
// Function to dynamically import anthropic support
async function getCreateVertexAnthropic() {
if (_anthropicImportAttempted) {
return _createVertexAnthropic;
}
_anthropicImportAttempted = true;
try {
// Try to import the anthropic module - available in @ai-sdk/google-vertex ^2.2.0+
// Use proper dynamic import without eval() for security
const anthropicModule = (await import("@ai-sdk/google-vertex/anthropic"));
_createVertexAnthropic = anthropicModule.createVertexAnthropic;
logger.debug("[GoogleVertexAI] Anthropic module successfully loaded");
return _createVertexAnthropic;
}
catch (error) {
// Anthropic module not available
logger.warn("[GoogleVertexAI] Anthropic module not available. Install @ai-sdk/google-vertex ^2.2.0 for Anthropic model support.");
return null;
}
}
// Configuration helpers - now using consolidated utility
const getVertexProjectId = () => {
return validateApiKey(createVertexProjectConfig());
};
const getVertexLocation = () => {
return (process.env.GOOGLE_CLOUD_LOCATION ||
process.env.VERTEX_LOCATION ||
process.env.GOOGLE_VERTEX_LOCATION ||
"us-central1");
};
const getDefaultVertexModel = () => {
// Use gemini-2.5-flash as default - latest and best price-performance model
// Override with VERTEX_MODEL environment variable if needed
return process.env.VERTEX_MODEL || "gemini-2.5-flash";
};
const hasGoogleCredentials = () => {
return !!(process.env.GOOGLE_APPLICATION_CREDENTIALS ||
process.env.GOOGLE_SERVICE_ACCOUNT_KEY ||
(process.env.GOOGLE_AUTH_CLIENT_EMAIL &&
process.env.GOOGLE_AUTH_PRIVATE_KEY));
};
// Enhanced Vertex settings creation with authentication fallback and proxy support
const createVertexSettings = () => {
const baseSettings = {
project: getVertexProjectId(),
location: getVertexLocation(),
fetch: createProxyFetch(),
};
// Check for principal account authentication first (recommended for production)
if (process.env.GOOGLE_APPLICATION_CREDENTIALS) {
logger.debug("Using principal account authentication (recommended)", {
credentialsPath: process.env.GOOGLE_APPLICATION_CREDENTIALS
? "[PROVIDED]"
: "[NOT_PROVIDED]",
authMethod: "principal_account",
});
// For principal account auth, we don't need to provide explicit credentials
// The google-auth-library will use GOOGLE_APPLICATION_CREDENTIALS automatically
return baseSettings;
}
// Fallback to explicit credentials for development
if (process.env.GOOGLE_AUTH_CLIENT_EMAIL &&
process.env.GOOGLE_AUTH_PRIVATE_KEY) {
logger.debug("Using explicit credentials authentication", {
authMethod: "explicit_credentials",
hasClientEmail: !!process.env.GOOGLE_AUTH_CLIENT_EMAIL,
hasPrivateKey: !!process.env.GOOGLE_AUTH_PRIVATE_KEY,
});
return {
...baseSettings,
googleAuthOptions: {
credentials: {
client_email: process.env.GOOGLE_AUTH_CLIENT_EMAIL,
private_key: process.env.GOOGLE_AUTH_PRIVATE_KEY.replace(/\\n/g, "\n"),
},
},
};
}
// Log warning if no valid authentication is available
logger.warn("No valid authentication found for Google Vertex AI", {
authMethod: "none",
hasPrincipalAccount: !!process.env.GOOGLE_APPLICATION_CREDENTIALS,
hasExplicitCredentials: !!(process.env.GOOGLE_AUTH_CLIENT_EMAIL &&
process.env.GOOGLE_AUTH_PRIVATE_KEY),
});
return baseSettings;
};
// Helper function to determine if a model is an Anthropic model
const isAnthropicModel = (modelName) => {
return modelName.toLowerCase().includes("claude");
};
/**
* Google Vertex AI Provider v2 - BaseProvider Implementation
*
* Features:
* - Extends BaseProvider for shared functionality
* - Preserves existing Google Cloud authentication
* - Maintains Anthropic model support via dynamic imports
* - Fresh model creation for each request
* - Enhanced error handling with setup guidance
* - Tool registration and context management
*/
export class GoogleVertexProvider extends BaseProvider {
projectId;
location;
registeredTools = new Map();
toolContext = {};
// Memory-managed cache for model configuration lookups to avoid repeated calls
// Uses WeakMap for automatic cleanup and bounded LRU for recently used models
static modelConfigCache = new Map();
static modelConfigCacheTime = 0;
static CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
static MAX_CACHE_SIZE = 50; // Prevent memory leaks by limiting cache size
// Memory-managed cache for maxTokens handling decisions to optimize streaming performance
static maxTokensCache = new Map();
static maxTokensCacheTime = 0;
constructor(modelName, providerName, sdk) {
super(modelName, "vertex", sdk);
// Validate Google Cloud credentials - now using consolidated utility
if (!hasGoogleCredentials()) {
validateApiKey(createGoogleAuthConfig());
}
// Initialize Google Cloud configuration
this.projectId = getVertexProjectId();
this.location = getVertexLocation();
logger.debug("Google Vertex AI BaseProvider v2 initialized", {
modelName: this.modelName,
projectId: this.projectId,
location: this.location,
provider: this.providerName,
});
}
getProviderName() {
return "vertex";
}
getDefaultModel() {
return getDefaultVertexModel();
}
/**
* Returns the Vercel AI SDK model instance for Google Vertex
* Creates fresh model instances for each request
*/
async getAISDKModel() {
const model = await this.getModel();
return model;
}
/**
* Gets the appropriate model instance (Google or Anthropic)
* Creates fresh instances for each request to ensure proper authentication
*/
async getModel() {
const modelName = this.modelName || getDefaultVertexModel();
// Check if this is an Anthropic model
if (isAnthropicModel(modelName)) {
logger.debug("Creating Anthropic model for Vertex AI", { modelName });
const anthropicModel = await this.createAnthropicModel(modelName);
if (anthropicModel) {
return anthropicModel;
}
// Fall back to regular model if Anthropic not available
logger.warn(`Anthropic model ${modelName} requested but not available, falling back to Google model`);
}
// Create fresh Google Vertex model with current settings
logger.debug("Creating Google Vertex model", {
modelName,
project: this.projectId,
location: this.location,
});
const vertex = createVertex(createVertexSettings());
const model = vertex(modelName);
return model;
}
// executeGenerate removed - BaseProvider handles all generation with tools
async executeStream(options, analysisSchema) {
const functionTag = "GoogleVertexProvider.executeStream";
let chunkCount = 0;
// Add timeout controller for consistency with other providers
const timeout = this.getTimeout(options);
const timeoutController = createTimeoutController(timeout, this.providerName, "stream");
try {
this.validateStreamOptions(options);
// Build message array from options
const messages = buildMessagesArray(options);
logger.debug(`${functionTag}: Starting stream request`, {
modelName: this.modelName,
promptLength: options.input.text.length,
hasSchema: !!analysisSchema,
});
const model = await this.getModel();
// Get all available tools (direct + MCP + external) for streaming
const shouldUseTools = !options.disableTools && this.supportsTools();
const tools = shouldUseTools ? await this.getAllTools() : {};
logger.debug(`${functionTag}: Tools for streaming`, {
shouldUseTools,
toolCount: Object.keys(tools).length,
toolNames: Object.keys(tools),
});
// Model-specific maxTokens handling
const modelName = this.modelName || getDefaultVertexModel();
// Use cached model configuration to determine maxTokens handling for streaming performance
// This avoids hardcoded model-specific logic and repeated config lookups
const shouldSetMaxTokens = this.shouldSetMaxTokensCached(modelName);
const maxTokens = shouldSetMaxTokens
? options.maxTokens || DEFAULT_MAX_TOKENS
: undefined;
// Build complete stream options with proper typing
let streamOptions = {
model: model,
messages: messages,
temperature: options.temperature,
...(maxTokens && { maxTokens }),
// Add tools support for streaming
...(shouldUseTools &&
Object.keys(tools).length > 0 && {
tools,
toolChoice: "auto",
maxSteps: options.maxSteps || DEFAULT_MAX_STEPS,
}),
abortSignal: timeoutController?.controller.signal,
onError: (event) => {
const error = event.error;
const errorMessage = error instanceof Error ? error.message : String(error);
logger.error(`${functionTag}: Stream error`, {
provider: this.providerName,
modelName: this.modelName,
error: errorMessage,
chunkCount,
});
},
onFinish: (event) => {
logger.debug(`${functionTag}: Stream finished`, {
finishReason: event.finishReason,
totalChunks: chunkCount,
});
},
onChunk: () => {
chunkCount++;
},
};
if (analysisSchema) {
try {
streamOptions = {
...streamOptions,
experimental_output: Output.object({
schema: analysisSchema,
}),
};
}
catch (error) {
logger.warn("Schema application failed, continuing without schema", {
error: String(error),
});
}
}
const result = streamText(streamOptions);
timeoutController?.cleanup();
// Transform string stream to content object stream using BaseProvider method
const transformedStream = this.createTextStream(result);
// Track tool calls and results for streaming
const toolCalls = [];
const toolResults = [];
return {
stream: transformedStream,
provider: this.providerName,
model: this.modelName,
...(shouldUseTools && {
toolCalls,
toolResults,
}),
};
}
catch (error) {
timeoutController?.cleanup();
logger.error(`${functionTag}: Exception`, {
provider: this.providerName,
modelName: this.modelName,
error: String(error),
chunkCount,
});
throw this.handleProviderError(error);
}
}
handleProviderError(error) {
const errorRecord = error;
if (typeof errorRecord?.name === "string" &&
errorRecord.name === "TimeoutError") {
return new TimeoutError(`Google Vertex AI request timed out. Consider increasing timeout or using a lighter model.`, this.defaultTimeout);
}
const message = typeof errorRecord?.message === "string"
? errorRecord.message
: "Unknown error occurred";
if (message.includes("PERMISSION_DENIED")) {
return new Error(`❌ Google Vertex AI Permission Denied\n\nYour Google Cloud credentials don't have permission to access Vertex AI.\n\nRequired Steps:\n1. Ensure your service account has Vertex AI User role\n2. Check if Vertex AI API is enabled in your project\n3. Verify your project ID is correct\n4. Confirm your location/region has Vertex AI available`);
}
if (message.includes("NOT_FOUND")) {
const modelSuggestions = this.getModelSuggestions(this.modelName);
return new Error(`❌ Google Vertex AI Model Not Found\n\n${message}\n\nModel '${this.modelName}' is not available.\n\nSuggested alternatives:\n${modelSuggestions}\n\nTroubleshooting:\n1. Check model name spelling and format\n2. Verify model is available in your region (${this.location})\n3. Ensure your project has access to the model\n4. For Claude models, enable Anthropic integration in Google Cloud Console`);
}
if (message.includes("QUOTA_EXCEEDED")) {
return new Error(`❌ Google Vertex AI Quota Exceeded\n\n${message}\n\nSolutions:\n1. Check your Vertex AI quotas in Google Cloud Console\n2. Request quota increase if needed\n3. Try a different model or reduce request frequency\n4. Consider using a different region`);
}
if (message.includes("INVALID_ARGUMENT")) {
return new Error(`❌ Google Vertex AI Invalid Request\n\n${message}\n\nCheck:\n1. Request parameters are within model limits\n2. Input text is properly formatted\n3. Temperature and other settings are valid\n4. Model supports your request type`);
}
return new Error(`❌ Google Vertex AI Provider Error\n\n${message}\n\nTroubleshooting:\n1. Check Google Cloud credentials and permissions\n2. Verify project ID and location settings\n3. Ensure Vertex AI API is enabled\n4. Check network connectivity`);
}
/**
* Memory-safe cache management for model configurations
* Implements LRU eviction to prevent memory leaks in long-running processes
*/
static evictLRUCacheEntries(cache) {
if (cache.size <= GoogleVertexProvider.MAX_CACHE_SIZE) {
return;
}
// Evict oldest entries (first entries in Map are oldest in insertion order)
const entriesToRemove = cache.size - GoogleVertexProvider.MAX_CACHE_SIZE + 5; // Remove extra to avoid frequent evictions
let removed = 0;
for (const key of cache.keys()) {
if (removed >= entriesToRemove) {
break;
}
cache.delete(key);
removed++;
}
logger.debug("GoogleVertexProvider: Evicted LRU cache entries", {
entriesRemoved: removed,
currentCacheSize: cache.size,
});
}
/**
* Access and refresh cache entry (moves to end for LRU)
*/
static accessCacheEntry(cache, key) {
const value = cache.get(key);
if (value !== undefined) {
// Move to end (most recently used)
cache.delete(key);
cache.set(key, value);
}
return value;
}
/**
* Memory-safe cached check for whether maxTokens should be set for the given model
* Optimized for streaming performance with LRU eviction to prevent memory leaks
*/
shouldSetMaxTokensCached(modelName) {
const now = Date.now();
// Check if cache is valid (within 5 minutes)
if (now - GoogleVertexProvider.maxTokensCacheTime >
GoogleVertexProvider.CACHE_DURATION) {
// Cache expired, refresh all cached results
GoogleVertexProvider.maxTokensCache.clear();
GoogleVertexProvider.maxTokensCacheTime = now;
}
// Check if we have cached result for this model (with LRU access)
const cachedResult = GoogleVertexProvider.accessCacheEntry(GoogleVertexProvider.maxTokensCache, modelName);
if (cachedResult !== undefined) {
return cachedResult;
}
// Calculate and cache the result with memory management
const shouldSet = !this.modelHasMaxTokensIssues(modelName);
GoogleVertexProvider.maxTokensCache.set(modelName, shouldSet);
// Prevent memory leaks by evicting old entries if cache grows too large
GoogleVertexProvider.evictLRUCacheEntries(GoogleVertexProvider.maxTokensCache);
return shouldSet;
}
/**
* Memory-safe check if model has maxTokens issues using configuration-based approach
* This replaces hardcoded model-specific logic with configurable behavior
* Includes LRU caching to avoid repeated configuration lookups during streaming
*/
modelHasMaxTokensIssues(modelName) {
const now = Date.now();
const cacheKey = "google-vertex-config";
// Check if cache is valid (within 5 minutes)
if (now - GoogleVertexProvider.modelConfigCacheTime >
GoogleVertexProvider.CACHE_DURATION) {
// Cache expired, refresh it with memory management
GoogleVertexProvider.modelConfigCache.clear();
const config = ModelConfigurationManager.getInstance();
const vertexConfig = config.getProviderConfig("google-vertex");
GoogleVertexProvider.modelConfigCache.set(cacheKey, vertexConfig);
GoogleVertexProvider.modelConfigCacheTime = now;
}
// Access cached config with LRU behavior
const vertexConfig = GoogleVertexProvider.accessCacheEntry(GoogleVertexProvider.modelConfigCache, cacheKey);
// Check if model is in the list of models with maxTokens issues
const modelsWithIssues = vertexConfig?.modelBehavior?.maxTokensIssues || [
"gemini-2.5-flash",
"gemini-2.5-pro",
];
return modelsWithIssues.some((problematicModel) => modelName.includes(problematicModel));
}
/**
* Check if Anthropic models are available
* @returns Promise<boolean> indicating if Anthropic support is available
*/
async hasAnthropicSupport() {
const createVertexAnthropic = await getCreateVertexAnthropic();
return createVertexAnthropic !== null;
}
/**
* Create an Anthropic model instance if available
* Uses fresh vertex settings for each request
* @param modelName Anthropic model name (e.g., 'claude-3-sonnet@20240229')
* @returns LanguageModelV1 instance or null if not available
*/
createAnthropicModel(modelName) {
return getCreateVertexAnthropic().then((createVertexAnthropic) => {
if (!createVertexAnthropic) {
return null;
}
// Use fresh vertex settings instead of cached config
// Type guard to ensure createVertexAnthropic is callable
if (typeof createVertexAnthropic !== "function") {
throw new Error("createVertexAnthropic is not a function");
}
const vertexSettings = createVertexSettings();
const vertexAnthropicInstance = createVertexAnthropic(vertexSettings);
// Type guard to ensure the returned instance has the expected model creation method
if (!vertexAnthropicInstance ||
typeof vertexAnthropicInstance !== "function") {
throw new Error("Failed to create valid Anthropic instance");
}
const model = vertexAnthropicInstance(modelName);
// Type guard to ensure the returned model implements LanguageModelV1
if (!model ||
typeof model !== "object" ||
!("specificationVersion" in model)) {
throw new Error("Failed to create valid LanguageModelV1 instance");
}
return model;
});
}
/**
* Register a tool with the AI provider
* @param name The name of the tool
* @param schema The Zod schema defining the tool's parameters
* @param description A description of what the tool does
* @param handler The function to execute when the tool is called
*/
registerTool(name, schema, description, handler) {
const functionTag = "GoogleVertexProvider.registerTool";
try {
const tool = {
description,
parameters: schema,
execute: async (params) => {
try {
const contextEnrichedParams = {
...params,
__context: this.toolContext,
};
return await handler(contextEnrichedParams);
}
catch (error) {
logger.error(`${functionTag}: Tool execution error`, {
toolName: name,
error: error instanceof Error ? error.message : String(error),
});
throw error;
}
},
};
this.registeredTools.set(name, tool);
logger.debug(`${functionTag}: Tool registered`, {
toolName: name,
modelName: this.modelName,
});
}
catch (error) {
logger.error(`${functionTag}: Tool registration error`, {
toolName: name,
error: error instanceof Error ? error.message : String(error),
});
throw error;
}
}
/**
* Set the context for tool execution
* @param context The context to use for tool execution
*/
setToolContext(context) {
this.toolContext = { ...this.toolContext, ...context };
logger.debug("GoogleVertexProvider.setToolContext: Tool context set", {
contextKeys: Object.keys(context),
});
}
/**
* Get the current tool execution context
* @returns The current tool execution context
*/
getToolContext() {
return { ...this.toolContext };
}
/**
* Set the tool executor function for custom tool execution
* This method is called by BaseProvider.setupToolExecutor()
* @param executor Function to execute tools by name
*/
setToolExecutor(executor) {
this.toolExecutor = executor;
logger.debug("GoogleVertexProvider.setToolExecutor: Tool executor set", {
hasExecutor: typeof executor === "function",
});
}
/**
* Clear all static caches - useful for testing and memory cleanup
* Public method to allow external cache management
*/
static clearCaches() {
GoogleVertexProvider.modelConfigCache.clear();
GoogleVertexProvider.maxTokensCache.clear();
GoogleVertexProvider.modelConfigCacheTime = 0;
GoogleVertexProvider.maxTokensCacheTime = 0;
logger.debug("GoogleVertexProvider: All caches cleared", {
clearedAt: Date.now(),
});
}
/**
* Get cache statistics for monitoring and debugging
*/
static getCacheStats() {
const now = Date.now();
return {
modelConfigCacheSize: GoogleVertexProvider.modelConfigCache.size,
maxTokensCacheSize: GoogleVertexProvider.maxTokensCache.size,
maxCacheSize: GoogleVertexProvider.MAX_CACHE_SIZE,
cacheAge: {
modelConfig: now - GoogleVertexProvider.modelConfigCacheTime,
maxTokens: now - GoogleVertexProvider.maxTokensCacheTime,
},
};
}
/**
* Get model suggestions when a model is not found
*/
getModelSuggestions(requestedModel) {
const availableModels = {
google: [
"gemini-2.5-pro",
"gemini-2.5-flash",
"gemini-2.5-flash-lite",
"gemini-2.0-flash-001",
"gemini-1.5-pro",
"gemini-1.5-flash",
],
claude: [
"claude-sonnet-4@20250514",
"claude-opus-4@20250514",
"claude-3-5-sonnet-20241022",
"claude-3-5-haiku-20241022",
"claude-3-sonnet-20240229",
"claude-3-haiku-20240307",
"claude-3-opus-20240229",
],
};
let suggestions = "\n🤖 Google Models (always available):\n";
availableModels.google.forEach((model) => {
suggestions += ` • ${model}\n`;
});
suggestions += "\n🧠 Claude Models (requires Anthropic integration):\n";
availableModels.claude.forEach((model) => {
suggestions += ` • ${model}\n`;
});
// If the requested model looks like a Claude model, provide specific guidance
if (requestedModel && requestedModel.toLowerCase().includes("claude")) {
suggestions += `\n💡 Tip: "${requestedModel}" appears to be a Claude model.\n`;
suggestions +=
"Ensure Anthropic integration is enabled in your Google Cloud project.\n";
suggestions += "Try using an available Claude model from the list above.";
}
return suggestions;
}
}
export default GoogleVertexProvider;
// Re-export for compatibility
export { GoogleVertexProvider as GoogleVertexAI };