diff --git a/internal/api/modules/amp/amp.go b/internal/api/modules/amp/amp.go index b5a139f6..281fda65 100644 --- a/internal/api/modules/amp/amp.go +++ b/internal/api/modules/amp/amp.go @@ -166,7 +166,10 @@ func (m *AmpModule) getAuthMiddleware(ctx modules.Context) gin.HandlerFunc { func (m *AmpModule) OnConfigUpdated(cfg *config.Config) error { // Update model mappings (hot-reload supported) if m.modelMapper != nil { + log.Infof("amp config updated: reloading %d model mapping(s)", len(cfg.AmpModelMappings)) m.modelMapper.UpdateMappings(cfg.AmpModelMappings) + } else { + log.Warnf("amp model mapper not initialized, skipping model mapping update") } if !m.enabled { diff --git a/internal/api/server.go b/internal/api/server.go index ab9c0354..fb3610c2 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -150,6 +150,9 @@ type Server struct { // management handler mgmt *managementHandlers.Handler + // ampModule is the Amp routing module for model mapping hot-reload + ampModule *ampmodule.AmpModule + // managementRoutesRegistered tracks whether the management routes have been attached to the engine. managementRoutesRegistered atomic.Bool // managementRoutesEnabled controls whether management endpoints serve real handlers. @@ -268,14 +271,14 @@ func NewServer(cfg *config.Config, authManager *auth.Manager, accessManager *sdk s.setupRoutes() // Register Amp module using V2 interface with Context - ampModule := ampmodule.NewLegacy(accessManager, AuthMiddleware(accessManager)) + s.ampModule = ampmodule.NewLegacy(accessManager, AuthMiddleware(accessManager)) ctx := modules.Context{ Engine: engine, BaseHandler: s.handlers, Config: cfg, AuthMiddleware: AuthMiddleware(accessManager), } - if err := modules.RegisterModule(ctx, ampModule); err != nil { + if err := modules.RegisterModule(ctx, s.ampModule); err != nil { log.Errorf("Failed to register Amp module: %v", err) } @@ -916,6 +919,16 @@ func (s *Server) UpdateClients(cfg *config.Config) { s.mgmt.SetAuthManager(s.handlers.AuthManager) } + // Notify Amp module of config changes (for model mapping hot-reload) + if s.ampModule != nil { + log.Debugf("triggering amp module config update") + if err := s.ampModule.OnConfigUpdated(cfg); err != nil { + log.Errorf("failed to update Amp module config: %v", err) + } + } else { + log.Warnf("amp module is nil, skipping config update") + } + // Count client sources from configuration and auth directory authFiles := util.CountAuthFiles(cfg.AuthDir) geminiAPIKeyCount := len(cfg.GeminiKey) diff --git a/internal/watcher/watcher.go b/internal/watcher/watcher.go index a284541a..af72212d 100644 --- a/internal/watcher/watcher.go +++ b/internal/watcher/watcher.go @@ -162,12 +162,14 @@ func NewWatcher(configPath, authDir string, reloadCallback func(*config.Config)) // Start begins watching the configuration file and authentication directory func (w *Watcher) Start(ctx context.Context) error { - // Watch the config file - if errAddConfig := w.watcher.Add(w.configPath); errAddConfig != nil { - log.Errorf("failed to watch config file %s: %v", w.configPath, errAddConfig) + // Watch the config file's parent directory instead of the file itself. + // This handles editors that use atomic save (write to temp, then rename). + configDir := filepath.Dir(w.configPath) + if errAddConfig := w.watcher.Add(configDir); errAddConfig != nil { + log.Errorf("failed to watch config directory %s: %v", configDir, errAddConfig) return errAddConfig } - log.Debugf("watching config file: %s", w.configPath) + log.Debugf("watching config directory: %s (for file: %s)", configDir, filepath.Base(w.configPath)) // Watch the auth directory if errAddAuthDir := w.watcher.Add(w.authDir); errAddAuthDir != nil { @@ -700,7 +702,23 @@ func (w *Watcher) isKnownAuthFile(path string) bool { func (w *Watcher) handleEvent(event fsnotify.Event) { // Filter only relevant events: config file or auth-dir JSON files. configOps := fsnotify.Write | fsnotify.Create | fsnotify.Rename - isConfigEvent := event.Name == w.configPath && event.Op&configOps != 0 + // Check if this event is for our config file (handle both exact match and basename match for directory watching) + isConfigEvent := false + if event.Op&configOps != 0 { + // Exact path match + if event.Name == w.configPath { + isConfigEvent = true + } else { + // Check if basename matches and it's in the config directory (for atomic save detection) + configDir := filepath.Dir(w.configPath) + configBase := filepath.Base(w.configPath) + eventDir := filepath.Dir(event.Name) + eventBase := filepath.Base(event.Name) + if eventDir == configDir && eventBase == configBase { + isConfigEvent = true + } + } + } authOps := fsnotify.Create | fsnotify.Write | fsnotify.Remove | fsnotify.Rename isAuthJSON := strings.HasPrefix(event.Name, w.authDir) && strings.HasSuffix(event.Name, ".json") && event.Op&authOps != 0 if !isConfigEvent && !isAuthJSON {