diff --git a/internal/api/server.go b/internal/api/server.go index 70bd487e..2a35b642 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -194,7 +194,7 @@ func NewServer(cfg *config.Config, authManager *auth.Manager, accessManager *sdk loggerToggle: toggle, configFilePath: configFilePath, } - s.applyAccessConfig(cfg) + s.applyAccessConfig(nil, cfg) // Initialize management handler s.mgmt = managementHandlers.NewHandler(cfg, configFilePath, authManager) if optionState.localPassword != "" { @@ -547,16 +547,23 @@ func corsMiddleware() gin.HandlerFunc { } } -func (s *Server) applyAccessConfig(cfg *config.Config) { - if s == nil || s.accessManager == nil { +func (s *Server) applyAccessConfig(oldCfg, newCfg *config.Config) { + if s == nil || s.accessManager == nil || newCfg == nil { return } - providers, err := sdkaccess.BuildProviders(cfg) + existing := s.accessManager.Providers() + providers, added, updated, removed, err := sdkaccess.ReconcileProviders(oldCfg, newCfg, existing) if err != nil { - log.Errorf("failed to update request auth providers: %v", err) + log.Errorf("failed to reconcile request auth providers: %v", err) return } s.accessManager.SetProviders(providers) + if len(added)+len(updated)+len(removed) > 0 { + log.Infof("server request auth providers reconciled (added=%d updated=%d removed=%d)", len(added), len(updated), len(removed)) + log.Debugf("server request auth provider details - added=%v updated=%v removed=%v", added, updated, removed) + } else { + log.Debug("server request auth providers unchanged after config update") + } } // UpdateClients updates the server's client list and configuration. @@ -566,44 +573,60 @@ func (s *Server) applyAccessConfig(cfg *config.Config) { // - clients: The new slice of AI service clients // - cfg: The new application configuration func (s *Server) UpdateClients(cfg *config.Config) { + oldCfg := s.cfg + // Update request logger enabled state if it has changed - if s.requestLogger != nil && s.cfg.RequestLog != cfg.RequestLog { + previousRequestLog := false + if oldCfg != nil { + previousRequestLog = oldCfg.RequestLog + } + if s.requestLogger != nil && (oldCfg == nil || previousRequestLog != cfg.RequestLog) { if s.loggerToggle != nil { s.loggerToggle(cfg.RequestLog) } else if toggler, ok := s.requestLogger.(interface{ SetEnabled(bool) }); ok { toggler.SetEnabled(cfg.RequestLog) } - log.Debugf("request logging updated from %t to %t", s.cfg.RequestLog, cfg.RequestLog) - } - - if s.cfg.LoggingToFile != cfg.LoggingToFile { - if err := logging.ConfigureLogOutput(cfg.LoggingToFile); err != nil { - log.Errorf("failed to reconfigure log output: %v", err) + if oldCfg != nil { + log.Debugf("request logging updated from %t to %t", previousRequestLog, cfg.RequestLog) } else { - log.Debugf("logging_to_file updated from %t to %t", s.cfg.LoggingToFile, cfg.LoggingToFile) + log.Debugf("request logging toggled to %t", cfg.RequestLog) } } - if s.cfg == nil || s.cfg.UsageStatisticsEnabled != cfg.UsageStatisticsEnabled { + if oldCfg != nil && oldCfg.LoggingToFile != cfg.LoggingToFile { + if err := logging.ConfigureLogOutput(cfg.LoggingToFile); err != nil { + log.Errorf("failed to reconfigure log output: %v", err) + } else { + log.Debugf("logging_to_file updated from %t to %t", oldCfg.LoggingToFile, cfg.LoggingToFile) + } + } + + if oldCfg == nil || oldCfg.UsageStatisticsEnabled != cfg.UsageStatisticsEnabled { usage.SetStatisticsEnabled(cfg.UsageStatisticsEnabled) - if s.cfg != nil { - log.Debugf("usage_statistics_enabled updated from %t to %t", s.cfg.UsageStatisticsEnabled, cfg.UsageStatisticsEnabled) + if oldCfg != nil { + log.Debugf("usage_statistics_enabled updated from %t to %t", oldCfg.UsageStatisticsEnabled, cfg.UsageStatisticsEnabled) + } else { + log.Debugf("usage_statistics_enabled toggled to %t", cfg.UsageStatisticsEnabled) } } // Update log level dynamically when debug flag changes - if s.cfg.Debug != cfg.Debug { + if oldCfg == nil || oldCfg.Debug != cfg.Debug { util.SetLogLevel(cfg) - log.Debugf("debug mode updated from %t to %t", s.cfg.Debug, cfg.Debug) + if oldCfg != nil { + log.Debugf("debug mode updated from %t to %t", oldCfg.Debug, cfg.Debug) + } else { + log.Debugf("debug mode toggled to %t", cfg.Debug) + } } + s.applyAccessConfig(oldCfg, cfg) s.cfg = cfg s.handlers.UpdateClients(cfg) if s.mgmt != nil { s.mgmt.SetConfig(cfg) s.mgmt.SetAuthManager(s.handlers.AuthManager) } - s.applyAccessConfig(cfg) // Count client sources from configuration and auth directory authFiles := util.CountAuthFiles(cfg.AuthDir) diff --git a/internal/registry/model_registry.go b/internal/registry/model_registry.go index 2987aa5d..0a941916 100644 --- a/internal/registry/model_registry.go +++ b/internal/registry/model_registry.go @@ -101,58 +101,220 @@ func (r *ModelRegistry) RegisterClient(clientID, clientProvider string, models [ r.mutex.Lock() defer r.mutex.Unlock() - // Remove any existing registration for this client - r.unregisterClientInternal(clientID) - provider := strings.ToLower(clientProvider) + seen := make(map[string]struct{}) modelIDs := make([]string, 0, len(models)) + newModels := make(map[string]*ModelInfo, len(models)) + for _, model := range models { + if model == nil || model.ID == "" { + continue + } + if _, exists := seen[model.ID]; exists { + continue + } + seen[model.ID] = struct{}{} + modelIDs = append(modelIDs, model.ID) + newModels[model.ID] = model + } + + if len(modelIDs) == 0 { + // No models supplied; unregister existing client state if present. + r.unregisterClientInternal(clientID) + delete(r.clientModels, clientID) + delete(r.clientProviders, clientID) + misc.LogCredentialSeparator() + return + } + now := time.Now() - for _, model := range models { - modelIDs = append(modelIDs, model.ID) - - if existing, exists := r.models[model.ID]; exists { - // Model already exists, increment count - existing.Count++ - existing.LastUpdated = now - if existing.SuspendedClients == nil { - existing.SuspendedClients = make(map[string]string) - } - if provider != "" { - if existing.Providers == nil { - existing.Providers = make(map[string]int) - } - existing.Providers[provider]++ - } - log.Debugf("Incremented count for model %s, now %d clients", model.ID, existing.Count) + oldModels, hadExisting := r.clientModels[clientID] + oldProvider, hadProvider := r.clientProviders[clientID] + providerChanged := hadProvider && oldProvider != provider + if !hadExisting { + // Pure addition path. + for _, modelID := range modelIDs { + model := newModels[modelID] + r.addModelRegistration(modelID, provider, model, now) + } + r.clientModels[clientID] = modelIDs + if provider != "" { + r.clientProviders[clientID] = provider } else { - // New model, create registration - registration := &ModelRegistration{ - Info: model, - Count: 1, - LastUpdated: now, - QuotaExceededClients: make(map[string]*time.Time), - SuspendedClients: make(map[string]string), - } - if provider != "" { - registration.Providers = map[string]int{provider: 1} - } - r.models[model.ID] = registration - log.Debugf("Registered new model %s from provider %s", model.ID, clientProvider) + delete(r.clientProviders, clientID) + } + log.Debugf("Registered client %s from provider %s with %d models", clientID, clientProvider, len(modelIDs)) + misc.LogCredentialSeparator() + return + } + + oldSet := make(map[string]struct{}, len(oldModels)) + for _, id := range oldModels { + oldSet[id] = struct{}{} + } + + added := make([]string, 0) + removed := make([]string, 0) + for _, id := range modelIDs { + if _, exists := oldSet[id]; !exists { + added = append(added, id) + } + } + for _, id := range oldModels { + if _, exists := newModels[id]; !exists { + removed = append(removed, id) } } - r.clientModels[clientID] = modelIDs + // Handle provider change for overlapping models before modifications. + if providerChanged && oldProvider != "" { + for _, id := range modelIDs { + if reg, ok := r.models[id]; ok && reg.Providers != nil { + if count, okProv := reg.Providers[oldProvider]; okProv { + if count <= 1 { + delete(reg.Providers, oldProvider) + } else { + reg.Providers[oldProvider] = count - 1 + } + } + } + } + } + + // Apply removals first to keep counters accurate. + for _, id := range removed { + r.removeModelRegistration(clientID, id, oldProvider, now) + } + + // Apply additions. + for _, id := range added { + model := newModels[id] + r.addModelRegistration(id, provider, model, now) + } + + // Update metadata for models that remain associated with the client. + addedSet := make(map[string]struct{}, len(added)) + for _, id := range added { + addedSet[id] = struct{}{} + } + for _, id := range modelIDs { + model := newModels[id] + if reg, ok := r.models[id]; ok { + reg.Info = cloneModelInfo(model) + reg.LastUpdated = now + if providerChanged { + if _, newlyAdded := addedSet[id]; newlyAdded { + continue + } + if reg.Providers == nil { + reg.Providers = make(map[string]int) + } + reg.Providers[provider]++ + } + } + } + + // Update client bookkeeping. + if len(modelIDs) > 0 { + r.clientModels[clientID] = modelIDs + } if provider != "" { r.clientProviders[clientID] = provider } else { delete(r.clientProviders, clientID) } - log.Debugf("Registered client %s from provider %s with %d models", clientID, clientProvider, len(models)) - // Separator at the end of the registration block (acts as boundary to next group) + + if len(added) == 0 && len(removed) == 0 && !providerChanged { + // Only metadata (e.g., display name) changed. + misc.LogCredentialSeparator() + return + } + + log.Debugf("Reconciled client %s (provider %s) models: +%d, -%d", clientID, provider, len(added), len(removed)) misc.LogCredentialSeparator() } +func (r *ModelRegistry) addModelRegistration(modelID, provider string, model *ModelInfo, now time.Time) { + if model == nil || modelID == "" { + return + } + if existing, exists := r.models[modelID]; exists { + existing.Count++ + existing.LastUpdated = now + existing.Info = cloneModelInfo(model) + if existing.SuspendedClients == nil { + existing.SuspendedClients = make(map[string]string) + } + if provider != "" { + if existing.Providers == nil { + existing.Providers = make(map[string]int) + } + existing.Providers[provider]++ + } + log.Debugf("Incremented count for model %s, now %d clients", modelID, existing.Count) + return + } + + registration := &ModelRegistration{ + Info: cloneModelInfo(model), + Count: 1, + LastUpdated: now, + QuotaExceededClients: make(map[string]*time.Time), + SuspendedClients: make(map[string]string), + } + if provider != "" { + registration.Providers = map[string]int{provider: 1} + } + r.models[modelID] = registration + log.Debugf("Registered new model %s from provider %s", modelID, provider) +} + +func (r *ModelRegistry) removeModelRegistration(clientID, modelID, provider string, now time.Time) { + registration, exists := r.models[modelID] + if !exists { + return + } + registration.Count-- + registration.LastUpdated = now + if registration.QuotaExceededClients != nil { + delete(registration.QuotaExceededClients, clientID) + } + if registration.SuspendedClients != nil { + delete(registration.SuspendedClients, clientID) + } + if registration.Count < 0 { + registration.Count = 0 + } + if provider != "" && registration.Providers != nil { + if count, ok := registration.Providers[provider]; ok { + if count <= 1 { + delete(registration.Providers, provider) + } else { + registration.Providers[provider] = count - 1 + } + } + } + log.Debugf("Decremented count for model %s, now %d clients", modelID, registration.Count) + if registration.Count <= 0 { + delete(r.models, modelID) + log.Debugf("Removed model %s as no clients remain", modelID) + } +} + +func cloneModelInfo(model *ModelInfo) *ModelInfo { + if model == nil { + return nil + } + copy := *model + if len(model.SupportedGenerationMethods) > 0 { + copy.SupportedGenerationMethods = append([]string(nil), model.SupportedGenerationMethods...) + } + if len(model.SupportedParameters) > 0 { + copy.SupportedParameters = append([]string(nil), model.SupportedParameters...) + } + return © +} + // UnregisterClient removes a client and decrements counts for its models // Parameters: // - clientID: Unique identifier for the client to remove diff --git a/internal/watcher/watcher.go b/internal/watcher/watcher.go index b6a8b0ae..cf8bd2bf 100644 --- a/internal/watcher/watcher.go +++ b/internal/watcher/watcher.go @@ -52,6 +52,39 @@ type Watcher struct { dispatchCancel context.CancelFunc } +type stableIDGenerator struct { + counters map[string]int +} + +func newStableIDGenerator() *stableIDGenerator { + return &stableIDGenerator{counters: make(map[string]int)} +} + +func (g *stableIDGenerator) next(kind string, parts ...string) (string, string) { + if g == nil { + return kind + ":000000000000", "000000000000" + } + hasher := sha256.New() + hasher.Write([]byte(kind)) + for _, part := range parts { + trimmed := strings.TrimSpace(part) + hasher.Write([]byte{0}) + hasher.Write([]byte(trimmed)) + } + digest := hex.EncodeToString(hasher.Sum(nil)) + if len(digest) < 12 { + digest = fmt.Sprintf("%012s", digest) + } + short := digest[:12] + key := kind + ":" + short + index := g.counters[key] + g.counters[key] = index + 1 + if index > 0 { + short = fmt.Sprintf("%s-%d", short, index) + } + return fmt.Sprintf("%s:%s", kind, short), short +} + // AuthUpdateAction represents the type of change detected in auth sources. type AuthUpdateAction string @@ -640,6 +673,7 @@ func (w *Watcher) removeClient(path string) { func (w *Watcher) SnapshotCoreAuths() []*coreauth.Auth { out := make([]*coreauth.Auth, 0, 32) now := time.Now() + idGen := newStableIDGenerator() // Also synthesize auth entries for OpenAI-compatibility providers directly from config w.clientsMutex.RLock() cfg := w.config @@ -647,14 +681,18 @@ func (w *Watcher) SnapshotCoreAuths() []*coreauth.Auth { if cfg != nil { // Gemini official API keys -> synthesize auths for i := range cfg.GlAPIKey { - k := cfg.GlAPIKey[i] + k := strings.TrimSpace(cfg.GlAPIKey[i]) + if k == "" { + continue + } + id, token := idGen.next("gemini:apikey", k) a := &coreauth.Auth{ - ID: fmt.Sprintf("gemini:apikey:%d", i), + ID: id, Provider: "gemini", Label: "gemini-apikey", Status: coreauth.StatusActive, Attributes: map[string]string{ - "source": fmt.Sprintf("config:gemini#%d", i), + "source": fmt.Sprintf("config:gemini[%s]", token), "api_key": k, }, CreatedAt: now, @@ -665,15 +703,20 @@ func (w *Watcher) SnapshotCoreAuths() []*coreauth.Auth { // Claude API keys -> synthesize auths for i := range cfg.ClaudeKey { ck := cfg.ClaudeKey[i] + key := strings.TrimSpace(ck.APIKey) + if key == "" { + continue + } + id, token := idGen.next("claude:apikey", key, ck.BaseURL) attrs := map[string]string{ - "source": fmt.Sprintf("config:claude#%d", i), - "api_key": ck.APIKey, + "source": fmt.Sprintf("config:claude[%s]", token), + "api_key": key, } if ck.BaseURL != "" { attrs["base_url"] = ck.BaseURL } a := &coreauth.Auth{ - ID: fmt.Sprintf("claude:apikey:%d", i), + ID: id, Provider: "claude", Label: "claude-apikey", Status: coreauth.StatusActive, @@ -686,15 +729,20 @@ func (w *Watcher) SnapshotCoreAuths() []*coreauth.Auth { // Codex API keys -> synthesize auths for i := range cfg.CodexKey { ck := cfg.CodexKey[i] + key := strings.TrimSpace(ck.APIKey) + if key == "" { + continue + } + id, token := idGen.next("codex:apikey", key, ck.BaseURL) attrs := map[string]string{ - "source": fmt.Sprintf("config:codex#%d", i), - "api_key": ck.APIKey, + "source": fmt.Sprintf("config:codex[%s]", token), + "api_key": key, } if ck.BaseURL != "" { attrs["base_url"] = ck.BaseURL } a := &coreauth.Auth{ - ID: fmt.Sprintf("codex:apikey:%d", i), + ID: id, Provider: "codex", Label: "codex-apikey", Status: coreauth.StatusActive, @@ -710,11 +758,16 @@ func (w *Watcher) SnapshotCoreAuths() []*coreauth.Auth { if providerName == "" { providerName = "openai-compatibility" } - base := compat.BaseURL + base := strings.TrimSpace(compat.BaseURL) for j := range compat.APIKeys { - key := compat.APIKeys[j] + key := strings.TrimSpace(compat.APIKeys[j]) + if key == "" { + continue + } + idKind := fmt.Sprintf("openai-compatibility:%s", providerName) + id, token := idGen.next(idKind, key, base) attrs := map[string]string{ - "source": fmt.Sprintf("config:%s#%d", compat.Name, j), + "source": fmt.Sprintf("config:%s[%s]", providerName, token), "base_url": base, "api_key": key, "compat_name": compat.Name, @@ -724,7 +777,7 @@ func (w *Watcher) SnapshotCoreAuths() []*coreauth.Auth { attrs["models_hash"] = hash } a := &coreauth.Auth{ - ID: fmt.Sprintf("openai-compatibility:%s:%d", compat.Name, j), + ID: id, Provider: providerName, Label: compat.Name, Status: coreauth.StatusActive, diff --git a/sdk/access/reconcile.go b/sdk/access/reconcile.go new file mode 100644 index 00000000..32e4072a --- /dev/null +++ b/sdk/access/reconcile.go @@ -0,0 +1,230 @@ +package access + +import ( + "reflect" + "sort" + "strings" + + "github.com/router-for-me/CLIProxyAPI/v6/internal/config" +) + +// ReconcileProviders builds the desired provider list by reusing existing providers when possible +// and creating or removing providers only when their configuration changed. It returns the final +// ordered provider slice along with the identifiers of providers that were added, updated, or +// removed compared to the previous configuration. +func ReconcileProviders(oldCfg, newCfg *config.Config, existing []Provider) (result []Provider, added, updated, removed []string, err error) { + if newCfg == nil { + return nil, nil, nil, nil, nil + } + + existingMap := make(map[string]Provider, len(existing)) + for _, provider := range existing { + if provider == nil { + continue + } + existingMap[provider.Identifier()] = provider + } + + oldCfgMap := accessProviderMap(oldCfg) + newEntries := collectProviderEntries(newCfg) + + result = make([]Provider, 0, len(newEntries)) + finalIDs := make(map[string]struct{}, len(newEntries)) + + for _, providerCfg := range newEntries { + key := providerIdentifier(providerCfg) + if key == "" { + continue + } + + if oldCfgProvider, ok := oldCfgMap[key]; ok && providerConfigEqual(oldCfgProvider, providerCfg) { + if existingProvider, ok := existingMap[key]; ok { + result = append(result, existingProvider) + finalIDs[key] = struct{}{} + continue + } + } + + provider, buildErr := buildProvider(providerCfg, newCfg) + if buildErr != nil { + return nil, nil, nil, nil, buildErr + } + if _, ok := oldCfgMap[key]; ok { + if _, existed := existingMap[key]; existed { + updated = append(updated, key) + } else { + added = append(added, key) + } + } else { + added = append(added, key) + } + result = append(result, provider) + finalIDs[key] = struct{}{} + } + + if len(result) == 0 && len(newCfg.APIKeys) > 0 { + config.SyncInlineAPIKeys(newCfg, newCfg.APIKeys) + if providerCfg := newCfg.ConfigAPIKeyProvider(); providerCfg != nil { + key := providerIdentifier(providerCfg) + if key != "" { + if oldCfgProvider, ok := oldCfgMap[key]; ok && providerConfigEqual(oldCfgProvider, providerCfg) { + if existingProvider, ok := existingMap[key]; ok { + result = append(result, existingProvider) + } else { + provider, buildErr := buildProvider(providerCfg, newCfg) + if buildErr != nil { + return nil, nil, nil, nil, buildErr + } + if _, existed := existingMap[key]; existed { + updated = append(updated, key) + } else { + added = append(added, key) + } + result = append(result, provider) + } + } else { + provider, buildErr := buildProvider(providerCfg, newCfg) + if buildErr != nil { + return nil, nil, nil, nil, buildErr + } + if _, ok := oldCfgMap[key]; ok { + if _, existed := existingMap[key]; existed { + updated = append(updated, key) + } else { + added = append(added, key) + } + } else { + added = append(added, key) + } + result = append(result, provider) + } + finalIDs[key] = struct{}{} + } + } + } + + removedSet := make(map[string]struct{}) + for id := range existingMap { + if _, ok := finalIDs[id]; !ok { + removedSet[id] = struct{}{} + } + } + + removed = make([]string, 0, len(removedSet)) + for id := range removedSet { + removed = append(removed, id) + } + + sort.Strings(added) + sort.Strings(updated) + sort.Strings(removed) + + return result, added, updated, removed, nil +} + +func accessProviderMap(cfg *config.Config) map[string]*config.AccessProvider { + result := make(map[string]*config.AccessProvider) + if cfg == nil { + return result + } + for i := range cfg.Access.Providers { + providerCfg := &cfg.Access.Providers[i] + if providerCfg.Type == "" { + continue + } + key := providerIdentifier(providerCfg) + if key == "" { + continue + } + result[key] = providerCfg + } + if len(result) == 0 && len(cfg.APIKeys) > 0 { + if provider := cfg.ConfigAPIKeyProvider(); provider != nil { + if key := providerIdentifier(provider); key != "" { + result[key] = provider + } + } + } + return result +} + +func collectProviderEntries(cfg *config.Config) []*config.AccessProvider { + entries := make([]*config.AccessProvider, 0, len(cfg.Access.Providers)) + if cfg == nil { + return entries + } + for i := range cfg.Access.Providers { + providerCfg := &cfg.Access.Providers[i] + if providerCfg.Type == "" { + continue + } + if key := providerIdentifier(providerCfg); key != "" { + entries = append(entries, providerCfg) + } + } + return entries +} + +func providerIdentifier(provider *config.AccessProvider) string { + if provider == nil { + return "" + } + if name := strings.TrimSpace(provider.Name); name != "" { + return name + } + typ := strings.TrimSpace(provider.Type) + if typ == "" { + return "" + } + if strings.EqualFold(typ, config.AccessProviderTypeConfigAPIKey) { + return config.DefaultAccessProviderName + } + return typ +} + +func providerConfigEqual(a, b *config.AccessProvider) bool { + if a == nil || b == nil { + return a == nil && b == nil + } + if !strings.EqualFold(strings.TrimSpace(a.Type), strings.TrimSpace(b.Type)) { + return false + } + if strings.TrimSpace(a.SDK) != strings.TrimSpace(b.SDK) { + return false + } + if !stringSetEqual(a.APIKeys, b.APIKeys) { + return false + } + if len(a.Config) != len(b.Config) { + return false + } + if len(a.Config) > 0 && !reflect.DeepEqual(a.Config, b.Config) { + return false + } + return true +} + +func stringSetEqual(a, b []string) bool { + if len(a) != len(b) { + return false + } + if len(a) == 0 { + return true + } + seen := make(map[string]int, len(a)) + for _, val := range a { + seen[val]++ + } + for _, val := range b { + count := seen[val] + if count == 0 { + return false + } + if count == 1 { + delete(seen, val) + } else { + seen[val] = count - 1 + } + } + return len(seen) == 0 +} diff --git a/sdk/cliproxy/service.go b/sdk/cliproxy/service.go index dba2cfda..7c96abe7 100644 --- a/sdk/cliproxy/service.go +++ b/sdk/cliproxy/service.go @@ -110,12 +110,23 @@ func (s *Service) refreshAccessProviders(cfg *config.Config) { if s == nil || s.accessManager == nil || cfg == nil { return } - providers, err := sdkaccess.BuildProviders(cfg) + s.cfgMu.RLock() + oldCfg := s.cfg + s.cfgMu.RUnlock() + + existing := s.accessManager.Providers() + providers, added, updated, removed, err := sdkaccess.ReconcileProviders(oldCfg, cfg, existing) if err != nil { - log.Errorf("failed to rebuild request auth providers: %v", err) + log.Errorf("failed to reconcile request auth providers: %v", err) return } s.accessManager.SetProviders(providers) + if len(added)+len(updated)+len(removed) > 0 { + log.Infof("request auth providers reconciled (added=%d updated=%d removed=%d)", len(added), len(updated), len(removed)) + log.Debugf("provider changes details - added=%v updated=%v removed=%v", added, updated, removed) + } else { + log.Debug("request auth providers unchanged after config reload") + } } func (s *Service) ensureAuthUpdateQueue(ctx context.Context) {