import * as fs from 'node:fs/promises'; import { PATHS, VERSION } from '../constants.ts'; import type { IModelGridConfig } from '../interfaces/config.ts'; import { logger } from '../logger.ts'; export class ConfigManager { public async loadConfig(): Promise { try { const configContent = await fs.readFile(PATHS.CONFIG_FILE, 'utf-8'); return this.normalizeConfig(JSON.parse(configContent) as Partial); } catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') { throw new Error(`Configuration file not found: ${PATHS.CONFIG_FILE}`); } throw error; } } public async saveConfig(config: IModelGridConfig): Promise { await fs.mkdir(PATHS.CONFIG_DIR, { recursive: true }); await fs.writeFile(PATHS.CONFIG_FILE, JSON.stringify(config, null, 2)); } public normalizeConfig(config: Partial): IModelGridConfig { this.logIgnoredConfigKeys(config); const filteredContainers = (config.containers || []).filter( (container) => (container as { type?: string }).type !== 'ollama', ); return { version: config.version || VERSION, api: { port: config.api?.port || 8080, host: config.api?.host || '0.0.0.0', apiKeys: config.api?.apiKeys || [], rateLimit: config.api?.rateLimit, cors: config.api?.cors ?? true, corsOrigins: config.api?.corsOrigins || ['*'], }, ui: { enabled: config.ui?.enabled ?? true, port: config.ui?.port || 8081, host: config.ui?.host || '0.0.0.0', assetSource: config.ui?.assetSource === 'disk' ? 'disk' : 'bundle', }, docker: { networkName: config.docker?.networkName || 'modelgrid', runtime: config.docker?.runtime || 'docker', socketPath: config.docker?.socketPath, }, gpus: { autoDetect: config.gpus?.autoDetect ?? true, assignments: config.gpus?.assignments || {}, }, containers: filteredContainers, models: { registryUrl: config.models?.registryUrl || 'https://list.modelgrid.com/catalog/models.json', autoDeploy: config.models?.autoDeploy ?? true, defaultEngine: 'vllm', autoLoad: config.models?.autoLoad || [], }, cluster: { enabled: config.cluster?.enabled ?? false, nodeName: config.cluster?.nodeName || 'modelgrid-local', role: config.cluster?.role || 'standalone', bindHost: config.cluster?.bindHost || '0.0.0.0', gossipPort: config.cluster?.gossipPort || 7946, sharedSecret: config.cluster?.sharedSecret, advertiseUrl: config.cluster?.advertiseUrl, controlPlaneUrl: config.cluster?.controlPlaneUrl, heartbeatIntervalMs: config.cluster?.heartbeatIntervalMs || 5000, seedNodes: config.cluster?.seedNodes || [], }, checkInterval: config.checkInterval || 30000, }; } private logIgnoredConfigKeys(config: Partial): void { const unknownTopLevelKeys = Object.keys(config).filter((key) => !['version', 'api', 'ui', 'docker', 'gpus', 'containers', 'models', 'cluster', 'checkInterval'] .includes(key) ); for (const key of unknownTopLevelKeys) { logger.warn(`Ignoring unknown config key: ${key}`); } const legacyModelConfig = config.models as { greenlistUrl?: string; autoPull?: boolean; defaultContainer?: string; } | undefined; if (legacyModelConfig?.greenlistUrl) { logger.warn('Ignoring removed config key: models.greenlistUrl'); } if (legacyModelConfig?.autoPull !== undefined) { logger.warn('Ignoring removed config key: models.autoPull'); } if (legacyModelConfig?.defaultContainer) { logger.warn('Ignoring removed config key: models.defaultContainer'); } for (const container of config.containers || []) { const containerType = (container as { type?: string }).type; if (containerType === 'ollama') { logger.warn('Ignoring unsupported container type: ollama'); } } } }