mirror of
https://github.com/mudler/LocalAI.git
synced 2026-03-02 05:37:53 -05:00
* feat: openresponses Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * Add ttl settings, fix tests Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix: register cors middleware by default Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * satisfy schema Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * Logitbias and logprobs Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * Add grammar Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * SSE compliance Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * tool JSON conversion Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * support background mode Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * swagger Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * drop code. This is handled in the handler Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * Small refactorings Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * background mode for MCP Signed-off-by: Ettore Di Giacinto <mudler@localai.io> --------- Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
242 lines
8.1 KiB
Go
242 lines
8.1 KiB
Go
package localai
|
|
|
|
import (
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/labstack/echo/v4"
|
|
"github.com/mudler/LocalAI/core/application"
|
|
"github.com/mudler/LocalAI/core/config"
|
|
"github.com/mudler/LocalAI/core/http/endpoints/openresponses"
|
|
"github.com/mudler/LocalAI/core/p2p"
|
|
"github.com/mudler/LocalAI/core/schema"
|
|
"github.com/mudler/xlog"
|
|
)
|
|
|
|
// GetSettingsEndpoint returns current settings with precedence (env > file > defaults)
|
|
func GetSettingsEndpoint(app *application.Application) echo.HandlerFunc {
|
|
return func(c echo.Context) error {
|
|
appConfig := app.ApplicationConfig()
|
|
settings := appConfig.ToRuntimeSettings()
|
|
return c.JSON(http.StatusOK, settings)
|
|
}
|
|
}
|
|
|
|
// UpdateSettingsEndpoint updates settings, saves to file, and applies immediately
|
|
func UpdateSettingsEndpoint(app *application.Application) echo.HandlerFunc {
|
|
return func(c echo.Context) error {
|
|
appConfig := app.ApplicationConfig()
|
|
startupConfig := app.StartupConfig()
|
|
|
|
if startupConfig == nil {
|
|
startupConfig = appConfig
|
|
}
|
|
|
|
body, err := io.ReadAll(c.Request().Body)
|
|
if err != nil {
|
|
return c.JSON(http.StatusBadRequest, schema.SettingsResponse{
|
|
Success: false,
|
|
Error: "Failed to read request body: " + err.Error(),
|
|
})
|
|
}
|
|
|
|
var settings config.RuntimeSettings
|
|
if err := json.Unmarshal(body, &settings); err != nil {
|
|
return c.JSON(http.StatusBadRequest, schema.SettingsResponse{
|
|
Success: false,
|
|
Error: "Failed to parse JSON: " + err.Error(),
|
|
})
|
|
}
|
|
|
|
// Validate timeouts if provided
|
|
if settings.WatchdogIdleTimeout != nil {
|
|
if _, err := time.ParseDuration(*settings.WatchdogIdleTimeout); err != nil {
|
|
return c.JSON(http.StatusBadRequest, schema.SettingsResponse{
|
|
Success: false,
|
|
Error: "Invalid watchdog_idle_timeout format: " + err.Error(),
|
|
})
|
|
}
|
|
}
|
|
if settings.WatchdogBusyTimeout != nil {
|
|
if _, err := time.ParseDuration(*settings.WatchdogBusyTimeout); err != nil {
|
|
return c.JSON(http.StatusBadRequest, schema.SettingsResponse{
|
|
Success: false,
|
|
Error: "Invalid watchdog_busy_timeout format: " + err.Error(),
|
|
})
|
|
}
|
|
}
|
|
if settings.WatchdogInterval != nil {
|
|
if _, err := time.ParseDuration(*settings.WatchdogInterval); err != nil {
|
|
return c.JSON(http.StatusBadRequest, schema.SettingsResponse{
|
|
Success: false,
|
|
Error: "Invalid watchdog_interval format: " + err.Error(),
|
|
})
|
|
}
|
|
}
|
|
if settings.LRUEvictionRetryInterval != nil {
|
|
if _, err := time.ParseDuration(*settings.LRUEvictionRetryInterval); err != nil {
|
|
return c.JSON(http.StatusBadRequest, schema.SettingsResponse{
|
|
Success: false,
|
|
Error: "Invalid lru_eviction_retry_interval format: " + err.Error(),
|
|
})
|
|
}
|
|
}
|
|
if settings.OpenResponsesStoreTTL != nil {
|
|
if *settings.OpenResponsesStoreTTL != "0" && *settings.OpenResponsesStoreTTL != "" {
|
|
if _, err := time.ParseDuration(*settings.OpenResponsesStoreTTL); err != nil {
|
|
return c.JSON(http.StatusBadRequest, schema.SettingsResponse{
|
|
Success: false,
|
|
Error: "Invalid open_responses_store_ttl format: " + err.Error(),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// Save to file
|
|
if appConfig.DynamicConfigsDir == "" {
|
|
return c.JSON(http.StatusBadRequest, schema.SettingsResponse{
|
|
Success: false,
|
|
Error: "DynamicConfigsDir is not set",
|
|
})
|
|
}
|
|
|
|
settingsFile := filepath.Join(appConfig.DynamicConfigsDir, "runtime_settings.json")
|
|
settingsJSON, err := json.MarshalIndent(settings, "", " ")
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, schema.SettingsResponse{
|
|
Success: false,
|
|
Error: "Failed to marshal settings: " + err.Error(),
|
|
})
|
|
}
|
|
|
|
if err := os.WriteFile(settingsFile, settingsJSON, 0600); err != nil {
|
|
return c.JSON(http.StatusInternalServerError, schema.SettingsResponse{
|
|
Success: false,
|
|
Error: "Failed to write settings file: " + err.Error(),
|
|
})
|
|
}
|
|
|
|
// Apply settings using centralized method
|
|
watchdogChanged := appConfig.ApplyRuntimeSettings(&settings)
|
|
|
|
// Handle API keys specially (merge with startup keys)
|
|
if settings.ApiKeys != nil {
|
|
envKeys := startupConfig.ApiKeys
|
|
runtimeKeys := *settings.ApiKeys
|
|
appConfig.ApiKeys = append(envKeys, runtimeKeys...)
|
|
}
|
|
|
|
// Update watchdog dynamically for settings that don't require restart
|
|
if settings.ForceEvictionWhenBusy != nil {
|
|
currentWD := app.ModelLoader().GetWatchDog()
|
|
if currentWD != nil {
|
|
currentWD.SetForceEvictionWhenBusy(*settings.ForceEvictionWhenBusy)
|
|
xlog.Info("Updated watchdog force eviction when busy setting", "forceEvictionWhenBusy", *settings.ForceEvictionWhenBusy)
|
|
}
|
|
}
|
|
|
|
// Update ModelLoader LRU eviction retry settings dynamically
|
|
maxRetries := appConfig.LRUEvictionMaxRetries
|
|
retryInterval := appConfig.LRUEvictionRetryInterval
|
|
if settings.LRUEvictionMaxRetries != nil {
|
|
maxRetries = *settings.LRUEvictionMaxRetries
|
|
}
|
|
if settings.LRUEvictionRetryInterval != nil {
|
|
if dur, err := time.ParseDuration(*settings.LRUEvictionRetryInterval); err == nil {
|
|
retryInterval = dur
|
|
}
|
|
}
|
|
if settings.LRUEvictionMaxRetries != nil || settings.LRUEvictionRetryInterval != nil {
|
|
app.ModelLoader().SetLRUEvictionRetrySettings(maxRetries, retryInterval)
|
|
xlog.Info("Updated LRU eviction retry settings", "maxRetries", maxRetries, "retryInterval", retryInterval)
|
|
}
|
|
|
|
// Update Open Responses store TTL dynamically
|
|
if settings.OpenResponsesStoreTTL != nil {
|
|
ttl := time.Duration(0)
|
|
if *settings.OpenResponsesStoreTTL != "0" && *settings.OpenResponsesStoreTTL != "" {
|
|
if dur, err := time.ParseDuration(*settings.OpenResponsesStoreTTL); err == nil {
|
|
ttl = dur
|
|
} else {
|
|
xlog.Warn("Invalid Open Responses store TTL format", "ttl", *settings.OpenResponsesStoreTTL, "error", err)
|
|
}
|
|
}
|
|
// Import the store package
|
|
store := openresponses.GetGlobalStore()
|
|
store.SetTTL(ttl)
|
|
xlog.Info("Updated Open Responses store TTL", "ttl", ttl)
|
|
}
|
|
|
|
// Check if agent job retention changed
|
|
agentJobChanged := settings.AgentJobRetentionDays != nil
|
|
|
|
// Restart watchdog if settings changed
|
|
if watchdogChanged {
|
|
if settings.WatchdogEnabled != nil && !*settings.WatchdogEnabled {
|
|
if err := app.StopWatchdog(); err != nil {
|
|
xlog.Error("Failed to stop watchdog", "error", err)
|
|
return c.JSON(http.StatusInternalServerError, schema.SettingsResponse{
|
|
Success: false,
|
|
Error: "Settings saved but failed to stop watchdog: " + err.Error(),
|
|
})
|
|
}
|
|
} else {
|
|
if err := app.RestartWatchdog(); err != nil {
|
|
xlog.Error("Failed to restart watchdog", "error", err)
|
|
return c.JSON(http.StatusInternalServerError, schema.SettingsResponse{
|
|
Success: false,
|
|
Error: "Settings saved but failed to restart watchdog: " + err.Error(),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// Restart agent job service if retention days changed
|
|
if agentJobChanged {
|
|
if err := app.RestartAgentJobService(); err != nil {
|
|
xlog.Error("Failed to restart agent job service", "error", err)
|
|
return c.JSON(http.StatusInternalServerError, schema.SettingsResponse{
|
|
Success: false,
|
|
Error: "Settings saved but failed to restart agent job service: " + err.Error(),
|
|
})
|
|
}
|
|
}
|
|
|
|
// Restart P2P if P2P settings changed
|
|
p2pChanged := settings.P2PToken != nil || settings.P2PNetworkID != nil || settings.Federated != nil
|
|
if p2pChanged {
|
|
if settings.P2PToken != nil && *settings.P2PToken == "" {
|
|
if err := app.StopP2P(); err != nil {
|
|
xlog.Error("Failed to stop P2P", "error", err)
|
|
return c.JSON(http.StatusInternalServerError, schema.SettingsResponse{
|
|
Success: false,
|
|
Error: "Settings saved but failed to stop P2P: " + err.Error(),
|
|
})
|
|
}
|
|
} else {
|
|
if settings.P2PToken != nil && *settings.P2PToken == "0" {
|
|
token := p2p.GenerateToken(60, 60)
|
|
settings.P2PToken = &token
|
|
appConfig.P2PToken = token
|
|
}
|
|
if err := app.RestartP2P(); err != nil {
|
|
xlog.Error("Failed to restart P2P", "error", err)
|
|
return c.JSON(http.StatusInternalServerError, schema.SettingsResponse{
|
|
Success: false,
|
|
Error: "Settings saved but failed to restart P2P: " + err.Error(),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, schema.SettingsResponse{
|
|
Success: true,
|
|
Message: "Settings updated successfully",
|
|
})
|
|
}
|
|
}
|