recoder-shared
Version:
Shared types, utilities, and configurations for Recoder
529 lines • 21.7 kB
JavaScript
"use strict";
/**
* Unified AI Provider Router for Cross-Platform Integration
* Manages AI providers with health monitoring, cost tracking, and intelligent routing
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.AIProviderRouter = void 0;
const tslib_1 = require("tslib");
const events_1 = require("events");
const axios_1 = tslib_1.__importDefault(require("axios"));
class AIProviderRouter extends events_1.EventEmitter {
constructor(config = {}) {
super();
this.config = config;
this.providers = new Map();
this.healthCheckInterval = null;
this.usageTracking = new Map();
this.requestQueue = new Map();
this.routingStrategy = { name: 'quality-optimized', config: {} };
this.config = {
baseURL: 'http://localhost:3001',
healthCheckInterval: 30000, // 30 seconds
enableCostTracking: true,
enableHealthMonitoring: true,
...config
};
if (this.config.routingStrategy) {
this.routingStrategy = this.config.routingStrategy;
}
this.api = axios_1.default.create({
baseURL: `${this.config.baseURL}/api`,
timeout: 10000
});
this.initializeDefaultProviders();
this.startHealthMonitoring();
this.setupEventHandlers();
}
initializeDefaultProviders() {
// Core AI providers with platform-specific configurations
const defaultProviders = [
{
id: 'anthropic-claude',
name: 'Anthropic Claude',
type: 'anthropic',
baseURL: 'https://api.anthropic.com',
models: [
{
id: 'claude-3-5-sonnet-20241022',
name: 'Claude 3.5 Sonnet',
contextLength: 200000,
inputCostPer1k: 3.00,
outputCostPer1k: 15.00,
capabilities: ['text-generation', 'code-generation', 'function-calling'],
supportedPlatforms: ['cli', 'web', 'mobile', 'desktop', 'extension']
}
],
capabilities: [
{ type: 'text-generation', supported: true },
{ type: 'code-generation', supported: true },
{ type: 'function-calling', supported: true },
{ type: 'streaming', supported: true }
]
},
{
id: 'groq-llama',
name: 'Groq LLaMA',
type: 'groq',
baseURL: 'https://api.groq.com/openai/v1',
models: [
{
id: 'llama3-groq-70b-8192-tool-use-preview',
name: 'LLaMA 3 70B Tool Use',
contextLength: 8192,
inputCostPer1k: 0.89,
outputCostPer1k: 0.89,
capabilities: ['text-generation', 'code-generation', 'function-calling'],
supportedPlatforms: ['cli', 'web', 'mobile', 'desktop', 'extension']
}
],
capabilities: [
{ type: 'text-generation', supported: true },
{ type: 'code-generation', supported: true },
{ type: 'function-calling', supported: true },
{ type: 'streaming', supported: true }
]
},
{
id: 'google-gemini',
name: 'Google Gemini',
type: 'gemini',
baseURL: 'https://generativelanguage.googleapis.com',
models: [
{
id: 'gemini-1.5-pro-latest',
name: 'Gemini 1.5 Pro',
contextLength: 2000000,
inputCostPer1k: 1.25,
outputCostPer1k: 5.00,
capabilities: ['text-generation', 'code-generation', 'image-analysis'],
supportedPlatforms: ['cli', 'web', 'mobile', 'desktop', 'extension']
}
],
capabilities: [
{ type: 'text-generation', supported: true },
{ type: 'code-generation', supported: true },
{ type: 'image-analysis', supported: true },
{ type: 'streaming', supported: true }
]
},
{
id: 'ollama-local',
name: 'Ollama (Local)',
type: 'ollama',
baseURL: 'http://localhost:11434',
models: [
{
id: 'codellama:13b',
name: 'Code Llama 13B',
contextLength: 16384,
inputCostPer1k: 0,
outputCostPer1k: 0,
capabilities: ['text-generation', 'code-generation'],
supportedPlatforms: ['cli', 'desktop']
}
],
capabilities: [
{ type: 'text-generation', supported: true },
{ type: 'code-generation', supported: true },
{ type: 'streaming', supported: true }
]
}
];
for (const provider of defaultProviders) {
this.addProvider(this.createProviderWithDefaults(provider));
}
}
createProviderWithDefaults(partial) {
return {
id: partial.id,
name: partial.name,
type: partial.type,
baseURL: partial.baseURL,
apiKey: partial.apiKey,
models: partial.models || [],
capabilities: partial.capabilities || [],
config: {
timeout: 30000,
retries: 3,
rateLimit: {
requestsPerMinute: 60,
tokensPerMinute: 100000
},
healthCheck: {
enabled: true,
interval: 30000,
timeout: 5000
},
...partial.config
},
status: {
status: 'healthy',
lastChecked: new Date().toISOString(),
responseTime: 0,
errorRate: 0,
uptime: 100,
errors: []
},
costs: {
totalTokens: 0,
totalCost: 0,
inputTokens: 0,
outputTokens: 0,
requestCount: 0,
lastReset: new Date().toISOString()
}
};
}
// Provider Management
addProvider(provider) {
this.providers.set(provider.id, provider);
this.usageTracking.set(provider.id, provider.costs);
this.requestQueue.set(provider.id, []);
this.emit('providerAdded', provider);
console.log(`Added AI provider: ${provider.name} (${provider.id})`);
}
removeProvider(providerId) {
const provider = this.providers.get(providerId);
if (!provider)
return false;
this.providers.delete(providerId);
this.usageTracking.delete(providerId);
this.requestQueue.delete(providerId);
this.emit('providerRemoved', provider);
return true;
}
getProvider(providerId) {
return this.providers.get(providerId);
}
getAllProviders() {
return Array.from(this.providers.values());
}
getHealthyProviders() {
return this.getAllProviders().filter(p => p.status.status === 'healthy');
}
// Model Management
getAvailableModels(platform) {
const models = [];
for (const provider of this.providers.values()) {
for (const model of provider.models) {
if (!platform || model.supportedPlatforms.includes(platform)) {
models.push(model);
}
}
}
return models;
}
getBestModelFor(task, platform, constraints) {
const healthyProviders = this.getHealthyProviders();
let bestMatch = null;
for (const provider of healthyProviders) {
for (const model of provider.models) {
if (!model.supportedPlatforms.includes(platform))
continue;
// Check constraints
if (constraints) {
if (constraints.maxCost && model.outputCostPer1k && model.outputCostPer1k > constraints.maxCost)
continue;
if (constraints.maxResponseTime && provider.status.responseTime > constraints.maxResponseTime)
continue;
if (constraints.requiresCapability) {
const hasAllCapabilities = constraints.requiresCapability.every(cap => model.capabilities.includes(cap));
if (!hasAllCapabilities)
continue;
}
}
// Calculate score based on routing strategy
const score = this.calculateModelScore(provider, model, task);
if (!bestMatch || score > bestMatch.score) {
bestMatch = { provider, model, score };
}
}
}
return bestMatch ? { provider: bestMatch.provider, model: bestMatch.model } : null;
}
calculateModelScore(provider, model, task) {
let score = 0;
switch (this.routingStrategy.name) {
case 'cost-optimized':
score = 1000 - (model.outputCostPer1k || 0);
break;
case 'speed-optimized':
score = 1000 - provider.status.responseTime;
break;
case 'quality-optimized':
// Prefer Claude for complex tasks, Groq for speed, Gemini for multimodal
if (provider.type === 'anthropic' && (task.includes('complex') || task.includes('reasoning'))) {
score += 500;
}
else if (provider.type === 'groq' && task.includes('fast')) {
score += 400;
}
else if (provider.type === 'gemini' && task.includes('image')) {
score += 450;
}
score += model.contextLength / 1000; // Prefer larger context
break;
case 'least-loaded':
const queue = this.requestQueue.get(provider.id) || [];
score = 1000 - queue.length;
break;
case 'round-robin':
score = Math.random() * 1000; // Random for round-robin effect
break;
}
// Apply provider health multiplier
const healthMultiplier = provider.status.status === 'healthy' ? 1.0 :
provider.status.status === 'degraded' ? 0.7 : 0.0;
score *= healthMultiplier;
return score;
}
// Request Routing
async routeRequest(request) {
const match = this.getBestModelFor(request.options.systemPrompt || 'general', request.platform, {
requiresCapability: request.options.functions ? ['function-calling'] : undefined
});
if (!match) {
throw new Error('No suitable AI provider available');
}
const { provider, model } = match;
// Add to queue
const queue = this.requestQueue.get(provider.id) || [];
queue.push(request);
this.requestQueue.set(provider.id, queue);
try {
const response = await this.executeRequest(provider, model, request);
// Update usage tracking
if (this.config.enableCostTracking) {
this.updateUsageTracking(provider.id, response.usage);
}
this.emit('requestCompleted', { provider, model, request, response });
return response;
}
finally {
// Remove from queue
const updatedQueue = queue.filter(r => r.id !== request.id);
this.requestQueue.set(provider.id, updatedQueue);
}
}
async executeRequest(provider, model, request) {
const startTime = Date.now();
const requestId = request.id;
try {
// This would normally make the actual API call to the provider
// For now, simulate the response structure
const simulatedResponse = {
id: requestId,
provider: provider.id,
model: model.id,
content: `Simulated response from ${provider.name} ${model.name}`,
usage: {
inputTokens: Math.floor(Math.random() * 1000) + 500,
outputTokens: Math.floor(Math.random() * 500) + 100,
totalTokens: 0,
cost: 0
},
metadata: {
responseTime: Date.now() - startTime,
timestamp: new Date().toISOString(),
cached: false
}
};
simulatedResponse.usage.totalTokens = simulatedResponse.usage.inputTokens + simulatedResponse.usage.outputTokens;
simulatedResponse.usage.cost = this.calculateCost(model, simulatedResponse.usage);
return simulatedResponse;
}
catch (error) {
this.handleProviderError(provider.id, {
timestamp: new Date().toISOString(),
type: 'api_error',
message: error.message,
context: { request: request.id }
});
throw error;
}
}
calculateCost(model, usage) {
const inputCost = (usage.inputTokens / 1000) * (model.inputCostPer1k || 0);
const outputCost = (usage.outputTokens / 1000) * (model.outputCostPer1k || 0);
return inputCost + outputCost;
}
// Health Monitoring
startHealthMonitoring() {
if (!this.config.enableHealthMonitoring || this.healthCheckInterval)
return;
this.healthCheckInterval = setInterval(async () => {
await this.performHealthChecks();
}, this.config.healthCheckInterval);
console.log(`Started AI provider health monitoring (${this.config.healthCheckInterval}ms interval)`);
}
async performHealthChecks() {
const providers = Array.from(this.providers.values());
const healthCheckPromises = providers.map(async (provider) => {
if (!provider.config.healthCheck.enabled)
return;
try {
const startTime = Date.now();
const isHealthy = await this.checkProviderHealth(provider);
const responseTime = Date.now() - startTime;
const newStatus = {
...provider.status,
status: isHealthy ? 'healthy' : 'error',
lastChecked: new Date().toISOString(),
responseTime,
uptime: isHealthy ? Math.min(provider.status.uptime + 1, 100) : Math.max(provider.status.uptime - 5, 0)
};
provider.status = newStatus;
this.emit('providerHealthUpdated', { providerId: provider.id, status: newStatus });
}
catch (error) {
this.handleProviderError(provider.id, {
timestamp: new Date().toISOString(),
type: 'network_error',
message: error.message
});
}
});
await Promise.allSettled(healthCheckPromises);
}
async checkProviderHealth(provider) {
try {
// Simple ping test - in real implementation, this would call the provider's health endpoint
const response = await axios_1.default.get(`${provider.baseURL}/health`, {
timeout: provider.config.healthCheck.timeout,
headers: provider.apiKey ? { 'Authorization': `Bearer ${provider.apiKey}` } : {}
});
return response.status === 200;
}
catch (error) {
// Most providers don't have health endpoints, so we simulate health based on recent usage
return provider.status.errorRate < 50; // Healthy if error rate is below 50%
}
}
handleProviderError(providerId, error) {
const provider = this.providers.get(providerId);
if (!provider)
return;
provider.status.errors.push(error);
// Keep only last 10 errors
if (provider.status.errors.length > 10) {
provider.status.errors = provider.status.errors.slice(-10);
}
// Calculate error rate
const recentErrors = provider.status.errors.filter(e => Date.now() - new Date(e.timestamp).getTime() < 300000 // Last 5 minutes
);
provider.status.errorRate = (recentErrors.length / 10) * 100; // Rough calculation
// Update status based on error rate
if (provider.status.errorRate > 75) {
provider.status.status = 'offline';
}
else if (provider.status.errorRate > 25) {
provider.status.status = 'degraded';
}
this.emit('providerError', { providerId, error });
}
// Usage Tracking
updateUsageTracking(providerId, usage) {
const costs = this.usageTracking.get(providerId);
if (!costs)
return;
costs.inputTokens += usage.inputTokens;
costs.outputTokens += usage.outputTokens;
costs.totalTokens += usage.inputTokens + usage.outputTokens;
costs.totalCost += usage.cost;
costs.requestCount += 1;
this.usageTracking.set(providerId, costs);
// Update provider costs
const provider = this.providers.get(providerId);
if (provider) {
provider.costs = { ...costs };
}
}
// Analytics and Reporting
getProviderAnalytics(timeframe = 'day') {
const providers = Array.from(this.providers.values());
return {
summary: {
totalProviders: providers.length,
healthyProviders: providers.filter(p => p.status.status === 'healthy').length,
totalRequests: providers.reduce((sum, p) => sum + p.costs.requestCount, 0),
totalCost: providers.reduce((sum, p) => sum + p.costs.totalCost, 0),
totalTokens: providers.reduce((sum, p) => sum + p.costs.totalTokens, 0)
},
providers: providers.map(p => ({
id: p.id,
name: p.name,
status: p.status.status,
responseTime: p.status.responseTime,
uptime: p.status.uptime,
costs: p.costs,
queueLength: this.requestQueue.get(p.id)?.length || 0
})),
timestamp: new Date().toISOString()
};
}
getRecommendation(task, priority = 'quality') {
// Set routing strategy based on priority
const oldStrategy = this.routingStrategy;
this.routingStrategy = {
name: priority === 'speed' ? 'speed-optimized' :
priority === 'cost' ? 'cost-optimized' : 'quality-optimized',
config: {}
};
const match = this.getBestModelFor(task, 'web'); // Use web as default platform
// Restore original strategy
this.routingStrategy = oldStrategy;
if (!match)
return null;
let reason = `Best ${priority} option for ${task}`;
if (priority === 'quality' && match.provider.type === 'anthropic') {
reason = 'Claude excels at complex reasoning and code generation';
}
else if (priority === 'speed' && match.provider.type === 'groq') {
reason = 'Groq provides fastest inference with high quality results';
}
else if (task === 'multimodal' && match.provider.type === 'gemini') {
reason = 'Gemini offers superior multimodal capabilities';
}
return {
provider: match.provider,
model: match.model,
reason
};
}
// Cleanup
destroy() {
if (this.healthCheckInterval) {
clearInterval(this.healthCheckInterval);
this.healthCheckInterval = null;
}
this.providers.clear();
this.usageTracking.clear();
this.requestQueue.clear();
this.removeAllListeners();
}
// Event handlers setup
setupEventHandlers() {
this.on('providerError', ({ providerId, error }) => {
console.error(`Provider ${providerId} error:`, error.message);
});
this.on('providerHealthUpdated', ({ providerId, status }) => {
if (status.status !== 'healthy') {
console.warn(`Provider ${providerId} status: ${status.status}`);
}
});
}
// Getters for status
get isHealthy() {
return this.getHealthyProviders().length > 0;
}
get totalProviders() {
return this.providers.size;
}
get routingMode() {
return this.routingStrategy.name;
}
}
exports.AIProviderRouter = AIProviderRouter;
exports.default = AIProviderRouter;
//# sourceMappingURL=ai-provider-router.js.map