adpa-enterprise-framework-automation
Version:
Modular, standards-compliant Node.js/TypeScript automation framework for enterprise requirements, project, and data management. Provides CLI and API for BABOK v3, PMBOK 7th Edition, and DMBOK 2.0 (in progress). Production-ready Express.js API with TypeSpe
362 lines • 15.5 kB
JavaScript
/**
* Provider Manager for Requirements Gathering Agent
* Handles provider configuration, validation, and failover
*/
import { DefaultAzureCredential } from '@azure/identity';
import fetch from 'node-fetch';
export class ProviderManager {
providers = new Map();
metrics = new Map();
quotas = new Map();
activeProvider = null;
fallbackQueue = [];
rateLimiters = new Map();
constructor() {
this.initializeProviders();
// If CURRENT_PROVIDER is set and valid, use it as the active provider
const explicit = process.env.CURRENT_PROVIDER;
if (explicit && this.providers.has(explicit)) {
this.activeProvider = explicit;
// Fallback queue: all other valid providers by priority except the explicit one
this.fallbackQueue = Array.from(this.providers.keys()).filter(p => p !== explicit);
}
}
initializeProviders() {
// Google AI Studio
const googleAI = {
name: 'google-ai',
check: async () => {
if (!process.env.GOOGLE_AI_API_KEY)
return false;
try {
const response = await fetch('https://generativelanguage.googleapis.com/v1beta/models', {
headers: { 'x-goog-api-key': process.env.GOOGLE_AI_API_KEY }
});
return response.ok;
}
catch (error) {
console.warn('Google AI check failed:', error instanceof Error ? error.message : String(error));
return false;
}
}, priority: 5, // Lower priority than Azure OpenAI
description: 'Google AI with Gemini models - supports ultra-large context',
setupGuide: 'Visit https://makersuite.google.com/app/apikey to get your API key',
endpoint: 'https://generativelanguage.googleapis.com/v1beta',
tokenLimit: process.env.GOOGLE_AI_MODEL?.includes('pro') ? 2097152 : 1048576,
costPerToken: 0.00001
};
this.addProvider(googleAI);
// Azure OpenAI with Entra ID
const azureOpenAI = {
name: 'azure-openai',
check: async () => {
if (!(process.env.AZURE_OPENAI_ENDPOINT && process.env.USE_ENTRA_ID === 'true'))
return false;
try {
const credential = new DefaultAzureCredential({
managedIdentityClientId: process.env.AZURE_CLIENT_ID,
tenantId: process.env.AZURE_TENANT_ID
});
const token = await credential.getToken('https://cognitiveservices.azure.com/.default');
return !!token;
}
catch (error) {
console.warn('Azure Entra ID authentication failed:', error instanceof Error ? error.message : String(error));
return false;
}
},
priority: 2,
description: 'Azure OpenAI with Entra ID authentication - enterprise-grade',
endpoint: process.env.AZURE_OPENAI_ENDPOINT,
tokenLimit: process.env.DEPLOYMENT_NAME?.includes('32k') ? 32000 : 8000,
costPerToken: 0.00002
};
this.addProvider(azureOpenAI); // Azure AI Studio (with API key)
const azureAIStudio = {
name: 'azure-ai-studio',
check: async () => {
// Check for either AZURE_AI_* or AZURE_OPENAI_* variables
const endpoint = process.env.AZURE_AI_ENDPOINT || process.env.AZURE_OPENAI_ENDPOINT;
const apiKey = process.env.AZURE_AI_API_KEY || process.env.AZURE_OPENAI_API_KEY;
if (!(endpoint?.includes('openai.azure.com') && apiKey)) {
return false;
}
try {
const baseUrl = endpoint.endsWith('/') ? endpoint.slice(0, -1) : endpoint;
const response = await fetch(`${baseUrl}/openai/deployments`, {
headers: {
'api-key': apiKey,
'Content-Type': 'application/json'
}
});
return response.ok;
}
catch (error) {
console.warn('Azure AI Studio check failed:', error instanceof Error ? error.message : String(error));
return false;
}
},
priority: 2,
description: 'Azure OpenAI with API key authentication',
endpoint: process.env.AZURE_AI_ENDPOINT || process.env.AZURE_OPENAI_ENDPOINT,
tokenLimit: (process.env.REQUIREMENTS_AGENT_MODEL || process.env.AZURE_OPENAI_DEPLOYMENT_NAME)?.includes('32k') ? 32000 : 8000,
costPerToken: 0.00002
};
this.addProvider(azureAIStudio); // Azure OpenAI (API Key) - matching interactive menu ID
const azureOpenAIKey = {
name: 'azure-openai-key',
check: async () => {
// Check for either AZURE_AI_* or AZURE_OPENAI_* variables
const endpoint = process.env.AZURE_AI_ENDPOINT || process.env.AZURE_OPENAI_ENDPOINT;
const apiKey = process.env.AZURE_AI_API_KEY || process.env.AZURE_OPENAI_API_KEY;
if (!(endpoint?.includes('openai.azure.com') && apiKey)) {
return false;
}
// Simple validation - just check that we have required config
// Skip the network call for now to avoid validation issues
return true;
},
priority: 1, // Higher priority than Google AI
description: 'Azure OpenAI with API key authentication',
endpoint: process.env.AZURE_AI_ENDPOINT || process.env.AZURE_OPENAI_ENDPOINT,
tokenLimit: (process.env.REQUIREMENTS_AGENT_MODEL || process.env.AZURE_OPENAI_DEPLOYMENT_NAME)?.includes('32k') ? 32000 : 8000,
costPerToken: 0.00002
};
this.addProvider(azureOpenAIKey); // GitHub AI
const githubAI = {
name: 'github-ai',
check: async () => {
if (!process.env.GITHUB_TOKEN) {
return false;
}
// Support both GitHub endpoints (new preview and legacy)
const endpoint = process.env.GITHUB_ENDPOINT ||
process.env.AZURE_AI_ENDPOINT ||
'https://models.inference.ai.azure.com';
try {
// Use appropriate API path based on endpoint
const apiPath = endpoint.includes('models.github.ai') ? '/chat/completions' : '/models';
// Ensure proper URL construction without double slashes
const baseUrl = endpoint.endsWith('/') ? endpoint.slice(0, -1) : endpoint;
const response = await fetch(`${baseUrl}${apiPath}`, {
headers: {
'Authorization': `Bearer ${process.env.GITHUB_TOKEN}`,
'Content-Type': 'application/json'
}
});
return response.ok;
}
catch (error) {
console.warn('GitHub AI check failed:', error instanceof Error ? error.message : String(error));
return false;
}
},
priority: 3,
description: 'GitHub AI with GPT-4 - cost-effective',
endpoint: process.env.GITHUB_ENDPOINT || process.env.AZURE_AI_ENDPOINT || 'https://models.inference.ai.azure.com',
tokenLimit: 128000,
costPerToken: 0.000015
};
this.addProvider(githubAI);
// Initialize default quotas
this.setDefaultQuotas();
}
setDefaultQuotas() {
const quotaConfigs = [
['google-ai', {
dailyTokenLimit: 100000000, // 100M tokens
requestsPerMinute: 60
}], ['azure-openai', {
dailyTokenLimit: 300000000, // 300M tokens
costLimit: 500, // $500 per day
requestsPerMinute: 150
}],
['azure-ai-studio', {
dailyTokenLimit: 300000000, // 300M tokens
costLimit: 500, // $500 per day
requestsPerMinute: 150
}],
['github-ai', {
dailyTokenLimit: 50000000, // 50M tokens
requestsPerMinute: 30
}],
['ollama', {
dailyTokenLimit: Infinity,
requestsPerMinute: 20
}]
];
quotaConfigs.forEach(([name, config]) => {
this.quotas.set(name, config);
});
}
initializeMetrics(providerName) {
this.metrics.set(providerName, {
totalRequests: 0,
successfulRequests: 0,
failedRequests: 0,
averageResponseTime: 0,
lastError: null,
lastErrorTime: null,
tokenUsage: {
total: 0,
average: 0
}
});
}
addProvider(config) {
this.providers.set(config.name, config);
this.initializeMetrics(config.name);
}
async validateConfigurations() {
const results = new Map();
const validProviders = [];
for (const [name, provider] of this.providers) {
try {
const isValid = await provider.check();
results.set(name, isValid);
if (isValid) {
validProviders.push({ name, priority: provider.priority });
}
}
catch (error) {
console.error(`Error validating ${name}:`, error instanceof Error ? error.message : String(error));
results.set(name, false);
}
}
// Sort by priority (lower number = higher priority)
validProviders.sort((a, b) => a.priority - b.priority);
// Set active provider to highest priority valid provider
if (validProviders.length > 0) {
this.activeProvider = validProviders[0].name;
// Set fallback queue to remaining providers in priority order
this.fallbackQueue = validProviders.slice(1).map(p => p.name);
}
return results;
}
getActiveProvider() {
return this.activeProvider;
}
async switchProvider() {
if (!this.fallbackQueue.length) {
console.error('No fallback providers available');
return false;
}
while (this.fallbackQueue.length > 0) {
const nextProvider = this.fallbackQueue.shift();
if (!nextProvider)
continue;
const provider = this.providers.get(nextProvider);
if (!provider)
continue;
try {
const isValid = await provider.check();
if (isValid) {
this.activeProvider = nextProvider;
console.log(`Switched to provider: ${nextProvider}`);
return true;
}
}
catch (error) {
console.warn(`Failed to switch to ${nextProvider}:`, error instanceof Error ? error.message : String(error));
}
}
return false;
}
updateMetrics(providerName, success, responseTime, tokenCount, error) {
const metrics = this.metrics.get(providerName);
if (!metrics)
return;
metrics.totalRequests++;
if (success) {
metrics.successfulRequests++;
metrics.tokenUsage.total += tokenCount;
metrics.tokenUsage.average = metrics.tokenUsage.total / metrics.successfulRequests;
}
else {
metrics.failedRequests++;
metrics.lastError = error?.message || 'Unknown error';
metrics.lastErrorTime = new Date();
}
metrics.averageResponseTime =
(metrics.averageResponseTime * (metrics.totalRequests - 1) + responseTime) / metrics.totalRequests;
}
getProviderMetrics(providerName) {
if (providerName) {
return this.metrics.get(providerName) || null;
}
return this.metrics;
}
getProviderConfig(providerName) {
return this.providers.get(providerName) || null;
}
async validateProvider(providerName) {
const provider = this.providers.get(providerName);
if (!provider)
return false;
return provider.check();
}
getTokenLimit(providerName) {
const provider = this.providers.get(providerName || this.activeProvider || '');
return provider?.tokenLimit || 4000;
}
async checkRateLimit(providerName) {
const quota = this.quotas.get(providerName);
if (!quota?.requestsPerMinute)
return true;
const now = Date.now();
const limiter = this.rateLimiters.get(providerName) || { lastRequest: 0, requestCount: 0 };
// Reset counter if more than a minute has passed
if (now - limiter.lastRequest > 60000) {
limiter.requestCount = 0;
}
// Check if under rate limit
if (limiter.requestCount >= quota.requestsPerMinute) {
return false;
}
// Update rate limiter
limiter.requestCount++;
limiter.lastRequest = now;
this.rateLimiters.set(providerName, limiter);
return true;
}
getDailyTokenUsage(providerName) {
return this.metrics.get(providerName)?.tokenUsage.total || 0;
}
getDailyCost(providerName) {
const provider = this.providers.get(providerName);
const metrics = this.metrics.get(providerName);
if (!provider?.costPerToken || !metrics)
return 0;
return metrics.tokenUsage.total * provider.costPerToken;
}
checkQuota(providerName) {
const quota = this.quotas.get(providerName);
if (!quota)
return true;
const tokenUsage = this.getDailyTokenUsage(providerName);
const cost = this.getDailyCost(providerName);
return (tokenUsage < quota.dailyTokenLimit &&
(!quota.costLimit || cost < quota.costLimit));
}
getProviderStatus(providerName) {
const provider = this.providers.get(providerName);
const quota = this.quotas.get(providerName);
if (!provider) {
return {
available: false,
withinRateLimit: false,
withinQuota: false
};
}
const tokenUsage = this.getDailyTokenUsage(providerName);
const cost = this.getDailyCost(providerName);
return {
available: true,
withinRateLimit: true, // Rate limiting is checked per-request
withinQuota: this.checkQuota(providerName),
quotaRemaining: quota ? quota.dailyTokenLimit - tokenUsage : undefined,
estimatedCost: provider.costPerToken ? cost : undefined
};
}
}
//# sourceMappingURL=ProviderManager.js.map