diff --git a/src/lib/config/index.ts b/src/lib/config/index.ts new file mode 100644 index 0000000..e8c3b5d --- /dev/null +++ b/src/lib/config/index.ts @@ -0,0 +1,137 @@ +import path from 'node:path'; +import fs from 'fs'; +import { Config, ConfigModelProvider, EnvMap, UIConfigSections } from './types'; +import ModelRegistry from '../models/registry'; +import { hashObj } from '../serverUtils'; + +class ConfigManager { + configPath: string = path.join( + process.env.DATA_DIR || process.cwd(), + '/data/config.json', + ); + configVersion = 1; + currentConfig: Config = { + version: this.configVersion, + general: {}, + modelProviders: [], + }; + uiConfigSections: UIConfigSections = { + general: [], + modelProviders: [], + }; + modelRegistry = new ModelRegistry(); + + constructor() { + this.initialize(); + } + + private initialize() { + this.initializeConfig(); + this.initializeFromEnv(); + } + + private saveConfig() { + fs.writeFileSync( + this.configPath, + JSON.stringify(this.currentConfig, null, 2), + ); + } + + private initializeConfig() { + const exists = fs.existsSync(this.configPath); + if (!exists) { + fs.writeFileSync( + this.configPath, + JSON.stringify(this.currentConfig, null, 2), + ); + } else { + try { + this.currentConfig = JSON.parse( + fs.readFileSync(this.configPath, 'utf-8'), + ); + } catch (err) { + if (err instanceof SyntaxError) { + console.error( + `Error parsing config file at ${this.configPath}:`, + err, + ); + console.log( + 'Loading default config and overwriting the existing file.', + ); + fs.writeFileSync( + this.configPath, + JSON.stringify(this.currentConfig, null, 2), + ); + return; + } else { + console.log('Unknown error reading config file:', err); + } + } + + this.currentConfig = this.migrateConfigNeeded(this.currentConfig); + } + } + + private migrateConfigNeeded(config: Config): Config { + /* TODO: Add migrations */ + return config; + } + + private initializeFromEnv() { + const providerConfigSections = this.modelRegistry.getUIConfigSection(); + + this.uiConfigSections.modelProviders = providerConfigSections; + + const newProviders: ConfigModelProvider[] = []; + + providerConfigSections.forEach((provider) => { + const newProvider: ConfigModelProvider & { required?: string[] } = { + id: crypto.randomUUID(), + name: `${provider.name} ${Math.floor(Math.random() * 1000)}`, + type: provider.key, + chatModels: [], + embeddingModels: [], + config: {}, + required: [], + hash: '', + }; + + provider.fields.forEach((field) => { + newProvider.config[field.key] = + process.env[field.env!] || + field.default || + ''; /* Env var must exist for providers */ + + if (field.required) newProvider.required?.push(field.key); + }); + + let configured = true; + + newProvider.required?.forEach((r) => { + if (!newProvider.config[r]) { + configured = false; + } + }); + + if (configured) { + const hash = hashObj(newProvider.config); + newProvider.hash = hash; + delete newProvider.required; + + const exists = this.currentConfig.modelProviders.find( + (p) => p.hash === hash, + ); + + if (!exists) { + newProviders.push(newProvider); + } + } + }); + + this.currentConfig.modelProviders.push(...newProviders); + + this.saveConfig(); + } +} + +new ConfigManager(); diff --git a/src/lib/config/types.ts b/src/lib/config/types.ts new file mode 100644 index 0000000..9cd89b9 --- /dev/null +++ b/src/lib/config/types.ts @@ -0,0 +1,82 @@ +type BaseUIConfigField = { + name: string; + key: string; + required: boolean; + description: string; + scope: 'client' | 'server'; + env?: string; +}; + +type StringUIConfigField = BaseUIConfigField & { + type: 'string'; + placeholder?: string; + default?: string; +}; + +type SelectUIConfigFieldOptions = { + name: string; + key: string; + value: string; +}; + +type SelectUIConfigField = BaseUIConfigField & { + type: 'select'; + default?: string; + options: SelectUIConfigFieldOptions[]; +}; + +type PasswordUIConfigField = BaseUIConfigField & { + type: 'password'; + placeholder?: string; + default?: string; +}; + +type UIConfigField = + | StringUIConfigField + | SelectUIConfigField + | PasswordUIConfigField; + +type ConfigModelProvider = { + id: string; + name: string; + type: string; + chatModels: string[]; + embeddingModels: string[]; + config: { [key: string]: any }; + hash: string; +}; + +type Config = { + version: number; + general: { + [key: string]: any; + }; + modelProviders: ConfigModelProvider[]; +}; + +type EnvMap = { + [key: string]: { + fieldKey: string; + providerKey: string; + }; +}; + +type ModelProviderUISection = { + name: string; + key: string; + fields: UIConfigField[]; +}; + +type UIConfigSections = { + general: UIConfigField[]; + modelProviders: ModelProviderUISection[]; +}; + +export type { + UIConfigField, + Config, + EnvMap, + UIConfigSections, + ModelProviderUISection, + ConfigModelProvider, +};