tasq/node_modules/agentic-flow/dist/router/router.js

350 lines
13 KiB
JavaScript

// Multi-model router core implementation
import { readFileSync, existsSync } from 'fs';
import { homedir } from 'os';
import { join } from 'path';
import { OpenRouterProvider } from './providers/openrouter.js';
import { AnthropicProvider } from './providers/anthropic.js';
import { ONNXLocalProvider } from './providers/onnx-local.js';
import { GeminiProvider } from './providers/gemini.js';
export class ModelRouter {
config;
providers = new Map();
metrics;
constructor(configPath) {
this.config = this.loadConfig(configPath);
this.initializeProviders();
this.metrics = this.initializeMetrics();
}
loadConfig(configPath) {
const paths = [
configPath,
process.env.AGENTIC_FLOW_ROUTER_CONFIG,
join(homedir(), '.agentic-flow', 'router.config.json'),
join(process.cwd(), 'router.config.json'),
join(process.cwd(), 'config', 'router.config.json'),
join(process.cwd(), 'router.config.example.json')
].filter(Boolean);
for (const path of paths) {
if (existsSync(path)) {
const content = readFileSync(path, 'utf-8');
const config = JSON.parse(content);
// Substitute environment variables
return this.substituteEnvVars(config);
}
}
// If no config file found, create config from environment variables
return this.createConfigFromEnv();
}
createConfigFromEnv() {
// Create minimal config from environment variables
const config = {
version: '1.0',
defaultProvider: process.env.PROVIDER || 'anthropic',
routing: { mode: 'manual' },
providers: {}
};
// Add Anthropic if API key exists
if (process.env.ANTHROPIC_API_KEY) {
config.providers.anthropic = {
apiKey: process.env.ANTHROPIC_API_KEY,
baseUrl: process.env.ANTHROPIC_BASE_URL
};
}
// Add OpenRouter if API key exists
if (process.env.OPENROUTER_API_KEY) {
config.providers.openrouter = {
apiKey: process.env.OPENROUTER_API_KEY,
baseUrl: process.env.OPENROUTER_BASE_URL
};
}
// Add Gemini if API key exists
if (process.env.GOOGLE_GEMINI_API_KEY) {
config.providers.gemini = {
apiKey: process.env.GOOGLE_GEMINI_API_KEY
};
}
// ONNX is always available (no API key needed)
config.providers.onnx = {
modelPath: process.env.ONNX_MODEL_PATH,
executionProviders: ['cpu']
};
return config;
}
substituteEnvVars(obj) {
if (typeof obj === 'string') {
// Replace ${VAR_NAME} with environment variable value
return obj.replace(/\$\{([^}]+)\}/g, (_, key) => {
const [varName, defaultValue] = key.split(':-');
return process.env[varName] || defaultValue || '';
});
}
if (Array.isArray(obj)) {
return obj.map(item => this.substituteEnvVars(item));
}
if (obj && typeof obj === 'object') {
const result = {};
for (const [key, value] of Object.entries(obj)) {
result[key] = this.substituteEnvVars(value);
}
return result;
}
return obj;
}
initializeProviders() {
const verbose = process.env.ROUTER_VERBOSE === 'true';
// Initialize Anthropic
if (this.config.providers.anthropic) {
try {
const provider = new AnthropicProvider(this.config.providers.anthropic);
this.providers.set('anthropic', provider);
if (verbose)
console.log('✅ Anthropic provider initialized');
}
catch (error) {
if (verbose)
console.error('❌ Failed to initialize Anthropic:', error);
}
}
// Initialize OpenRouter
if (this.config.providers.openrouter) {
try {
const provider = new OpenRouterProvider(this.config.providers.openrouter);
this.providers.set('openrouter', provider);
if (verbose)
console.log('✅ OpenRouter provider initialized');
}
catch (error) {
if (verbose)
console.error('❌ Failed to initialize OpenRouter:', error);
}
}
// Initialize ONNX Local
if (this.config.providers.onnx) {
try {
const provider = new ONNXLocalProvider({
modelPath: this.config.providers.onnx.modelPath || './models/phi-4/cpu_and_mobile/cpu-int4-rtn-block-32-acc-level-4/model.onnx',
executionProviders: this.config.providers.onnx.executionProviders || ['cpu'],
maxTokens: this.config.providers.onnx.maxTokens || 100,
temperature: this.config.providers.onnx.temperature || 0.7
});
this.providers.set('onnx', provider);
if (verbose)
console.log('✅ ONNX Local provider initialized');
}
catch (error) {
if (verbose)
console.error('❌ Failed to initialize ONNX:', error);
}
}
// Initialize Gemini
if (this.config.providers.gemini) {
try {
const provider = new GeminiProvider(this.config.providers.gemini);
this.providers.set('gemini', provider);
if (verbose)
console.log('✅ Gemini provider initialized');
}
catch (error) {
if (verbose)
console.error('❌ Failed to initialize Gemini:', error);
}
}
// TODO: Initialize other providers (OpenAI, Ollama, LiteLLM)
// Will be implemented in Phase 1
}
initializeMetrics() {
return {
totalRequests: 0,
totalCost: 0,
totalTokens: { input: 0, output: 0 },
providerBreakdown: {},
agentBreakdown: {}
};
}
async chat(params, agentType) {
const startTime = Date.now();
const provider = await this.selectProvider(params, agentType);
try {
const response = await provider.chat(params);
// Update metrics
this.updateMetrics(provider.name, response, Date.now() - startTime, agentType);
// Add metadata
response.metadata = {
...response.metadata,
provider: provider.name,
latency: Date.now() - startTime
};
return response;
}
catch (error) {
return this.handleProviderError(error, params, agentType);
}
}
async *stream(params, agentType) {
const provider = await this.selectProvider(params, agentType);
if (!provider.stream) {
throw new Error(`Provider ${provider.name} does not support streaming`);
}
try {
const iterator = provider.stream(params);
for await (const chunk of iterator) {
yield chunk;
}
}
catch (error) {
throw error;
}
}
async selectProvider(params, agentType) {
// If provider is explicitly specified in params, use it
if (params.provider) {
const forcedProvider = this.providers.get(params.provider);
if (forcedProvider) {
return forcedProvider;
}
console.warn(`⚠️ Requested provider '${params.provider}' not available, falling back to routing logic`);
}
const routingMode = this.config.routing?.mode || 'manual';
switch (routingMode) {
case 'manual':
return this.getDefaultProvider();
case 'rule-based':
return this.selectByRules(params, agentType);
case 'cost-optimized':
return this.selectByCost(params);
case 'performance-optimized':
return this.selectByPerformance(params);
default:
return this.getDefaultProvider();
}
}
getDefaultProvider() {
const provider = this.providers.get(this.config.defaultProvider);
if (!provider) {
throw new Error(`Default provider ${this.config.defaultProvider} not initialized`);
}
return provider;
}
selectByRules(params, agentType) {
const rules = this.config.routing?.rules || [];
for (const rule of rules) {
if (this.matchesRule(rule.condition, params, agentType)) {
const provider = this.providers.get(rule.action.provider);
if (provider) {
console.log(`🎯 Routing via rule: ${rule.reason || 'matched condition'}`);
return provider;
}
}
}
return this.getDefaultProvider();
}
matchesRule(condition, params, agentType) {
if (condition.agentType && agentType) {
if (!condition.agentType.includes(agentType)) {
return false;
}
}
if (condition.requiresTools !== undefined) {
if (condition.requiresTools && (!params.tools || params.tools.length === 0)) {
return false;
}
}
// TODO: Add more condition matching logic
return true;
}
selectByCost(params) {
// For now, prefer cheaper providers
// TODO: Implement actual cost calculation
const providerOrder = ['openrouter', 'anthropic', 'openai'];
for (const providerType of providerOrder) {
const provider = this.providers.get(providerType);
if (provider) {
console.log(`💰 Cost-optimized routing: selected ${provider.name}`);
return provider;
}
}
return this.getDefaultProvider();
}
selectByPerformance(params) {
// For now, use metrics to select fastest provider
let fastestProvider = null;
let lowestLatency = Infinity;
for (const [providerType, provider] of this.providers) {
const breakdown = this.metrics.providerBreakdown[providerType];
if (breakdown && breakdown.avgLatency < lowestLatency) {
lowestLatency = breakdown.avgLatency;
fastestProvider = provider;
}
}
if (fastestProvider) {
console.log(`⚡ Performance-optimized routing: selected ${fastestProvider.name}`);
return fastestProvider;
}
return this.getDefaultProvider();
}
async handleProviderError(error, params, agentType) {
console.error(`❌ Provider error from ${error.provider}:`, error.message);
// Try fallback chain
const fallbackChain = this.config.fallbackChain || [];
for (const providerType of fallbackChain) {
if (providerType === error.provider)
continue; // Skip failed provider
const provider = this.providers.get(providerType);
if (provider) {
console.log(`🔄 Falling back to ${provider.name}`);
try {
return await provider.chat(params);
}
catch (fallbackError) {
console.error(`❌ Fallback provider ${provider.name} also failed`);
continue;
}
}
}
throw error; // No fallback succeeded
}
updateMetrics(providerName, response, latency, agentType) {
this.metrics.totalRequests++;
if (response.usage) {
this.metrics.totalTokens.input += response.usage.inputTokens;
this.metrics.totalTokens.output += response.usage.outputTokens;
}
if (response.metadata?.cost) {
this.metrics.totalCost += response.metadata.cost;
}
// Provider breakdown
if (!this.metrics.providerBreakdown[providerName]) {
this.metrics.providerBreakdown[providerName] = {
requests: 0,
cost: 0,
avgLatency: 0,
errors: 0
};
}
const breakdown = this.metrics.providerBreakdown[providerName];
breakdown.requests++;
breakdown.cost += response.metadata?.cost || 0;
breakdown.avgLatency = (breakdown.avgLatency * (breakdown.requests - 1) + latency) / breakdown.requests;
// Agent breakdown
if (agentType) {
if (!this.metrics.agentBreakdown) {
this.metrics.agentBreakdown = {};
}
if (!this.metrics.agentBreakdown[agentType]) {
this.metrics.agentBreakdown[agentType] = { requests: 0, cost: 0 };
}
this.metrics.agentBreakdown[agentType].requests++;
this.metrics.agentBreakdown[agentType].cost += response.metadata?.cost || 0;
}
}
getMetrics() {
return { ...this.metrics };
}
getConfig() {
return { ...this.config };
}
getProviders() {
return new Map(this.providers);
}
}
//# sourceMappingURL=router.js.map