package config import ( "encoding/json" "fmt" "os" "sync" "time" ) // ModelsConfigStore manages persistence of model configuration to a JSON file. type ModelsConfigStore struct { mu sync.RWMutex path string config *ModelsConfig } // NewModelsConfigStore creates a ModelsConfigStore, creating an empty config file if it doesn't exist. func NewModelsConfigStore(path string) (*ModelsConfigStore, error) { s := &ModelsConfigStore{ path: path, config: &ModelsConfig{ Version: "1.0", Providers: make(map[string]*ProviderConfig), Models: make(map[string]*ModelConfig), Routing: make(map[string]*RoutingRule), }, } if err := s.load(); err != nil { return nil, err } return s, nil } func (s *ModelsConfigStore) load() error { data, err := os.ReadFile(s.path) if err != nil { if os.IsNotExist(err) { return s.save() // Initialize empty file. } return fmt.Errorf("read model config file: %w", err) } if len(data) == 0 { return nil } var cfg ModelsConfig if err := json.Unmarshal(data, &cfg); err != nil { return fmt.Errorf("parse model config file: %w", err) } if cfg.Providers == nil { cfg.Providers = make(map[string]*ProviderConfig) } if cfg.Models == nil { cfg.Models = make(map[string]*ModelConfig) } if cfg.Routing == nil { cfg.Routing = make(map[string]*RoutingRule) } if cfg.Version == "" { cfg.Version = "1.0" } s.config = &cfg return nil } func (s *ModelsConfigStore) save() error { data, err := json.MarshalIndent(s.config, "", " ") if err != nil { return fmt.Errorf("marshal model config: %w", err) } tmpPath := s.path + ".tmp" if err := os.WriteFile(tmpPath, data, 0640); err != nil { return fmt.Errorf("write model config file: %w", err) } return os.Rename(tmpPath, s.path) } // HasConfig returns true if there are any providers or models configured. func (s *ModelsConfigStore) HasConfig() bool { s.mu.RLock() defer s.mu.RUnlock() return len(s.config.Providers) > 0 || len(s.config.Models) > 0 } // ---- Providers ---- func (s *ModelsConfigStore) ListProviders() []*ProviderConfig { s.mu.RLock() defer s.mu.RUnlock() result := make([]*ProviderConfig, 0, len(s.config.Providers)) for _, p := range s.config.Providers { result = append(result, p) } return result } func (s *ModelsConfigStore) GetProvider(name string) (*ProviderConfig, error) { s.mu.RLock() defer s.mu.RUnlock() p, ok := s.config.Providers[name] if !ok { return nil, fmt.Errorf("provider not found: %s", name) } return p, nil } func (s *ModelsConfigStore) SetProvider(cfg *ProviderConfig) error { s.mu.Lock() defer s.mu.Unlock() if cfg.Name == "" { return fmt.Errorf("provider name is required") } if cfg.BaseURL == "" { return fmt.Errorf("provider base_url is required") } cfg.UpdatedAt = time.Now() s.config.Providers[cfg.Name] = cfg return s.save() } func (s *ModelsConfigStore) DeleteProvider(name string) error { s.mu.Lock() defer s.mu.Unlock() if _, ok := s.config.Providers[name]; !ok { return fmt.Errorf("provider not found: %s", name) } delete(s.config.Providers, name) return s.save() } // ---- Models ---- func (s *ModelsConfigStore) ListModels() []*ModelConfig { s.mu.RLock() defer s.mu.RUnlock() result := make([]*ModelConfig, 0, len(s.config.Models)) for _, m := range s.config.Models { result = append(result, m) } return result } func (s *ModelsConfigStore) GetModel(id string) (*ModelConfig, error) { s.mu.RLock() defer s.mu.RUnlock() m, ok := s.config.Models[id] if !ok { return nil, fmt.Errorf("model not found: %s", id) } return m, nil } func (s *ModelsConfigStore) SetModel(cfg *ModelConfig) error { s.mu.Lock() defer s.mu.Unlock() if cfg.ID == "" { return fmt.Errorf("model id is required") } if cfg.Provider == "" { return fmt.Errorf("model provider is required") } cfg.UpdatedAt = time.Now() if cfg.Params == nil { cfg.Params = make(map[string]interface{}) } if cfg.Tags == nil { cfg.Tags = []string{} } s.config.Models[cfg.ID] = cfg return s.save() } func (s *ModelsConfigStore) DeleteModel(id string) error { s.mu.Lock() defer s.mu.Unlock() if _, ok := s.config.Models[id]; !ok { return fmt.Errorf("model not found: %s", id) } delete(s.config.Models, id) return s.save() } // ---- Routing ---- func (s *ModelsConfigStore) ListRouting() []*RoutingRule { s.mu.RLock() defer s.mu.RUnlock() result := make([]*RoutingRule, 0, len(s.config.Routing)) for _, r := range s.config.Routing { result = append(result, r) } return result } func (s *ModelsConfigStore) GetRouting(purpose string) (*RoutingRule, error) { s.mu.RLock() defer s.mu.RUnlock() r, ok := s.config.Routing[purpose] if !ok { return nil, fmt.Errorf("routing not found: %s", purpose) } return r, nil } func (s *ModelsConfigStore) SetRouting(rule *RoutingRule) error { s.mu.Lock() defer s.mu.Unlock() if rule.Purpose == "" { return fmt.Errorf("routing purpose is required") } if len(rule.FallbackChain) == 0 { return fmt.Errorf("routing fallback_chain is required") } s.config.Routing[rule.Purpose] = rule return s.save() } func (s *ModelsConfigStore) DeleteRouting(purpose string) error { s.mu.Lock() defer s.mu.Unlock() if _, ok := s.config.Routing[purpose]; !ok { return fmt.Errorf("routing not found: %s", purpose) } delete(s.config.Routing, purpose) return s.save() } // GetConfig returns a copy of the full config (for ai-core loader compatibility). func (s *ModelsConfigStore) GetConfig() *ModelsConfig { s.mu.RLock() defer s.mu.RUnlock() // Return shallow copy; callers should treat as read-only. return s.config }