296 lines
8.1 KiB
Go
296 lines
8.1 KiB
Go
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
|
|
}
|