UNPKG

@lobehub/chat

Version:

Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.

291 lines (248 loc) • 9.46 kB
/** * Model Resolver Service * * Unified service for model, VAE, and component resolution * Handles all model-related operations through the client service */ import debug from 'debug'; import { COMPONENT_NODE_MAPPINGS, CUSTOM_SD_CONFIG, SUPPORTED_MODEL_FORMATS, } from '@/server/services/comfyui/config/constants'; import { MODEL_ID_VARIANT_MAP, MODEL_REGISTRY, } from '@/server/services/comfyui/config/modelRegistry'; import { SYSTEM_COMPONENTS } from '@/server/services/comfyui/config/systemComponents'; import { ComfyUIClientService } from '@/server/services/comfyui/core/comfyUIClientService'; import { ModelResolverError } from '@/server/services/comfyui/errors/modelResolverError'; import { TTLCacheManager } from '@/server/services/comfyui/utils/cacheManager'; import { getModelsByVariant } from '@/server/services/comfyui/utils/staticModelLookup'; const log = debug('lobe-image:comfyui:model-resolver'); /** * Check if a filename has a supported model format extension * @param filename - The filename to check * @returns True if the filename has a supported model format extension */ const isModelFile = (filename: string): boolean => { return SUPPORTED_MODEL_FORMATS.some((ext) => filename.endsWith(ext)); }; /** * Model validation result */ export interface ModelValidationResult { actualFileName?: string; exists: boolean; } /** * Internal model resolution details */ interface ModelResolutionDetails { cleanId: string; expectedFiles: string[]; fileName?: string; variant?: string; } /** * Model Resolver Service * Provides model resolution, validation, and component selection * * Caching strategy: * - Model name resolution: Cached locally (business logic) * - Component lists (VAE, CLIP, etc.): Cached in ComfyUIClientService * * @params clientService - The ComfyUI client service instance * @returns The resolved model filename or undefined if not found * @note This service does not handle workflow building or execution */ export class ModelResolverService { private clientService: ComfyUIClientService; private cacheManager: TTLCacheManager; constructor(clientService: ComfyUIClientService) { this.clientService = clientService; this.cacheManager = new TTLCacheManager(60_000); // 1 minute TTL } /** * Internal method to resolve model details with all information * This eliminates DRY violations between resolveModelFileName and validateModel */ private async _resolveModelDetails(modelId: string): Promise<ModelResolutionDetails> { log('Resolving model details:', modelId); // Clean model ID (remove prefix) const cleanId = modelId.replace(/^comfyui\//, ''); // Get mapped variant and expected files const mappedVariant = MODEL_ID_VARIANT_MAP[cleanId]; const expectedFiles = mappedVariant ? getModelsByVariant(mappedVariant) : []; // Special handling for custom SD models - force fixed filename if (cleanId === 'stable-diffusion-custom' || cleanId === 'stable-diffusion-custom-refiner') { const fixedFileName = CUSTOM_SD_CONFIG.MODEL_FILENAME; // Verify the custom model file exists on server const serverModels = await this.getAvailableModelFiles(); const fileName = serverModels.includes(fixedFileName) ? fixedFileName : undefined; if (fileName) { log('Resolved custom SD model to fixed filename:', fileName); } return { cleanId, expectedFiles, fileName, variant: mappedVariant, }; } // 1. Try model ID mapping first log('Checking MODEL_ID_VARIANT_MAP for:', cleanId); log('Mapped variant result:', mappedVariant); if (mappedVariant) { log('Found model ID mapping:', cleanId, '->', mappedVariant); log('Prioritized models for variant', mappedVariant, ':', expectedFiles); const serverModels = await this.getAvailableModelFiles(); // Find first available model from prioritized list for (const filename of expectedFiles) { if (serverModels.includes(filename)) { log('Found available model by variant:', filename); return { cleanId, expectedFiles, fileName: filename, variant: mappedVariant, }; } } log('No prioritized models available on server for variant:', mappedVariant); } else { log('No mapping found for cleanId:', cleanId); } // 2. Direct registry lookup (filename is the registry key) if (MODEL_REGISTRY[cleanId]) { log('Found in registry:', cleanId); return { cleanId, expectedFiles, fileName: cleanId, variant: mappedVariant, }; } // 3. If it's already a model file format, check if it exists on server if (isModelFile(cleanId)) { const serverModels = await this.getAvailableModelFiles(); if (serverModels.includes(cleanId)) { log('Found on server:', cleanId); return { cleanId, expectedFiles, fileName: cleanId, variant: mappedVariant, }; } } // 4. Not found return { cleanId, expectedFiles, fileName: undefined, variant: mappedVariant, }; } /** * Resolve a model ID to its actual filename * Fixed: removed over-defensive programming and guessing strategies */ async resolveModelFileName(modelId: string): Promise<string | undefined> { return this.cacheManager.get(`model:${modelId}`, async () => { const details = await this._resolveModelDetails(modelId); return details.fileName; }); } /** * Get available model files from server */ async getAvailableModelFiles(): Promise<string[]> { const checkpoints = await this.clientService.getCheckpoints(); return checkpoints || []; } /** * Get available VAE files from server * Note: Results are cached in ComfyUIClientService.getNodeDefs() */ async getAvailableVAEFiles(): Promise<string[]> { // Use SDK's getNodeDefs method (already includes caching) const nodeDefs = await this.clientService.getNodeDefs('VAELoader'); if (!nodeDefs?.VAELoader?.input?.required?.vae_name?.[0]) { return []; } const vaeList = nodeDefs.VAELoader.input.required.vae_name[0]; if (!Array.isArray(vaeList)) { return []; } return vaeList; } /** * Get available component files from ComfyUI node * Generic method that queries ComfyUI for any node type's available files * Note: Results are cached in ComfyUIClientService.getNodeDefs() * @param loaderNode - The ComfyUI node name (e.g., 'CLIPLoader', 'VAELoader') * @param inputKey - The input field name to query (e.g., 'clip_name', 'vae_name') */ async getAvailableComponentFiles(loaderNode: string, inputKey: string): Promise<string[]> { const nodeDefs = await this.clientService.getNodeDefs(loaderNode); const loader = nodeDefs?.[loaderNode]; if (!loader?.input?.required?.[inputKey]?.[0]) { // Node doesn't exist or no files available - normal case return []; } const componentList = loader.input.required[inputKey][0]; if (!Array.isArray(componentList)) { return []; } return componentList; } /** * Get optimal component for a specific type and model family * New method: provides single component query functionality * @param type - Component type (clip, t5, vae, unet) * @param modelFamily - Model family (FLUX, SD3, etc.) * @returns The best matching component name */ async getOptimalComponent(type: string, modelFamily: string): Promise<string | undefined> { // Get prioritized components from configuration const configComponents = Object.entries(SYSTEM_COMPONENTS) .filter(([, config]) => config.type === type && config.modelFamily === modelFamily) .sort(([, a], [, b]) => a.priority - b.priority); // Get node mapping for this component type const nodeMapping = COMPONENT_NODE_MAPPINGS[type]; if (!nodeMapping) { return undefined; } // Get available files from server const serverFiles = await this.getAvailableComponentFiles(nodeMapping.node, nodeMapping.field); // Return first matching component from config priority for (const [name] of configComponents) { if (serverFiles.includes(name)) { return name; } } // No matching component found return undefined; } /** * Validate if a model exists * @throws ModelResolverError if model not found with details about expected files */ async validateModel(modelId: string): Promise<ModelValidationResult> { // Use the internal method to get all resolution details const details = await this._resolveModelDetails(modelId); if (!details.fileName) { // Create simplified error message with only top priority models // expectedFiles are already sorted by priority from getModelsByVariant const topPriorityFiles = details.expectedFiles.slice(0, 1); // Show top priority options let errorMessage = `Model not found: ${topPriorityFiles.join(', ')}, please install one first.`; throw new ModelResolverError(ModelResolverError.Reasons.MODEL_NOT_FOUND, errorMessage, { expectedFiles: details.expectedFiles, modelId, variant: details.variant, }); } return { actualFileName: details.fileName, exists: true }; } }