// Dynagate config manager package main import ( "context" "encoding/csv" "log" "os" "strings" "sync" "time" ) type ModelConfig struct { Model string Key string Endpoint string Extra string } type ConfigManager struct { filePath string mu sync.RWMutex configs []ModelConfig uniqueModels []string lastModTime time.Time lastSize int64 } func NewConfigManager(filePath string) *ConfigManager { cm := &ConfigManager{ filePath: filePath, } if err := cm.Load(); err != nil { log.Printf("Initial configuration load failed or file not found (will retry on change): %v", err) } return cm } func (cm *ConfigManager) Load() error { file, err := os.Open(cm.filePath) if err != nil { return err } defer file.Close() stat, err := file.Stat() if err != nil { return err } reader := csv.NewReader(file) reader.FieldsPerRecord = -1 rows, err := reader.ReadAll() if err != nil { return err } var configs []ModelConfig if len(rows) == 0 { cm.update(configs, stat.ModTime(), stat.Size()) return nil } // Detect header row hasHeader := false firstRow := rows[0] for _, col := range firstRow { colLower := strings.ToLower(strings.TrimSpace(col)) if colLower == "model" || colLower == "key" || colLower == "endpoint" { hasHeader = true break } } var startIdx int modelIdx, keyIdx, endpointIdx, extraIdx := 0, 1, 2, -1 if hasHeader { startIdx = 1 modelIdx, keyIdx, endpointIdx, extraIdx = -1, -1, -1, -1 for i, col := range firstRow { colLower := strings.ToLower(strings.TrimSpace(col)) switch colLower { case "model": modelIdx = i case "key": keyIdx = i case "endpoint": endpointIdx = i case "extra": extraIdx = i } } // Fallbacks if columns are not named exactly if modelIdx == -1 { modelIdx = 0 } if keyIdx == -1 { keyIdx = 1 } if endpointIdx == -1 { endpointIdx = 2 } } else { startIdx = 0 } for i := startIdx; i < len(rows); i++ { row := rows[i] maxIdx := modelIdx if keyIdx > maxIdx { maxIdx = keyIdx } if endpointIdx > maxIdx { maxIdx = endpointIdx } if len(row) <= maxIdx { continue } m := strings.TrimSpace(row[modelIdx]) k := strings.TrimSpace(row[keyIdx]) ep := strings.TrimSpace(row[endpointIdx]) if m == "" || k == "" || ep == "" { continue } extra := "" if hasHeader { if extraIdx != -1 && len(row) > extraIdx { extra = strings.TrimSpace(row[extraIdx]) } } else { if len(row) > 3 { extra = strings.TrimSpace(row[3]) } } configs = append(configs, ModelConfig{ Model: m, Key: k, Endpoint: ep, Extra: extra, }) } cm.update(configs, stat.ModTime(), stat.Size()) log.Printf("Loaded %d model configurations from %s", len(configs), cm.filePath) return nil } func (cm *ConfigManager) update(configs []ModelConfig, modTime time.Time, size int64) { cm.mu.Lock() defer cm.mu.Unlock() cm.configs = configs cm.lastModTime = modTime cm.lastSize = size var uniqueModels []string seen := make(map[string]bool) for _, cfg := range configs { if !seen[cfg.Model] { seen[cfg.Model] = true uniqueModels = append(uniqueModels, cfg.Model) } } cm.uniqueModels = uniqueModels } func (cm *ConfigManager) GetConfigs() []ModelConfig { cm.mu.RLock() defer cm.mu.RUnlock() res := make([]ModelConfig, len(cm.configs)) copy(res, cm.configs) return res } func (cm *ConfigManager) GetUniqueModels() []string { cm.mu.RLock() defer cm.mu.RUnlock() res := make([]string, len(cm.uniqueModels)) copy(res, cm.uniqueModels) return res } func (cm *ConfigManager) Watch(ctx context.Context) { ticker := time.NewTicker(1 * time.Second) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: stat, err := os.Stat(cm.filePath) if err != nil { continue } cm.mu.RLock() lastMod := cm.lastModTime lastSz := cm.lastSize cm.mu.RUnlock() if stat.ModTime().After(lastMod) || stat.Size() != lastSz { log.Printf("Detected changes in %s, reloading...", cm.filePath) if err := cm.Load(); err != nil { log.Printf("Error reloading configuration: %v", err) } else { log.Printf("Configuration reloaded successfully") } } } } }