@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
434 lines (433 loc) • 16.3 kB
JavaScript
/**
* Utility functions for AI provider management
* Consolidated from providerUtils-fixed.ts
*/
import { AIProviderFactory } from "../core/factory.js";
import { logger } from "./logger.js";
import { ProviderHealthChecker } from "./providerHealth.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, check health first
try {
const health = await ProviderHealthChecker.checkProviderHealth(requestedProvider, { includeConnectivityTest: false, cacheResults: true });
if (health.isHealthy) {
logger.debug(`[getBestProvider] Using healthy explicitly requested provider: ${requestedProvider}`);
return requestedProvider;
}
else {
logger.warn(`[getBestProvider] Requested provider ${requestedProvider} is unhealthy, finding alternative`, { error: health.error });
}
}
catch (error) {
logger.warn(`[getBestProvider] Health check failed for ${requestedProvider}, using anyway`, { error: error instanceof Error ? error.message : String(error) });
return requestedProvider; // Return anyway for explicit requests
}
}
// 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_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:
* - Vertex (Google Cloud AI) is prioritized first for its enterprise-grade reliability and advanced model capabilities.
* - Google AI follows as second 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 in our use cases.
* - Ollama is kept as a fallback for local deployments when available.
* Please update this comment if the order is changed in the future, and document the rationale for maintainability.
*/
const providers = [
"vertex", // Prioritize Google Cloud AI (Vertex) first
"google-ai", // Google AI ecosystem support
"openai", // Reliable with broad model support
"anthropic",
"bedrock",
"azure",
"mistral",
"huggingface",
"ollama", // Keep as fallback
];
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 === "ollama") {
try {
const response = await fetch("http://localhost:11434/api/tags", {
method: "GET",
signal: AbortSignal.timeout(2000),
});
if (response.ok) {
const { models } = await response.json();
const defaultOllamaModel = "llama3.2:latest";
return models.some((m) => m.name === defaultOllamaModel);
}
return false;
}
catch (error) {
return false;
}
}
try {
const provider = await AIProviderFactory.createProvider(providerName);
await provider.generate({ prompt: "test", maxTokens: 1 });
return true;
}
catch (error) {
return false;
}
}
/**
* Google Cloud Project ID validation regex
* Format requirements:
* - Must start with a lowercase letter
* - Can contain lowercase letters, numbers, and hyphens
* - Must end with a lowercase letter or number
* - Total length must be 6-30 characters
*/
const GOOGLE_CLOUD_PROJECT_ID_REGEX = /^[a-z][a-z0-9-]{4,28}[a-z0-9]$/;
/**
* 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 (!GOOGLE_CLOUD_PROJECT_ID_REGEX.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 (!/^sk-[A-Za-z0-9]{48,}$/.test(apiKey)) {
result.invalidVars.push("OPENAI_API_KEY (should start with 'sk-' followed by 48+ characters)");
}
}
/**
* Validate Anthropic credentials
*/
function validateAnthropicCredentials(result) {
const apiKey = process.env.ANTHROPIC_API_KEY;
if (!apiKey) {
result.missingVars.push("ANTHROPIC_API_KEY");
}
else if (!/^sk-ant-[A-Za-z0-9-_]{95,}$/.test(apiKey)) {
result.invalidVars.push("ANTHROPIC_API_KEY (should start with 'sk-ant-' followed by 95+ 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 (!/^[a-f0-9]{32}$/.test(apiKey)) {
result.invalidVars.push("AZURE_OPENAI_API_KEY (should be 32 hexadecimal 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 (!/^[A-Za-z0-9_-]{39}$/.test(apiKey)) {
result.invalidVars.push("GOOGLE_AI_API_KEY (should be 39 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 (!/^hf_[A-Za-z0-9]{37}$/.test(apiKey)) {
result.invalidVars.push("HUGGINGFACE_API_KEY (should start with 'hf_' followed by 37 characters)");
}
}
/**
* Validate Mistral credentials
*/
function validateMistralCredentials(result) {
const apiKey = process.env.MISTRAL_API_KEY;
if (!apiKey) {
result.missingVars.push("MISTRAL_API_KEY");
}
else if (!/^[A-Za-z0-9]{32,}$/.test(apiKey)) {
result.invalidVars.push("MISTRAL_API_KEY (should be 32+ 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",
"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());
}