import type { BaseChatModel } from '@langchain/core/language_models/chat_models'; import type { FastifyBaseLogger } from 'fastify'; import { LLMProviderFactory, type ModelConfig, LLMProvider, type LicenseModelsConfig } from './provider.js'; import type { ModelMiddleware } from './middleware.js'; import type { License } from '../types/user.js'; /** * Model routing strategies */ export enum RoutingStrategy { /** Use user's preferred model from license */ USER_PREFERENCE = 'user_preference', /** Route based on query complexity */ COMPLEXITY = 'complexity', /** Route based on license tier */ LICENSE_TIER = 'license_tier', /** Use cheapest available model */ COST_OPTIMIZED = 'cost_optimized', } /** * Model router * Intelligently selects which model to use based on various factors */ export class ModelRouter { private factory: LLMProviderFactory; private logger: FastifyBaseLogger; private defaultModel: ModelConfig; private licenseModels?: LicenseModelsConfig; constructor(factory: LLMProviderFactory, logger: FastifyBaseLogger) { this.factory = factory; this.logger = logger; this.defaultModel = factory.getDefaultModel(); this.licenseModels = factory.getLicenseModelsConfig(); } /** * Route to appropriate model based on context */ async route( message: string, license: License, strategy: RoutingStrategy = RoutingStrategy.USER_PREFERENCE, userId?: string, maxTokens?: number ): Promise<{ model: BaseChatModel; middleware: ModelMiddleware }> { let modelConfig: ModelConfig; switch (strategy) { case RoutingStrategy.USER_PREFERENCE: modelConfig = this.routeByUserPreference(license); break; case RoutingStrategy.COMPLEXITY: modelConfig = this.routeByComplexity(message, license); break; case RoutingStrategy.LICENSE_TIER: modelConfig = this.routeByLicenseTier(license); break; case RoutingStrategy.COST_OPTIMIZED: modelConfig = this.routeByCost(license); break; default: modelConfig = this.defaultModel; } if (maxTokens !== undefined) { modelConfig = { ...modelConfig, maxTokens }; } this.logger.info( { userId, strategy, provider: modelConfig.provider, model: modelConfig.model, maxTokens: modelConfig.maxTokens, }, 'Routing to model' ); return this.factory.createModel(modelConfig); } /** * Route based on user's preferred model (if set in license) */ private routeByUserPreference(license: License): ModelConfig { // Check if user has custom model preference const preferredModel = license.preferredModel as ModelConfig | undefined; if (preferredModel && this.isModelAllowed(preferredModel, license)) { return preferredModel; } // Fall back to license tier default return this.routeByLicenseTier(license); } /** * Route based on query complexity */ private routeByComplexity(message: string, license: License): ModelConfig { const isComplex = this.isComplexQuery(message); // Use configuration if available if (this.licenseModels) { const tierConfig = this.licenseModels[license.licenseType]; if (tierConfig) { const model = isComplex ? tierConfig.complex : tierConfig.default; return { provider: this.defaultModel.provider as LLMProvider, model }; } } // Fallback to hardcoded defaults if (license.licenseType === 'enterprise') { return isComplex ? { provider: LLMProvider.DEEP_INFRA, model: 'Qwen/Qwen3-235B-A22B-Instruct-2507' } : { provider: LLMProvider.DEEP_INFRA, model: 'zai-org/GLM-5' }; } if (license.licenseType === 'pro') { return isComplex ? { provider: LLMProvider.DEEP_INFRA, model: 'zai-org/GLM-5' } : { provider: LLMProvider.DEEP_INFRA, model: 'zai-org/GLM-5' }; } return { provider: LLMProvider.DEEP_INFRA, model: 'zai-org/GLM-5' }; } /** * Route based on license tier */ private routeByLicenseTier(license: License): ModelConfig { // Use configuration if available if (this.licenseModels) { const tierConfig = this.licenseModels[license.licenseType]; if (tierConfig) { return { provider: this.defaultModel.provider as LLMProvider, model: tierConfig.default }; } } // Fallback to hardcoded defaults switch (license.licenseType) { case 'enterprise': return { provider: LLMProvider.DEEP_INFRA, model: 'zai-org/GLM-5' }; case 'pro': return { provider: LLMProvider.DEEP_INFRA, model: 'zai-org/GLM-5' }; case 'free': return { provider: LLMProvider.DEEP_INFRA, model: 'zai-org/GLM-5' }; default: return this.defaultModel; } } /** * Route to cheapest available model */ private routeByCost(license: License): ModelConfig { // Use configuration if available if (this.licenseModels) { const tierConfig = this.licenseModels[license.licenseType]; if (tierConfig) { return { provider: this.defaultModel.provider as LLMProvider, model: tierConfig.cost_optimized }; } } // Fallback: use GLM-5 return { provider: LLMProvider.DEEP_INFRA, model: 'zai-org/GLM-5' }; } /** * Check if model is allowed for user's license */ private isModelAllowed(model: ModelConfig, license: License): boolean { // Use configuration if available if (this.licenseModels) { const tierConfig = this.licenseModels[license.licenseType]; if (tierConfig) { // Check allowed_models list if defined if (tierConfig.allowed_models && tierConfig.allowed_models.length > 0) { return tierConfig.allowed_models.includes(model.model); } // Check blocked_models list if defined if (tierConfig.blocked_models && tierConfig.blocked_models.length > 0) { return !tierConfig.blocked_models.includes(model.model); } // No restrictions if neither list is defined return true; } } // Fallback to hardcoded defaults if (license.licenseType === 'free') { const allowedModels = ['zai-org/GLM-5']; return allowedModels.includes(model.model); } if (license.licenseType === 'pro') { const blockedModels = ['Qwen/Qwen3-235B-A22B-Instruct-2507']; return !blockedModels.includes(model.model); } // Enterprise: all models allowed return true; } /** * Determine if query is complex */ private isComplexQuery(message: string): boolean { const complexityIndicators = [ // Multi-step analysis 'backtest', 'analyze', 'compare', 'optimize', // Code generation 'write', 'create', 'implement', 'build', // Deep reasoning 'explain why', 'what if', 'how would', // Long messages (> 200 chars likely complex) message.length > 200, ]; const messageLower = message.toLowerCase(); return complexityIndicators.some((indicator) => typeof indicator === 'string' ? messageLower.includes(indicator) : indicator ); } }