UNPKG

@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
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 };