diff --git a/framework/configstore/rdb.go b/framework/configstore/rdb.go index 472400c7f..ce6a335d7 100644 --- a/framework/configstore/rdb.go +++ b/framework/configstore/rdb.go @@ -1949,6 +1949,45 @@ func (s *RDBConfigStore) UpdateProxyConfig(ctx context.Context, config *tables.G }).Error } +// GetRestartRequiredConfig retrieves the restart required configuration from the database. +func (s *RDBConfigStore) GetRestartRequiredConfig(ctx context.Context) (*tables.RestartRequiredConfig, error) { + var configEntry tables.TableGovernanceConfig + if err := s.db.WithContext(ctx).First(&configEntry, "key = ?", tables.ConfigRestartRequiredKey).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + return nil, err + } + if configEntry.Value == "" { + return nil, nil + } + var restartConfig tables.RestartRequiredConfig + if err := json.Unmarshal([]byte(configEntry.Value), &restartConfig); err != nil { + return nil, fmt.Errorf("failed to unmarshal restart required config: %w", err) + } + return &restartConfig, nil +} + +// SetRestartRequiredConfig sets the restart required configuration in the database. +func (s *RDBConfigStore) SetRestartRequiredConfig(ctx context.Context, config *tables.RestartRequiredConfig) error { + configJSON, err := json.Marshal(config) + if err != nil { + return fmt.Errorf("failed to marshal restart required config: %w", err) + } + return s.db.WithContext(ctx).Save(&tables.TableGovernanceConfig{ + Key: tables.ConfigRestartRequiredKey, + Value: string(configJSON), + }).Error +} + +// ClearRestartRequiredConfig clears the restart required configuration in the database. +func (s *RDBConfigStore) ClearRestartRequiredConfig(ctx context.Context) error { + return s.db.WithContext(ctx).Save(&tables.TableGovernanceConfig{ + Key: tables.ConfigRestartRequiredKey, + Value: `{"required":false,"reason":""}`, + }).Error +} + // GetSession retrieves a session from the database. func (s *RDBConfigStore) GetSession(ctx context.Context, token string) (*tables.SessionsTable, error) { var session tables.SessionsTable diff --git a/framework/configstore/store.go b/framework/configstore/store.go index 04196c03f..2585e1971 100644 --- a/framework/configstore/store.go +++ b/framework/configstore/store.go @@ -123,6 +123,11 @@ type ConfigStore interface { GetProxyConfig(ctx context.Context) (*tables.GlobalProxyConfig, error) UpdateProxyConfig(ctx context.Context, config *tables.GlobalProxyConfig) error + // Restart required config CRUD + GetRestartRequiredConfig(ctx context.Context) (*tables.RestartRequiredConfig, error) + SetRestartRequiredConfig(ctx context.Context, config *tables.RestartRequiredConfig) error + ClearRestartRequiredConfig(ctx context.Context) error + // Session CRUD GetSession(ctx context.Context, token string) (*tables.SessionsTable, error) CreateSession(ctx context.Context, session *tables.SessionsTable) error diff --git a/framework/configstore/tables/config.go b/framework/configstore/tables/config.go index cd3cdf82a..44a1fe4cd 100644 --- a/framework/configstore/tables/config.go +++ b/framework/configstore/tables/config.go @@ -8,8 +8,16 @@ const ( ConfigIsAuthEnabledKey = "is_auth_enabled" ConfigDisableAuthOnInferenceKey = "disable_auth_on_inference" ConfigProxyKey = "proxy_config" + ConfigRestartRequiredKey = "restart_required" ) +// RestartRequiredConfig represents the restart required configuration +// This is set when a config change requires a server restart to take effect +type RestartRequiredConfig struct { + Required bool `json:"required"` + Reason string `json:"reason,omitempty"` +} + // GlobalProxyConfig represents the global proxy configuration diff --git a/transports/bifrost-http/handlers/config.go b/transports/bifrost-http/handlers/config.go index b7669f2be..175567689 100644 --- a/transports/bifrost-http/handlers/config.go +++ b/transports/bifrost-http/handlers/config.go @@ -7,6 +7,7 @@ import ( "fmt" "net/http" "slices" + "strings" "time" "github.com/fasthttp/router" @@ -156,6 +157,13 @@ func (h *ConfigHandler) getConfig(ctx *fasthttp.RequestCtx) { } mapConfig["proxy_config"] = proxyConfig } + // Fetching restart required config + restartConfig, err := h.store.ConfigStore.GetRestartRequiredConfig(ctx) + if err != nil { + logger.Warn(fmt.Sprintf("failed to get restart required config from store: %v", err)) + } else if restartConfig != nil { + mapConfig["restart_required"] = restartConfig + } } SendJSON(ctx, mapConfig) } @@ -209,6 +217,7 @@ func (h *ConfigHandler) updateConfig(ctx *fasthttp.RequestCtx) { updatedConfig := currentConfig shouldReloadTelemetryPlugin := false + var restartReasons []string if payload.ClientConfig.DropExcessRequests != currentConfig.DropExcessRequests { h.configManager.UpdateDropExcessRequests(ctx, payload.ClientConfig.DropExcessRequests) @@ -218,19 +227,42 @@ func (h *ConfigHandler) updateConfig(ctx *fasthttp.RequestCtx) { if !slices.Equal(payload.ClientConfig.PrometheusLabels, currentConfig.PrometheusLabels) { updatedConfig.PrometheusLabels = payload.ClientConfig.PrometheusLabels shouldReloadTelemetryPlugin = true + restartReasons = append(restartReasons, "Prometheus labels") } if !slices.Equal(payload.ClientConfig.AllowedOrigins, currentConfig.AllowedOrigins) { updatedConfig.AllowedOrigins = payload.ClientConfig.AllowedOrigins + restartReasons = append(restartReasons, "Allowed origins") } + if payload.ClientConfig.InitialPoolSize != currentConfig.InitialPoolSize { + restartReasons = append(restartReasons, "Initial pool size") + } updatedConfig.InitialPoolSize = payload.ClientConfig.InitialPoolSize + + if payload.ClientConfig.EnableLogging != currentConfig.EnableLogging { + restartReasons = append(restartReasons, "Logging enabled") + } updatedConfig.EnableLogging = payload.ClientConfig.EnableLogging + + if payload.ClientConfig.DisableContentLogging != currentConfig.DisableContentLogging { + restartReasons = append(restartReasons, "Content logging") + } updatedConfig.DisableContentLogging = payload.ClientConfig.DisableContentLogging + + if payload.ClientConfig.EnableGovernance != currentConfig.EnableGovernance { + restartReasons = append(restartReasons, "Governance enabled") + } updatedConfig.EnableGovernance = payload.ClientConfig.EnableGovernance + updatedConfig.EnforceGovernanceHeader = payload.ClientConfig.EnforceGovernanceHeader updatedConfig.AllowDirectKeys = payload.ClientConfig.AllowDirectKeys + + if payload.ClientConfig.MaxRequestBodySizeMB != currentConfig.MaxRequestBodySizeMB { + restartReasons = append(restartReasons, "Max request body size") + } updatedConfig.MaxRequestBodySizeMB = payload.ClientConfig.MaxRequestBodySizeMB + updatedConfig.EnableLiteLLMFallbacks = payload.ClientConfig.EnableLiteLLMFallbacks // Validate LogRetentionDays @@ -337,7 +369,7 @@ func (h *ConfigHandler) updateConfig(ctx *fasthttp.RequestCtx) { // } } // Checking auth config and trying to update if required - if payload.AuthConfig != nil && payload.AuthConfig.IsEnabled { + if payload.AuthConfig != nil { // Getting current governance config authConfig, err := h.store.ConfigStore.GetAuthConfig(ctx) if err != nil { @@ -347,29 +379,52 @@ func (h *ConfigHandler) updateConfig(ctx *fasthttp.RequestCtx) { return } } - if authConfig == nil && payload.AuthConfig.IsEnabled && (payload.AuthConfig.AdminUserName == "" || payload.AuthConfig.AdminPassword == "") { - SendError(ctx, fasthttp.StatusBadRequest, "auth username and password must be provided") - return + + // Check if auth config has changed (for restart required flag) + authChanged := false + if authConfig == nil { + // No existing config, any enabled state is a change + if payload.AuthConfig.IsEnabled { + authChanged = true + } + } else { + // Compare with existing config + if payload.AuthConfig.IsEnabled != authConfig.IsEnabled || + payload.AuthConfig.AdminUserName != authConfig.AdminUserName || + payload.AuthConfig.DisableAuthOnInference != authConfig.DisableAuthOnInference || + (payload.AuthConfig.AdminPassword != "" && payload.AuthConfig.AdminPassword != "") { + authChanged = true + } } - // Fetching current Auth config - if payload.AuthConfig.AdminUserName != "" { - if payload.AuthConfig.AdminPassword == "" { - if authConfig == nil || authConfig.AdminPassword == "" { - SendError(ctx, fasthttp.StatusBadRequest, "auth password must be provided") - return - } - // Assuming that password hasn't been changed - payload.AuthConfig.AdminPassword = authConfig.AdminPassword - } else { - // Password has been changed - // We will hash the password - hashedPassword, err := encrypt.Hash(payload.AuthConfig.AdminPassword) - if err != nil { - logger.Warn(fmt.Sprintf("failed to hash password: %v", err)) - SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("failed to hash password: %v", err)) - return + if authChanged { + restartReasons = append(restartReasons, "Authentication") + } + + if payload.AuthConfig.IsEnabled { + if authConfig == nil && (payload.AuthConfig.AdminUserName == "" || payload.AuthConfig.AdminPassword == "") { + SendError(ctx, fasthttp.StatusBadRequest, "auth username and password must be provided") + return + } + // Fetching current Auth config + if payload.AuthConfig.AdminUserName != "" { + if payload.AuthConfig.AdminPassword == "" { + if authConfig == nil || authConfig.AdminPassword == "" { + SendError(ctx, fasthttp.StatusBadRequest, "auth password must be provided") + return + } + // Assuming that password hasn't been changed + payload.AuthConfig.AdminPassword = authConfig.AdminPassword + } else { + // Password has been changed + // We will hash the password + hashedPassword, err := encrypt.Hash(payload.AuthConfig.AdminPassword) + if err != nil { + logger.Warn(fmt.Sprintf("failed to hash password: %v", err)) + SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("failed to hash password: %v", err)) + return + } + payload.AuthConfig.AdminPassword = string(hashedPassword) } - payload.AuthConfig.AdminPassword = string(hashedPassword) } } err = h.configManager.UpdateAuthConfig(ctx, payload.AuthConfig) @@ -379,6 +434,18 @@ func (h *ConfigHandler) updateConfig(ctx *fasthttp.RequestCtx) { return } } + + // Set restart required flag if any restart-requiring configs changed + if len(restartReasons) > 0 { + reason := fmt.Sprintf("%s settings have been updated. A restart is required for changes to take full effect.", strings.Join(restartReasons, ", ")) + if err := h.store.ConfigStore.SetRestartRequiredConfig(ctx, &configstoreTables.RestartRequiredConfig{ + Required: true, + Reason: reason, + }); err != nil { + logger.Warn(fmt.Sprintf("failed to set restart required config: %v", err)) + } + } + ctx.SetStatusCode(fasthttp.StatusOK) SendJSON(ctx, map[string]any{ "status": "success", @@ -530,6 +597,14 @@ func (h *ConfigHandler) updateProxyConfig(ctx *fasthttp.RequestCtx) { return } + // Set restart required flag for proxy config changes + if err := h.store.ConfigStore.SetRestartRequiredConfig(ctx, &configstoreTables.RestartRequiredConfig{ + Required: true, + Reason: "Proxy configuration has been updated. A restart is required for all changes to take full effect.", + }); err != nil { + logger.Warn(fmt.Sprintf("failed to set restart required config: %v", err)) + } + ctx.SetStatusCode(fasthttp.StatusOK) SendJSON(ctx, map[string]any{ "status": "success", diff --git a/transports/bifrost-http/lib/config.go b/transports/bifrost-http/lib/config.go index b5a0fff8b..83d9728db 100644 --- a/transports/bifrost-http/lib/config.go +++ b/transports/bifrost-http/lib/config.go @@ -411,6 +411,10 @@ func initStoresFromFile(ctx context.Context, config *Config, configData *ConfigD return err } logger.Info("config store initialized") + // Clear restart required flag on server startup + if err = config.ConfigStore.ClearRestartRequiredConfig(ctx); err != nil { + logger.Warn("failed to clear restart required config: %v", err) + } } // Initialize log store @@ -1522,6 +1526,11 @@ func loadConfigFromDefaults(ctx context.Context, config *Config, configDBPath, l return nil, err } + // Clear restart required flag on server startup + if err = config.ConfigStore.ClearRestartRequiredConfig(ctx); err != nil { + logger.Warn("failed to clear restart required config: %v", err) + } + // Load or create default client config if err = loadDefaultClientConfig(ctx, config); err != nil { return nil, err diff --git a/ui/app/workspace/config/views/pricingConfigView.tsx b/ui/app/workspace/config/views/pricingConfigView.tsx index 3d126b324..d23c302f5 100644 --- a/ui/app/workspace/config/views/pricingConfigView.tsx +++ b/ui/app/workspace/config/views/pricingConfigView.tsx @@ -89,7 +89,7 @@ export default function PricingConfigView() {
-
+