route-claudecode
Version:
Advanced routing and transformation system for Claude Code outputs to multiple AI providers
908 lines • 40.1 kB
JavaScript
"use strict";
/**
* Intelligent Routing Engine
* Routes requests to appropriate providers based on model, content, and configuration
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.RoutingEngine = void 0;
const logger_1 = require("@/utils/logger");
const tokenizer_1 = require("@/utils/tokenizer");
const response_stats_1 = require("@/utils/response-stats");
const simple_provider_manager_1 = require("./simple-provider-manager");
class RoutingEngine {
routingConfig;
providerHealth = new Map();
simpleProviderManager;
// 临时禁用的providers - 非持久化存储
temporarilyDisabledProviders = new Set();
constructor(routingConfig) {
this.routingConfig = routingConfig;
// Initialize simple provider manager for round-robin and blacklisting
this.simpleProviderManager = new simple_provider_manager_1.SimpleProviderManager();
logger_1.logger.info('Routing engine initialized with simplified provider management', {
categories: Object.keys(routingConfig),
hasBackup: Object.values(routingConfig).some(config => config.backup && config.backup.length > 0),
hasMultiProvider: Object.values(routingConfig).some(config => config.providers && config.providers.length > 0)
});
// Initialize provider health tracking
this.initializeProviderHealth();
}
/**
* Route a request to the appropriate provider
*/
async route(request, requestId) {
try {
logger_1.logger.trace(requestId, 'routing', 'Starting request routing', {
model: request.model,
messageCount: request.messages.length
});
// Step 1: Determine routing category based on request characteristics
const originalCategory = this.determineCategory(request);
logger_1.logger.debug(`Determined routing category: ${originalCategory}`, { requestId }, requestId, 'routing');
// Step 2: Get provider and model from category configuration with fallback to default
let categoryRule = this.routingConfig[originalCategory];
let category = originalCategory;
if (!categoryRule) {
logger_1.logger.warn(`No routing configuration found for category: ${originalCategory}, falling back to default`, {
requestId,
originalCategory,
fallbackCategory: 'default'
});
// Fallback to default category
categoryRule = this.routingConfig['default'];
if (!categoryRule) {
throw new Error(`No routing configuration found for category: ${originalCategory} and no default configuration available`);
}
// Update the category to default for downstream processing
category = 'default';
}
// Step 3: Select provider with advanced routing (backup + multi-provider + failover)
const selectedProvider = await this.selectProviderWithBackup(categoryRule, category, requestId);
// Step 4: Apply model mapping and return provider
this.applyModelMapping(request, selectedProvider.provider, selectedProvider.model, category);
logger_1.logger.debug(`Routing ${category} to ${selectedProvider.provider}`, {
category,
provider: selectedProvider.provider,
targetModel: selectedProvider.model,
originalModel: request.model,
backupAvailable: categoryRule.backup ? categoryRule.backup.length : 0
}, requestId, 'routing');
return selectedProvider.provider;
}
catch (error) {
logger_1.logger.error('Error during routing', error, requestId, 'routing');
throw error;
}
}
/**
* Select provider with multi-provider and backup support
*/
async selectProviderWithBackup(categoryRule, category, requestId) {
// Check if using new multi-provider format
if (categoryRule.providers && categoryRule.providers.length > 0) {
return this.selectFromMultiProvider(categoryRule, category, requestId);
}
// Fallback to legacy single provider + backup format
return this.selectFromLegacyBackup(categoryRule, category, requestId);
}
/**
* Select from multi-provider configuration with weighted selection and intelligent blacklisting
*/
async selectFromMultiProvider(categoryRule, category, requestId) {
const providers = categoryRule.providers;
logger_1.logger.debug(`Enhanced weighted multi-provider selection for ${category}`, {
providerCount: providers.length,
allProviders: providers.map(p => ({ provider: p.provider, model: p.model, weight: p.weight || 1 }))
}, requestId, 'routing');
// Convert to weighted providers (default weight = 1 if not specified)
const weightedProviders = providers.map(p => ({
providerId: p.provider,
model: p.model,
weight: p.weight || 1
}));
// Use enhanced weighted selection with dynamic weight redistribution
const selectedProvider = this.simpleProviderManager.selectProviderWeighted(weightedProviders, category);
if (!selectedProvider) {
logger_1.logger.error('No weighted providers available (all blacklisted)', {
category,
providers: weightedProviders,
blacklistStatus: this.simpleProviderManager.getBlacklistStatus()
}, requestId, 'routing');
throw new Error(`No providers available for category: ${category}`);
}
logger_1.logger.info(`🎯 Weighted provider selected: ${selectedProvider.providerId}`, {
category,
selectedProvider: selectedProvider.providerId,
selectedModel: selectedProvider.model,
selectedWeight: selectedProvider.weight,
totalProviders: providers.length,
selectionMethod: 'weighted-with-redistribution'
}, requestId, 'routing');
return {
provider: selectedProvider.providerId,
model: selectedProvider.model || selectedProvider.providerId
};
}
/**
* Select from legacy single provider + backup configuration using weighted selection
*/
async selectFromLegacyBackup(categoryRule, category, requestId) {
// Build list of providers (primary + backups) with weights
const providers = [];
// Add primary provider (weight = 1 by default)
if (categoryRule.provider && categoryRule.model) {
providers.push({
provider: categoryRule.provider,
model: categoryRule.model,
weight: 1
});
}
// Add backup providers (check if they have weights, default to 1)
if (categoryRule.backup && categoryRule.backup.length > 0) {
providers.push(...categoryRule.backup.map(backup => ({
provider: backup.provider,
model: backup.model,
weight: backup.weight || 1
})));
}
if (providers.length === 0) {
throw new Error(`No providers configured for category: ${category}`);
}
// Convert to weighted providers for enhanced selection
const weightedProviders = providers.map(p => ({
providerId: p.provider,
model: p.model,
weight: p.weight || 1
}));
// Use weighted selection if multiple providers, otherwise use simple selection
if (weightedProviders.length > 1) {
const selectedProvider = this.simpleProviderManager.selectProviderWeighted(weightedProviders, category);
if (selectedProvider) {
logger_1.logger.debug(`🎯 Legacy weighted provider selection: ${selectedProvider.providerId}`, {
category,
selectedProvider: selectedProvider.providerId,
selectedModel: selectedProvider.model,
selectedWeight: selectedProvider.weight,
totalProviders: providers.length,
isBackupProvider: selectedProvider.providerId !== categoryRule.provider,
selectionMethod: 'legacy-weighted'
}, requestId, 'routing');
return {
provider: selectedProvider.providerId,
model: selectedProvider.model || selectedProvider.providerId
};
}
}
// Fallback to simple round-robin for legacy compatibility
const providerIds = providers.map(p => p.provider);
let selectedProviderId = null;
let selectedModel = null;
for (let i = 0; i < providerIds.length; i++) {
const candidateProviderId = this.simpleProviderManager.selectProvider(providerIds, category);
if (!candidateProviderId)
break;
const candidateEntry = providers.find(p => p.provider === candidateProviderId);
if (candidateEntry) {
// Check if this provider+model combination is blacklisted
if (!this.simpleProviderManager.isBlacklisted(candidateProviderId, candidateEntry.model)) {
selectedProviderId = candidateProviderId;
selectedModel = candidateEntry.model;
break;
}
}
}
if (!selectedProviderId || !selectedModel) {
logger_1.logger.error('No providers available in legacy config (all blacklisted)', {
category,
providerIds,
blacklistStatus: this.simpleProviderManager.getBlacklistStatus()
}, requestId, 'routing');
// Fallback to first provider
return providers[0];
}
// Find the corresponding provider entry
const selectedProvider = providers.find(p => p.provider === selectedProviderId);
if (!selectedProvider) {
logger_1.logger.error('Selected provider not found in legacy configuration', {
selectedProviderId,
availableProviders: providers.map(p => p.provider)
}, requestId, 'routing');
return providers[0];
}
logger_1.logger.debug(`Legacy round-robin provider selection: ${selectedProviderId}`, {
category,
model: selectedModel,
totalProviders: providers.length,
isBackupProvider: selectedProviderId !== categoryRule.provider,
selectionMethod: 'legacy-round-robin'
}, requestId, 'routing');
return {
provider: selectedProviderId,
model: selectedModel
};
}
/**
* Get provider success rate for health-based selection
*/
getProviderSuccessRate(providerId) {
const health = this.providerHealth.get(providerId);
if (!health || health.totalRequests === 0) {
return 1.0; // Assume 100% for new providers
}
return health.successCount / health.totalRequests;
}
/**
* Check if provider is healthy using simple provider manager
* Now supports model-specific blacklisting
*/
isProviderHealthy(providerId, model) {
// 🚫 临时禁用检查 - 最高优先级
if (this.temporarilyDisabledProviders.has(providerId)) {
logger_1.logger.debug(`Provider ${providerId} is temporarily disabled via user control`);
return false;
}
// Use simple provider manager blacklist check (now supports model-specific)
if (this.simpleProviderManager.isBlacklisted(providerId, model)) {
logger_1.logger.debug(`Provider ${providerId} is blacklisted by simple provider manager`, {
model: model || 'all-models',
scope: model ? 'model-specific' : 'provider-wide'
});
return false;
}
// Keep legacy health check for compatibility
const health = this.providerHealth.get(providerId);
if (!health) {
return true; // Assume healthy for new providers
}
// Simple consecutive errors check
if (health.consecutiveErrors >= 5) {
return false;
}
return health.isHealthy;
}
/**
* Initialize provider health tracking
*/
initializeProviderHealth() {
const allProviders = new Set();
// Collect all provider IDs from configuration
Object.values(this.routingConfig).forEach(config => {
if (config.provider) {
allProviders.add(config.provider);
}
if (config.backup) {
config.backup.forEach(backup => allProviders.add(backup.provider));
}
if (config.providers) {
config.providers.forEach(provider => allProviders.add(provider.provider));
}
});
// Initialize health tracking for all providers
allProviders.forEach(providerId => {
this.providerHealth.set(providerId, {
providerId,
isHealthy: true,
consecutiveErrors: 0,
errorHistory: [],
totalRequests: 0,
successCount: 0,
failureCount: 0,
inCooldown: false,
// Enhanced intelligent failover fields
isPermanentlyBlacklisted: false,
temporaryBackoffLevel: 0,
authFailureCount: 0,
networkFailureCount: 0,
gatewayFailureCount: 0,
// 429错误临时黑名单支持
isTemporarilyBlacklisted: false,
rateLimitFailureCount: 0
});
});
logger_1.logger.info(`Initialized health tracking for ${allProviders.size} providers`, {
providers: Array.from(allProviders)
});
}
/**
* Record provider request result for health tracking and statistics with intelligent failover
*/
recordProviderResult(providerId, success, error, httpCode, model, responseTimeMs, isStreaming = false) {
let health = this.providerHealth.get(providerId);
if (!health) {
// Initialize if not exists
health = {
providerId,
isHealthy: true,
consecutiveErrors: 0,
errorHistory: [],
totalRequests: 0,
successCount: 0,
failureCount: 0,
inCooldown: false,
// Enhanced intelligent failover fields
isPermanentlyBlacklisted: false,
temporaryBackoffLevel: 0,
authFailureCount: 0,
networkFailureCount: 0,
gatewayFailureCount: 0,
// 429错误临时黑名单支持
isTemporarilyBlacklisted: false,
rateLimitFailureCount: 0
};
this.providerHealth.set(providerId, health);
}
health.totalRequests++;
if (success) {
health.successCount++;
health.consecutiveErrors = 0;
health.isHealthy = true;
health.lastSuccessTime = new Date();
health.inCooldown = false;
// Reset temporary backoff on successful request
if (health.temporaryBackoffLevel > 0) {
logger_1.logger.info(`Provider ${providerId} recovered - resetting backoff level`, {
previousBackoffLevel: health.temporaryBackoffLevel,
totalRequests: health.totalRequests
});
health.temporaryBackoffLevel = 0;
health.nextRetryTime = undefined;
}
// Report success to simple provider manager for blacklist recovery
this.simpleProviderManager.reportSuccess(providerId, model);
// 记录成功响应统计
if (model) {
response_stats_1.responseStatsManager.recordSuccess(providerId, model, responseTimeMs || 0, isStreaming);
}
logger_1.logger.debug(`Provider ${providerId} request succeeded`, {
totalRequests: health.totalRequests,
successRate: health.successCount / health.totalRequests,
model: model || 'unknown',
responseTime: responseTimeMs ? `${responseTimeMs}ms` : 'unknown'
});
}
else {
health.failureCount++;
health.consecutiveErrors++;
health.lastFailureTime = new Date();
// 🧠 INTELLIGENT FAILURE CATEGORIZATION
const failureCategory = this.categorizeFailure(error, httpCode);
// Add to error history
health.errorHistory.push({
timestamp: new Date(),
errorType: failureCategory,
errorMessage: error || 'No error message provided',
httpCode
});
// Keep only last 10 errors
if (health.errorHistory.length > 10) {
health.errorHistory = health.errorHistory.slice(-10);
}
// Report failure to simple provider manager with model-specific blacklisting
this.simpleProviderManager.reportFailure(providerId, error || 'Unknown error', httpCode, model);
// 🚨 APPLY INTELLIGENT FAILOVER LOGIC
this.applyIntelligentFailover(health, failureCategory, error, httpCode);
// 记录失败响应统计
if (model) {
response_stats_1.responseStatsManager.recordFailure(providerId, model, error || 'unknown', isStreaming);
}
}
}
/**
* 🧠 Intelligent failure categorization
*/
categorizeFailure(error, httpCode) {
const errorLower = (error || '').toLowerCase();
// Rate limit failures (temporary blacklist candidates)
if (httpCode === 429)
return 'rate_limit';
if (errorLower.includes('rate limit') || errorLower.includes('quota') || errorLower.includes('exhausted'))
return 'rate_limit';
if (errorLower.includes('too many requests'))
return 'rate_limit';
// Authentication failures (permanent blacklist candidates)
if (httpCode === 401 || httpCode === 403)
return 'authentication';
if (errorLower.includes('unauthorized') || errorLower.includes('forbidden'))
return 'authentication';
if (errorLower.includes('token') && (errorLower.includes('invalid') || errorLower.includes('expired')))
return 'authentication';
if (errorLower.includes('authentication') || errorLower.includes('auth'))
return 'authentication';
// Network failures (temporary backoff candidates)
if (errorLower.includes('network') || errorLower.includes('connection'))
return 'network';
if (errorLower.includes('timeout') || errorLower.includes('etimedout'))
return 'network';
if (errorLower.includes('econnreset') || errorLower.includes('enotfound'))
return 'network';
if (errorLower.includes('dns') || errorLower.includes('resolve'))
return 'network';
// Gateway failures (temporary backoff candidates)
if (httpCode === 502 || httpCode === 503 || httpCode === 504)
return 'gateway';
if (errorLower.includes('gateway') || errorLower.includes('proxy'))
return 'gateway';
if (errorLower.includes('upstream') || errorLower.includes('bad gateway'))
return 'gateway';
// Other failures
if (httpCode && httpCode >= 500)
return 'server_error';
if (httpCode && httpCode >= 400)
return 'client_error';
return 'unknown';
}
/**
* 🚨 Apply intelligent failover logic based on failure type
*/
applyIntelligentFailover(health, failureCategory, error, httpCode) {
const now = new Date();
switch (failureCategory) {
case 'authentication':
health.authFailureCount++;
health.lastAuthFailure = now;
// 🔒 PERMANENT BLACKLISTING for authentication failures
if (health.authFailureCount >= 3) {
health.isPermanentlyBlacklisted = true;
health.blacklistReason = `Authentication failures: ${health.authFailureCount} consecutive failures. Last error: ${error}`;
health.isHealthy = false;
logger_1.logger.error(`🔒 Provider ${health.providerId} PERMANENTLY BLACKLISTED due to authentication failures`, {
authFailureCount: health.authFailureCount,
lastError: error,
httpCode,
blacklistReason: health.blacklistReason
});
}
else {
logger_1.logger.warn(`⚠️ Authentication failure ${health.authFailureCount}/3 for provider ${health.providerId}`, {
error, httpCode, remainingAttempts: 3 - health.authFailureCount
});
}
break;
case 'rate_limit':
health.rateLimitFailureCount++;
health.lastRateLimitFailure = now;
// 🚫 TEMPORARY BLACKLISTING for 429 rate limit errors
// 根据配置中的blacklistDuration设置临时拉黑时长(默认5分钟)
const blacklistDurationSeconds = 300; // 5 minutes
health.isTemporarilyBlacklisted = true;
health.temporaryBlacklistUntil = new Date(now.getTime() + (blacklistDurationSeconds * 1000));
health.isHealthy = false;
logger_1.logger.warn(`🚫 Provider ${health.providerId} TEMPORARILY BLACKLISTED for rate limit (429)`, {
rateLimitFailures: health.rateLimitFailureCount,
blacklistDurationSeconds,
blacklistUntil: health.temporaryBlacklistUntil.toISOString(),
error, httpCode
});
break;
case 'network':
case 'gateway':
// Update failure counters
if (failureCategory === 'network') {
health.networkFailureCount++;
health.lastNetworkFailure = now;
}
else {
health.gatewayFailureCount++;
health.lastGatewayFailure = now;
}
// 📈 PROGRESSIVE TEMPORARY BACKOFF for network/gateway issues
const failureCount = failureCategory === 'network' ? health.networkFailureCount : health.gatewayFailureCount;
if (failureCount >= 3) {
// Escalate backoff level (max level 3)
health.temporaryBackoffLevel = Math.min(health.temporaryBackoffLevel + 1, 3);
// Calculate backoff duration: 1min → 5min → 10min
const backoffMinutes = health.temporaryBackoffLevel === 1 ? 1 :
health.temporaryBackoffLevel === 2 ? 5 : 10;
health.nextRetryTime = new Date(now.getTime() + (backoffMinutes * 60 * 1000));
health.isHealthy = false;
health.inCooldown = true;
health.cooldownUntil = health.nextRetryTime;
logger_1.logger.warn(`📈 Provider ${health.providerId} entering backoff level ${health.temporaryBackoffLevel}`, {
failureCategory,
failureCount,
backoffMinutes,
nextRetryTime: health.nextRetryTime.toISOString(),
error, httpCode
});
}
break;
default:
// Standard failover logic for other errors
if (health.consecutiveErrors >= 5) {
health.isHealthy = false;
health.inCooldown = true;
health.cooldownUntil = new Date(now.getTime() + 60000); // 1 minute cooldown
logger_1.logger.warn(`Provider ${health.providerId} marked unhealthy after ${health.consecutiveErrors} consecutive errors`, {
errorType: failureCategory,
error, httpCode,
cooldownUntil: health.cooldownUntil
});
}
break;
}
}
/**
* Mark provider as healthy or unhealthy (legacy method)
*/
setProviderHealth(providerId, healthy) {
let health = this.providerHealth.get(providerId);
if (!health) {
health = {
providerId,
isHealthy: healthy,
consecutiveErrors: 0,
errorHistory: [],
totalRequests: 0,
successCount: 0,
failureCount: 0,
inCooldown: false,
// Enhanced intelligent failover support - 添加缺失的属性
isPermanentlyBlacklisted: false,
temporaryBackoffLevel: 0,
authFailureCount: 0,
networkFailureCount: 0,
gatewayFailureCount: 0,
// 429错误临时黑名单支持
isTemporarilyBlacklisted: false,
rateLimitFailureCount: 0
};
this.providerHealth.set(providerId, health);
}
else {
health.isHealthy = healthy;
if (healthy) {
health.consecutiveErrors = 0;
health.inCooldown = false;
}
}
logger_1.logger.info(`Provider health manually updated: ${providerId} = ${healthy ? 'healthy' : 'unhealthy'}`);
}
/**
* Get provider health status
*/
getProviderHealth(providerId) {
return this.providerHealth.get(providerId);
}
/**
* Get all provider health statuses
*/
getAllProviderHealth() {
const result = {};
this.providerHealth.forEach((health, providerId) => {
result[providerId] = health;
});
return result;
}
/**
* Apply failover filtering based on error conditions
*/
applyFailoverFiltering(providers, failoverConfig, requestId) {
const availableProviders = [];
for (const provider of providers) {
const health = this.providerHealth.get(provider.provider);
let shouldExclude = false;
if (health && failoverConfig.triggers.length > 0) {
for (const trigger of failoverConfig.triggers) {
if (this.shouldTriggerFailover(health, trigger)) {
logger_1.logger.warn(`Provider ${provider.provider} excluded by failover trigger`, {
triggerType: trigger.type,
threshold: trigger.threshold,
consecutiveErrors: health.consecutiveErrors,
errorHistory: health.errorHistory.slice(-3) // Last 3 errors
}, requestId, 'routing');
shouldExclude = true;
break;
}
}
}
if (!shouldExclude) {
availableProviders.push(provider);
}
}
logger_1.logger.debug(`Failover filtering applied`, {
originalCount: providers.length,
availableCount: availableProviders.length,
excludedProviders: providers.filter(p => !availableProviders.some(ap => ap.provider === p.provider)).map(p => p.provider)
}, requestId, 'routing');
return availableProviders.length > 0 ? availableProviders : providers; // Never exclude all providers
}
/**
* Check if failover should be triggered for a provider
*/
shouldTriggerFailover(health, trigger) {
switch (trigger.type) {
case 'consecutive_errors':
return health.consecutiveErrors >= trigger.threshold;
case 'http_error':
if (trigger.httpCodes && trigger.httpCodes.length > 0) {
// Count recent errors with specific HTTP codes
const recentErrors = this.getRecentErrors(health, trigger.timeWindow || 300); // 5 minutes default
const httpErrorCount = recentErrors.filter(error => error.httpCode && trigger.httpCodes.includes(error.httpCode)).length;
return httpErrorCount >= trigger.threshold;
}
return false;
case 'network_timeout':
// Count network timeout errors in recent time window
const recentErrors = this.getRecentErrors(health, trigger.timeWindow || 300);
const timeoutCount = recentErrors.filter(error => error.errorType.toLowerCase().includes('timeout') ||
error.errorType.toLowerCase().includes('network')).length;
return timeoutCount >= trigger.threshold;
case 'auth_failed':
// Count authentication failures in recent time window
const recentAuthErrors = this.getRecentErrors(health, trigger.timeWindow || 300);
const authErrorCount = recentAuthErrors.filter(error => error.errorType.toLowerCase().includes('auth') ||
error.errorType.toLowerCase().includes('unauthorized') ||
(error.httpCode && [401, 403].includes(error.httpCode))).length;
return authErrorCount >= trigger.threshold;
default:
logger_1.logger.warn(`Unknown failover trigger type: ${trigger.type}`);
return false;
}
}
/**
* Get recent errors within specified time window (seconds)
*/
getRecentErrors(health, timeWindowSeconds) {
const cutoffTime = new Date(Date.now() - (timeWindowSeconds * 1000));
return health.errorHistory.filter(error => error.timestamp >= cutoffTime);
}
/**
* Check if provider should be failed over based on error conditions
*/
shouldFailoverProvider(providerId, errorType, httpCode) {
const health = this.providerHealth.get(providerId);
if (!health) {
return false;
}
// Get failover configs from all categories that use this provider
const failoverConfigs = [];
Object.values(this.routingConfig).forEach(config => {
if (config.failover && config.failover.enabled) {
// Check if this provider is used in this category
const usesProvider = config.provider === providerId ||
(config.backup && config.backup.some(b => b.provider === providerId)) ||
(config.providers && config.providers.some(p => p.provider === providerId));
if (usesProvider) {
failoverConfigs.push(config.failover);
}
}
});
// Check if any failover config would trigger
for (const failoverConfig of failoverConfigs) {
for (const trigger of failoverConfig.triggers) {
if (this.shouldTriggerFailover(health, trigger)) {
return true;
}
}
}
return false;
}
/**
* Determine the routing category based on request characteristics
*/
determineCategory(request) {
// Check for background models (haiku models for lightweight tasks)
if (request.model.includes('haiku')) {
return 'background';
}
// Check for explicit thinking mode
if (request.metadata?.thinking) {
return 'thinking';
}
// Check for long context based on token count
const tokenCount = this.calculateRequestTokens(request);
if (tokenCount > 45000) {
return 'longcontext';
}
// Check for search tools
if (request.metadata?.tools && Array.isArray(request.metadata.tools)) {
const hasSearchTools = request.metadata.tools.some((tool) => typeof tool === 'object' && tool.name && (tool.name.toLowerCase().includes('search') ||
tool.name.toLowerCase().includes('web') ||
tool.name === 'WebSearch'));
if (hasSearchTools) {
return 'search';
}
}
// Default category for all other cases
return 'default';
}
/**
* Apply model mapping based on routing configuration
*/
applyModelMapping(request, providerId, targetModel, category) {
// Initialize metadata if not present
if (!request.metadata) {
request.metadata = { requestId: 'routing-generated' };
}
// Store original model for reference
request.metadata.originalModel = request.model;
request.metadata.targetProvider = providerId;
request.metadata.routingCategory = category;
// CRITICAL: Replace the model name directly in the request
// This ensures all downstream processing uses the correct target model
const originalModel = request.model;
request.model = targetModel;
logger_1.logger.info(`Model routing applied: ${originalModel} -> ${targetModel}`, {
category,
providerId,
originalModel,
targetModel: targetModel,
transformation: `${originalModel} -> ${targetModel} via ${providerId}`
});
}
/**
* Calculate approximate token count for routing decisions
*/
calculateRequestTokens(request) {
try {
return (0, tokenizer_1.calculateTokenCount)(request.messages, request.metadata?.system, request.metadata?.tools);
}
catch (error) {
logger_1.logger.warn('Failed to calculate token count, using message length estimation', error);
// Fallback: rough estimation based on character count
let totalChars = 0;
request.messages.forEach(msg => {
if (typeof msg.content === 'string') {
totalChars += msg.content.length;
}
else if (Array.isArray(msg.content)) {
msg.content.forEach((block) => {
if (block.text)
totalChars += block.text.length;
});
}
});
// Rough conversion: ~4 characters per token
return Math.ceil(totalChars / 4);
}
}
/**
* Update routing configuration
*/
updateConfig(routingConfig) {
this.routingConfig = routingConfig;
logger_1.logger.info('Routing configuration updated', {
categories: Object.keys(routingConfig)
});
}
/**
* Get current routing configuration summary
*/
getConfigSummary() {
const summary = {};
for (const [category, config] of Object.entries(this.routingConfig)) {
summary[category] = {
provider: config.provider,
model: config.model
};
}
return {
categories: Object.keys(this.routingConfig),
routing: summary
};
}
/**
* Get routing engine statistics (for compatibility)
*/
getStats() {
return {
categories: Object.keys(this.routingConfig),
routing: this.routingConfig,
providerHealth: this.getAllProviderHealth(),
roundRobinState: this.getRoundRobinState()
};
}
/**
* Get current round robin state from simple provider manager
*/
getRoundRobinState() {
return this.simpleProviderManager.getRoundRobinState();
}
/**
* Reset provider health (for testing or recovery)
*/
resetProviderHealth(providerId) {
if (providerId) {
const health = this.providerHealth.get(providerId);
if (health) {
health.isHealthy = true;
health.consecutiveErrors = 0;
health.errorHistory = [];
health.inCooldown = false;
health.cooldownUntil = undefined;
logger_1.logger.info(`Provider health reset: ${providerId}`);
}
}
else {
// Reset all providers
this.providerHealth.forEach((health) => {
health.isHealthy = true;
health.consecutiveErrors = 0;
health.errorHistory = [];
health.inCooldown = false;
health.cooldownUntil = undefined;
});
logger_1.logger.info('All provider health reset');
}
}
// ==================== 并发控制方法 ====================
// 🔥 移除了所有并发控制方法 - HTTP天然隔离,无需进程锁
/**
* 获取响应统计数据
*/
getResponseStats() {
return response_stats_1.responseStatsManager.getAllStats();
}
/**
* 获取统计汇总
*/
getStatsSummary() {
return response_stats_1.responseStatsManager.getSummaryStats();
}
/**
* 强制输出统计日志
*/
logCurrentStats() {
response_stats_1.responseStatsManager.logSummaryStats();
}
/**
* 重置统计数据
*/
resetStats() {
response_stats_1.responseStatsManager.reset();
}
/**
* 临时禁用provider(非持久化)
*/
temporarilyDisableProvider(providerId) {
if (!this.providerHealth.has(providerId)) {
logger_1.logger.warn(`Cannot disable unknown provider: ${providerId}`);
return false;
}
this.temporarilyDisabledProviders.add(providerId);
logger_1.logger.info(`Provider ${providerId} temporarily disabled via user control`);
return true;
}
/**
* 临时启用provider(非持久化)
*/
temporarilyEnableProvider(providerId) {
if (!this.providerHealth.has(providerId)) {
logger_1.logger.warn(`Cannot enable unknown provider: ${providerId}`);
return false;
}
const wasDisabled = this.temporarilyDisabledProviders.delete(providerId);
if (wasDisabled) {
logger_1.logger.info(`Provider ${providerId} temporarily enabled via user control`);
}
return wasDisabled;
}
/**
* 检查provider是否被临时禁用
*/
isProviderTemporarilyDisabled(providerId) {
return this.temporarilyDisabledProviders.has(providerId);
}
/**
* 获取所有临时禁用的providers
*/
getTemporarilyDisabledProviders() {
return Array.from(this.temporarilyDisabledProviders);
}
/**
* 清除所有临时禁用状态(重启时自动调用)
*/
clearTemporaryDisables() {
this.temporarilyDisabledProviders.clear();
this.simpleProviderManager.clearAllBlacklists();
logger_1.logger.info('All temporary provider disables and blacklists cleared');
}
/**
* Get blacklist status from simple provider manager
*/
getBlacklistStatus() {
return this.simpleProviderManager.getBlacklistStatus();
}
}
exports.RoutingEngine = RoutingEngine;
//# sourceMappingURL=engine.js.map