package cliproxy import ( "context" "strings" "time" "github.com/router-for-me/CLIProxyAPI/v7/internal/watcher/synthesizer" coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" "github.com/router-for-me/CLIProxyAPI/v7/sdk/config" log "github.com/sirupsen/logrus" ) func (s *Service) applyConfigUpdate(newCfg *config.Config) { s.applyConfigUpdateWithAuthSynthesis(context.Background(), newCfg, true) } func (s *Service) applyWatcherConfigUpdate(newCfg *config.Config) { s.applyConfigUpdateWithAuthSynthesis(context.Background(), newCfg, false) } type configCommit struct { cfg *config.Config sequence uint64 } type routingRuntimeState struct { strategy string sessionAffinity bool sessionAffinityTTL time.Duration } func normalizedRoutingRuntimeState(cfg *config.Config) routingRuntimeState { state := routingRuntimeState{ strategy: "round-robin", sessionAffinityTTL: time.Hour, } if cfg == nil { return state } switch strings.ToLower(strings.TrimSpace(cfg.Routing.Strategy)) { case "weighted-round-robin", "weightedroundrobin", "wrr": state.strategy = "weighted-round-robin" case "fill-first", "fillfirst", "ff": state.strategy = "fill-first" } state.sessionAffinity = cfg.Routing.SessionAffinity if ttl := strings.TrimSpace(cfg.Routing.SessionAffinityTTL); ttl != "" { if parsed, errParse := time.ParseDuration(ttl); errParse == nil && parsed > 0 { state.sessionAffinityTTL = parsed } } return state } func newRoutingSelector(state routingRuntimeState) coreauth.Selector { var selector coreauth.Selector switch state.strategy { case "weighted-round-robin": selector = &coreauth.WeightedRoundRobinSelector{} case "fill-first": selector = &coreauth.FillFirstSelector{} default: selector = &coreauth.RoundRobinSelector{} } if state.sessionAffinity { selector = coreauth.NewSessionAffinitySelectorWithConfig(coreauth.SessionAffinityConfig{ Fallback: selector, TTL: state.sessionAffinityTTL, }) } return selector } func (s *Service) applyConfigUpdateWithAuthSynthesis(ctx context.Context, newCfg *config.Config, synthesizeConfigAuths bool) bool { commit := s.commitConfigUpdate(newCfg) if commit.cfg == nil { return false } return s.applyConfigRuntime(ctx, commit, synthesizeConfigAuths) } // commitConfigUpdate applies only in-memory configuration state. Runtime work that // may block on plugins, models, storage, or networking is deliberately deferred. func (s *Service) commitConfigUpdate(newCfg *config.Config) configCommit { if s == nil { return configCommit{} } s.configUpdateMu.Lock() defer s.configUpdateMu.Unlock() if newCfg == nil { s.cfgMu.RLock() newCfg = s.cfg s.cfgMu.RUnlock() } if newCfg == nil { return configCommit{} } if errValidate := newCfg.ValidateCredentialWeights(); errValidate != nil { log.WithError(errValidate).Warn("rejected config update with invalid credential weights") return configCommit{} } s.cfgMu.Lock() s.cfg = newCfg s.cfgMu.Unlock() s.configSequence++ return configCommit{cfg: newCfg, sequence: s.configSequence} } func (s *Service) configCommitCurrent(commit configCommit) bool { if s == nil || commit.sequence == 0 { return false } s.configUpdateMu.Lock() current := s.configSequence == commit.sequence s.configUpdateMu.Unlock() return current } func (s *Service) applyConfigRuntime(ctx context.Context, commit configCommit, synthesizeConfigAuths bool) bool { cfg := commit.cfg if s == nil || cfg == nil { return false } s.configRuntimeMu.Lock() defer s.configRuntimeMu.Unlock() if !s.configCommitCurrent(commit) { return false } if ctx == nil { ctx = context.Background() } if errContext := ctx.Err(); errContext != nil { return false } if !s.applyManagerConfig(ctx, commit) { return false } if errContext := ctx.Err(); errContext != nil { return false } if !s.applyPprofConfigContext(ctx, cfg) { return false } if errContext := ctx.Err(); errContext != nil { return false } if !s.updateServerClientsContext(ctx, cfg) { return false } if errContext := ctx.Err(); errContext != nil { return false } registrationCtx := coreauth.WithSkipPersist(ctx) s.syncPluginRuntimeConfigForConfig(registrationCtx, cfg) if errContext := ctx.Err(); errContext != nil { return false } var auths []*coreauth.Auth if s.coreManager != nil { auths = s.coreManager.List() } s.registerAvailableExecutors(registrationCtx, executorRegistrationOptions{ includeBaseline: cfg.Home.Enabled, forceReplaceAuths: true, auths: auths, }) if errContext := ctx.Err(); errContext != nil { return false } if synthesizeConfigAuths { s.registerConfigAPIKeyAuths(registrationCtx, cfg) } if errContext := ctx.Err(); errContext != nil { return false } if s.coreManager != nil && !cfg.Home.Enabled && cfg.SaveCooldownStatus { if errRestoreCooldown := s.coreManager.RestoreCooldownStates(registrationCtx); errRestoreCooldown != nil && ctx.Err() == nil { log.Warnf("failed to restore cooldown state after config update: %v", errRestoreCooldown) } } if errContext := ctx.Err(); errContext != nil { return false } s.syncPluginModelRuntime(registrationCtx) return ctx.Err() == nil } func (s *Service) applyManagerConfig(ctx context.Context, commit configCommit) bool { if s == nil || s.coreManager == nil || commit.cfg == nil { return s != nil && commit.cfg != nil } if ctx == nil { ctx = context.Background() } if errContext := ctx.Err(); errContext != nil { return false } routingState := normalizedRoutingRuntimeState(commit.cfg) if s.appliedRoutingState == nil || *s.appliedRoutingState != routingState { s.coreManager.SetSelector(newRoutingSelector(routingState)) s.appliedRoutingState = &routingState } s.applyRetryConfig(commit.cfg) store := s.resolveCooldownStateStore(commit.cfg) if !s.coreManager.ApplyConfigWithCooldownStateStore(ctx, commit.cfg, store) { return false } s.coreManager.SetOAuthModelAlias(commit.cfg.OAuthModelAlias) return true } func (s *Service) updateServerClientsContext(ctx context.Context, cfg *config.Config) bool { if s == nil || cfg == nil || (ctx != nil && ctx.Err() != nil) { return false } if s.updateServerClientsContextFn != nil { return s.updateServerClientsContextFn(ctx, cfg) } if s.server == nil { return true } return s.server.UpdateClientsContext(ctx, cfg) } func (s *Service) reloadConfigFromWatcher() bool { if s == nil || s.watcher == nil { return false } return s.watcher.ReloadConfigIfChanged() } func (s *Service) registerConfigAPIKeyAuths(ctx context.Context, cfg *config.Config) { if s == nil || s.coreManager == nil || cfg == nil { return } if ctx == nil { ctx = context.Background() } configSynth := synthesizer.NewConfigSynthesizer() auths, errSynthesize := configSynth.Synthesize(&synthesizer.SynthesisContext{ Config: cfg, Now: time.Now(), IDGenerator: synthesizer.NewStableIDGenerator(), }) if errSynthesize != nil { log.Warnf("failed to synthesize config API key auths: %v", errSynthesize) return } registrationCtx := coreauth.WithDeferredAPIKeyModelAliasRebuild(ctx) tasks := make([]modelRegistrationTask, 0, len(auths)) needsAliasRebuild := false for _, auth := range auths { if !coreauth.IsConfigAPIKeyAuth(auth) { continue } prepared := s.prepareCoreAuthForModelRegistration(registrationCtx, auth) if prepared == nil { continue } needsAliasRebuild = true authForRegistration := prepared tasks = append(tasks, modelRegistrationTask{ phase: modelRegistrationPhaseConfigAPIKey, category: modelRegistrationCategory(authForRegistration), run: func(compatCache *openAICompatibilityRegistrationCache) { s.completeModelRegistrationForAuthWithCache(registrationCtx, authForRegistration, compatCache) }, }) } if needsAliasRebuild { s.coreManager.RefreshAPIKeyModelAlias() } s.runModelRegistrationTasks(registrationCtx, tasks) } func forceHomeRuntimeConfig(cfg *config.Config) { if cfg == nil { return } cfg.APIKeys = nil cfg.UsageStatisticsEnabled = true cfg.DisableCooling = true cfg.SaveCooldownStatus = false cfg.WebsocketAuth = false cfg.RemoteManagement.AllowRemote = false cfg.RemoteManagement.DisableControlPanel = true cfg.Plugins.StoreAuth = nil }