UNPKG

@juspay/neurolink

Version:

Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applicatio

431 lines 16.8 kB
/** * Utility functions for AI provider management * Consolidated from providerUtils-fixed.ts */ import { AIProviderFactory } from "../core/factory.js"; import { logger } from "./logger.js"; import { AIProviderName } from "../constants/enums.js"; import { ProviderHealthChecker } from "./providerHealth.js"; import { API_KEY_FORMATS, API_KEY_LENGTHS, PROJECT_ID_FORMAT, } from "./providerConfig.js"; /** * Get the best available provider based on real-time availability checks * Enhanced version consolidated from providerUtils-fixed.ts * @param requestedProvider - Optional preferred provider name * @returns The best provider name to use */ export async function getBestProvider(requestedProvider) { // Check requested provider FIRST - explicit user choice overrides defaults if (requestedProvider && requestedProvider !== "auto") { // For explicit provider requests, ALWAYS honor the request // Never override explicit provider selection with health-based fallbacks logger.debug(`[getBestProvider] Using explicitly requested provider: ${requestedProvider}`); // Optional health check for logging purposes only try { const health = await ProviderHealthChecker.checkProviderHealth(requestedProvider, { includeConnectivityTest: false, cacheResults: true }); if (health.isHealthy) { logger.debug(`[getBestProvider] Explicitly requested provider ${requestedProvider} is healthy`); } else { logger.warn(`[getBestProvider] Explicitly requested provider ${requestedProvider} may have issues, but using anyway`, { error: health.error }); } } catch (error) { logger.warn(`[getBestProvider] Health check failed for explicitly requested provider ${requestedProvider}, using anyway`, { error: error instanceof Error ? error.message : String(error) }); } // ALWAYS return the explicitly requested provider return requestedProvider; } // Use health checker to get best available provider const healthyProvider = await ProviderHealthChecker.getBestHealthyProvider(); if (healthyProvider) { logger.debug(`[getBestProvider] Selected healthy provider: ${healthyProvider}`); return healthyProvider; } // Fallback to legacy provider checking if health system fails logger.warn("[getBestProvider] Health system failed, falling back to legacy checking"); // Check for explicit default provider in env (only when no provider requested) if (process.env.DEFAULT_PROVIDER && (await isProviderAvailable(process.env.DEFAULT_PROVIDER))) { logger.debug(`[getBestProvider] Using default provider from env: ${process.env.DEFAULT_PROVIDER}`); return process.env.DEFAULT_PROVIDER; } // Special case for Ollama - prioritize local when available if ((process.env.OLLAMA_BASE_URL || process.env.OLLAMA_API_BASE) && process.env.OLLAMA_MODEL) { try { if (await isProviderAvailable("ollama")) { logger.debug(`[getBestProvider] Prioritizing working local Ollama`); return "ollama"; // Prioritize working local AI } } catch { // Fall through to cloud providers } } /** * Provider priority order rationale: * - LiteLLM and Ollama are prioritized first for local/self-hosted deployments, * avoiding unnecessary dependence on external providers during fallback scenarios. * - Vertex (Google Cloud AI) follows for enterprise-grade reliability. * - Google AI follows as second cloud priority for comprehensive Google AI ecosystem support. * - OpenAI maintains high priority due to its consistent reliability and broad model support. * - Other providers are ordered based on a combination of reliability, feature set, and historical performance. * Please update this comment if the order is changed in the future, and document the rationale for maintainability. */ const providers = [ "litellm", // Prioritize self-hosted proxy deployments first "ollama", // Local models when the configured runtime target is installed "vertex", // Google Cloud AI (enterprise) "google-ai", // Google AI ecosystem support "openai", // Reliable with broad model support "anthropic", "bedrock", "azure", "mistral", "huggingface", ]; for (const provider of providers) { if (await isProviderAvailable(provider)) { logger.debug(`[getBestProvider] Selected provider: ${provider}`); return provider; } } throw new Error("No available AI providers. Please check your configurations."); } /** * Check if a provider is truly available by performing a quick authentication test. * Enhanced function consolidated from providerUtils-fixed.ts * @param providerName - The name of the provider to check. * @returns True if the provider is available and authenticated. */ async function isProviderAvailable(providerName) { if (!hasProviderEnvVars(providerName) && providerName !== "ollama") { return false; } if (providerName === "litellm") { const availability = await ProviderHealthChecker.checkFallbackProviderAvailability(AIProviderName.LITELLM, process.env.LITELLM_MODEL || "openai/gpt-4o-mini"); return availability.available; } if (providerName === "ollama") { const availability = await ProviderHealthChecker.checkFallbackProviderAvailability(AIProviderName.OLLAMA, process.env.OLLAMA_MODEL || "llama3.1:8b"); return availability.available; } try { const provider = await AIProviderFactory.createProvider(providerName); await provider.generate({ prompt: "test", maxTokens: 1 }); return true; } catch { return false; } } /** * Validate environment variable values for a provider * Addresses GitHub Copilot comment about adding environment variable validation * @param provider - Provider name to validate * @returns Validation result with detailed information */ export function validateProviderEnvVars(provider) { const result = { isValid: true, missingVars: [], invalidVars: [], warnings: [], }; switch (provider.toLowerCase()) { case "bedrock": case "amazon": case "aws": validateAwsCredentials(result); break; case "vertex": case "googlevertex": case "google": case "gemini": validateVertexCredentials(result); break; case "openai": case "gpt": validateOpenAICredentials(result); break; case "anthropic": case "claude": validateAnthropicCredentials(result); break; case "azure": case "azureopenai": validateAzureCredentials(result); break; case "google-ai": case "google-studio": validateGoogleAICredentials(result); break; case "huggingface": case "hugging-face": case "hf": validateHuggingFaceCredentials(result); break; case "mistral": case "mistral-ai": case "mistralai": validateMistralCredentials(result); break; case "ollama": case "local": case "local-ollama": // Ollama doesn't require environment variables break; case "litellm": // LiteLLM validation can be added if needed break; default: result.isValid = false; result.warnings.push(`Unknown provider: ${provider}`); } result.isValid = result.missingVars.length === 0 && result.invalidVars.length === 0; return result; } /** * Validate AWS credentials with flexible validation * Note: AWS credential formats can vary, so validation is kept reasonably flexible */ function validateAwsCredentials(result) { const accessKeyId = process.env.AWS_ACCESS_KEY_ID; const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY; const region = process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION; if (!accessKeyId) { result.missingVars.push("AWS_ACCESS_KEY_ID"); } else if (!/^[A-Z0-9]{16,}$/.test(accessKeyId)) { // Flexible validation: at least 16 uppercase alphanumeric characters result.invalidVars.push("AWS_ACCESS_KEY_ID (should be uppercase alphanumeric characters, typically 20 chars)"); } if (!secretAccessKey) { result.missingVars.push("AWS_SECRET_ACCESS_KEY"); } else if (!/^[A-Za-z0-9+/]{30,}$/.test(secretAccessKey)) { // Flexible validation: at least 30 base64 characters (can vary in length) result.invalidVars.push("AWS_SECRET_ACCESS_KEY (should be base64 characters, typically 40+ chars)"); } if (!region) { result.warnings.push("AWS_REGION not set, will use default region"); } } /** * Validate Google Vertex credentials */ function validateVertexCredentials(result) { const projectId = process.env.GOOGLE_CLOUD_PROJECT_ID || process.env.VERTEX_PROJECT_ID || process.env.GOOGLE_VERTEX_PROJECT || process.env.GOOGLE_CLOUD_PROJECT; const hasCredentials = process.env.GOOGLE_APPLICATION_CREDENTIALS || process.env.GOOGLE_SERVICE_ACCOUNT_KEY || (process.env.GOOGLE_AUTH_CLIENT_EMAIL && process.env.GOOGLE_AUTH_PRIVATE_KEY); if (!projectId) { result.missingVars.push("GOOGLE_CLOUD_PROJECT_ID (or variant)"); } else if (!PROJECT_ID_FORMAT.PATTERN.test(projectId)) { result.invalidVars.push("Project ID format invalid (must be 6-30 lowercase letters, digits, hyphens)"); } if (!hasCredentials) { result.missingVars.push("Google credentials (GOOGLE_APPLICATION_CREDENTIALS or explicit auth)"); } if (process.env.GOOGLE_AUTH_CLIENT_EMAIL && !isValidEmail(process.env.GOOGLE_AUTH_CLIENT_EMAIL)) { result.invalidVars.push("GOOGLE_AUTH_CLIENT_EMAIL (invalid email format)"); } } /** * Validate OpenAI credentials */ function validateOpenAICredentials(result) { const apiKey = process.env.OPENAI_API_KEY; if (!apiKey) { result.missingVars.push("OPENAI_API_KEY"); } else if (!API_KEY_FORMATS.openai.test(apiKey)) { result.invalidVars.push(`OPENAI_API_KEY (should start with 'sk-' followed by ${API_KEY_LENGTHS.OPENAI_MIN}+ characters)`); } } /** * Validate Anthropic credentials */ function validateAnthropicCredentials(result) { const apiKey = process.env.ANTHROPIC_API_KEY; if (!apiKey) { result.missingVars.push("ANTHROPIC_API_KEY"); } else if (!API_KEY_FORMATS.anthropic.test(apiKey)) { result.invalidVars.push(`ANTHROPIC_API_KEY (should start with 'sk-ant-' followed by ${API_KEY_LENGTHS.ANTHROPIC_MIN}+ characters)`); } } /** * Validate Azure credentials */ function validateAzureCredentials(result) { const apiKey = process.env.AZURE_OPENAI_API_KEY; const endpoint = process.env.AZURE_OPENAI_ENDPOINT; if (!apiKey) { result.missingVars.push("AZURE_OPENAI_API_KEY"); } else if (!API_KEY_FORMATS.azure.test(apiKey)) { result.invalidVars.push(`AZURE_OPENAI_API_KEY (should be at least ${API_KEY_LENGTHS.AZURE_MIN} alphanumeric characters)`); } if (!endpoint) { result.missingVars.push("AZURE_OPENAI_ENDPOINT"); } else if (!isValidUrl(endpoint)) { result.invalidVars.push("AZURE_OPENAI_ENDPOINT (should be a valid HTTPS URL)"); } } /** * Validate Google AI credentials */ function validateGoogleAICredentials(result) { const apiKey = process.env.GOOGLE_AI_API_KEY || process.env.GOOGLE_GENERATIVE_AI_API_KEY; if (!apiKey) { result.missingVars.push("GOOGLE_AI_API_KEY (or GOOGLE_GENERATIVE_AI_API_KEY)"); } else if (!API_KEY_FORMATS["google-ai"].test(apiKey)) { result.invalidVars.push(`GOOGLE_AI_API_KEY (should be ${API_KEY_LENGTHS.GOOGLE_AI_EXACT} alphanumeric characters with dashes/underscores)`); } } /** * Validate HuggingFace credentials */ function validateHuggingFaceCredentials(result) { const apiKey = process.env.HUGGINGFACE_API_KEY || process.env.HF_TOKEN; if (!apiKey) { result.missingVars.push("HUGGINGFACE_API_KEY (or HF_TOKEN)"); } else if (!API_KEY_FORMATS.huggingface.test(apiKey)) { result.invalidVars.push(`HUGGINGFACE_API_KEY (should start with 'hf_' followed by ${API_KEY_LENGTHS.HUGGINGFACE_EXACT} characters)`); } } /** * Validate Mistral credentials */ function validateMistralCredentials(result) { const apiKey = process.env.MISTRAL_API_KEY; if (!apiKey) { result.missingVars.push("MISTRAL_API_KEY"); } else if (!API_KEY_FORMATS.mistral.test(apiKey)) { result.invalidVars.push(`MISTRAL_API_KEY (should be ${API_KEY_LENGTHS.MISTRAL_EXACT} alphanumeric characters)`); } } /** * Helper function to validate email format */ function isValidEmail(email) { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(email); } /** * Helper function to validate URL format */ function isValidUrl(url) { try { const parsedUrl = new URL(url); return parsedUrl.protocol === "https:"; } catch { return false; } } /** * Check if a provider has the minimum required environment variables * NOTE: This only checks if variables exist, not if they're valid * For validation, use validateProviderEnvVars instead * @param provider - Provider name to check * @returns True if the provider has required environment variables */ export function hasProviderEnvVars(provider) { switch (provider.toLowerCase()) { case "bedrock": case "amazon": case "aws": return !!(process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY); case "vertex": case "googlevertex": case "google": case "gemini": return !!((process.env.GOOGLE_CLOUD_PROJECT_ID || process.env.VERTEX_PROJECT_ID || process.env.GOOGLE_VERTEX_PROJECT || process.env.GOOGLE_CLOUD_PROJECT) && (process.env.GOOGLE_APPLICATION_CREDENTIALS || process.env.GOOGLE_SERVICE_ACCOUNT_KEY || (process.env.GOOGLE_AUTH_CLIENT_EMAIL && process.env.GOOGLE_AUTH_PRIVATE_KEY))); case "openai": case "gpt": return !!process.env.OPENAI_API_KEY; case "anthropic": case "claude": return !!process.env.ANTHROPIC_API_KEY; case "azure": case "azureopenai": return !!process.env.AZURE_OPENAI_API_KEY; case "google-ai": case "google-studio": return !!(process.env.GOOGLE_AI_API_KEY || process.env.GOOGLE_GENERATIVE_AI_API_KEY); case "huggingface": case "hugging-face": case "hf": return !!(process.env.HUGGINGFACE_API_KEY || process.env.HF_TOKEN); case "ollama": case "local": case "local-ollama": // For Ollama, we check if the service is potentially available // This is a basic check - actual connectivity will be verified during usage return true; // Ollama doesn't require environment variables, just local service case "mistral": case "mistral-ai": case "mistralai": return !!process.env.MISTRAL_API_KEY; case "litellm": // LiteLLM requires a proxy server, which can be checked for availability // Default base URL is assumed, or can be configured via environment return true; // LiteLLM proxy availability will be checked during usage default: return false; } } /** * Get available provider names * @returns Array of available provider names */ export function getAvailableProviders() { return [ "bedrock", "vertex", "openai", "anthropic", "azure", "google-ai", "litellm", "huggingface", "ollama", "mistral", ]; } /** * Validate provider name * @param provider - Provider name to validate * @returns True if provider name is valid */ export function isValidProvider(provider) { return getAvailableProviders().includes(provider.toLowerCase()); } /** * Type guard for provider error */ export function isProviderError(error) { return error instanceof Error && "provider" in error; } //# sourceMappingURL=providerUtils.js.map