Skip to content

Commit 948f3bf

Browse files
committed
feat: add upgrade checker service, API endpoints, and CLI command
- UpgradeChecker background service: checks every 6h, auto-upgrades when enabled - API endpoints: GET /backends/upgrades, POST /backends/upgrades/check, POST /backends/upgrade/:name - CLI: `localai backends upgrade` command, version display in `backends list` - BackendManager interface: add UpgradeBackend and CheckUpgrades methods - Wire upgrade op through GalleryService backend handler - Distributed mode: fan-out upgrade to worker nodes via NATS
1 parent 1e083cd commit 948f3bf

12 files changed

Lines changed: 454 additions & 12 deletions

File tree

core/application/application.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ type Application struct {
3737

3838
// Distributed mode services (nil when not in distributed mode)
3939
distributed *DistributedServices
40+
41+
// Upgrade checker (background service for detecting backend upgrades)
42+
upgradeChecker *UpgradeChecker
4043
}
4144

4245
func newApplication(appConfig *config.ApplicationConfig) *Application {
@@ -79,6 +82,10 @@ func (a *Application) AgentJobService() *agentpool.AgentJobService {
7982
return a.agentJobService
8083
}
8184

85+
func (a *Application) UpgradeChecker() *UpgradeChecker {
86+
return a.upgradeChecker
87+
}
88+
8289
func (a *Application) AgentPoolService() *agentpool.AgentPoolService {
8390
return a.agentPoolService.Load()
8491
}

core/application/startup.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,13 @@ func New(opts ...config.AppOption) (*Application, error) {
231231
xlog.Error("error registering external backends", "error", err)
232232
}
233233

234+
// Start background upgrade checker for backends
235+
if len(options.BackendGalleries) > 0 {
236+
uc := NewUpgradeChecker(options, application.ModelLoader())
237+
application.upgradeChecker = uc
238+
go uc.Run(options.Context)
239+
}
240+
234241
if options.ConfigFile != "" {
235242
if err := application.ModelConfigLoader().LoadMultipleModelConfigsSingleFile(options.ConfigFile, configLoaderOpts...); err != nil {
236243
xlog.Error("error loading config file", "error", err)
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
package application
2+
3+
import (
4+
"context"
5+
"sync"
6+
"time"
7+
8+
"github.com/mudler/LocalAI/core/config"
9+
"github.com/mudler/LocalAI/core/gallery"
10+
"github.com/mudler/LocalAI/pkg/model"
11+
"github.com/mudler/LocalAI/pkg/system"
12+
"github.com/mudler/xlog"
13+
)
14+
15+
// UpgradeChecker periodically checks for backend upgrades and optionally
16+
// auto-upgrades them. It caches the last check results for API queries.
17+
type UpgradeChecker struct {
18+
appConfig *config.ApplicationConfig
19+
modelLoader *model.ModelLoader
20+
galleries []config.Gallery
21+
systemState *system.SystemState
22+
23+
checkInterval time.Duration
24+
stop chan struct{}
25+
done chan struct{}
26+
triggerCh chan struct{}
27+
28+
mu sync.RWMutex
29+
lastUpgrades map[string]gallery.UpgradeInfo
30+
lastCheckTime time.Time
31+
}
32+
33+
// NewUpgradeChecker creates a new UpgradeChecker service.
34+
func NewUpgradeChecker(appConfig *config.ApplicationConfig, ml *model.ModelLoader) *UpgradeChecker {
35+
return &UpgradeChecker{
36+
appConfig: appConfig,
37+
modelLoader: ml,
38+
galleries: appConfig.BackendGalleries,
39+
systemState: appConfig.SystemState,
40+
checkInterval: 6 * time.Hour,
41+
stop: make(chan struct{}),
42+
done: make(chan struct{}),
43+
triggerCh: make(chan struct{}, 1),
44+
lastUpgrades: make(map[string]gallery.UpgradeInfo),
45+
}
46+
}
47+
48+
// Run starts the upgrade checker loop. It waits 30 seconds after startup,
49+
// performs an initial check, then re-checks every 6 hours.
50+
func (uc *UpgradeChecker) Run(ctx context.Context) {
51+
defer close(uc.done)
52+
53+
// Initial delay: don't slow down startup
54+
select {
55+
case <-ctx.Done():
56+
return
57+
case <-uc.stop:
58+
return
59+
case <-time.After(30 * time.Second):
60+
}
61+
62+
// First check
63+
uc.runCheck(ctx)
64+
65+
// Periodic loop
66+
ticker := time.NewTicker(uc.checkInterval)
67+
defer ticker.Stop()
68+
69+
for {
70+
select {
71+
case <-ctx.Done():
72+
return
73+
case <-uc.stop:
74+
return
75+
case <-ticker.C:
76+
uc.runCheck(ctx)
77+
case <-uc.triggerCh:
78+
uc.runCheck(ctx)
79+
}
80+
}
81+
}
82+
83+
// Shutdown stops the upgrade checker loop.
84+
func (uc *UpgradeChecker) Shutdown() {
85+
close(uc.stop)
86+
<-uc.done
87+
}
88+
89+
// TriggerCheck forces an immediate upgrade check.
90+
func (uc *UpgradeChecker) TriggerCheck() {
91+
select {
92+
case uc.triggerCh <- struct{}{}:
93+
default:
94+
// Already triggered, skip
95+
}
96+
}
97+
98+
// GetAvailableUpgrades returns the cached upgrade check results.
99+
func (uc *UpgradeChecker) GetAvailableUpgrades() map[string]gallery.UpgradeInfo {
100+
uc.mu.RLock()
101+
defer uc.mu.RUnlock()
102+
103+
// Return a copy to avoid races
104+
result := make(map[string]gallery.UpgradeInfo, len(uc.lastUpgrades))
105+
for k, v := range uc.lastUpgrades {
106+
result[k] = v
107+
}
108+
return result
109+
}
110+
111+
func (uc *UpgradeChecker) runCheck(ctx context.Context) {
112+
upgrades, err := gallery.CheckBackendUpgrades(ctx, uc.galleries, uc.systemState)
113+
114+
uc.mu.Lock()
115+
uc.lastCheckTime = time.Now()
116+
if err != nil {
117+
xlog.Debug("Backend upgrade check failed", "error", err)
118+
uc.mu.Unlock()
119+
return
120+
}
121+
uc.lastUpgrades = upgrades
122+
uc.mu.Unlock()
123+
124+
if len(upgrades) == 0 {
125+
xlog.Debug("All backends up to date")
126+
return
127+
}
128+
129+
// Log available upgrades
130+
for name, info := range upgrades {
131+
if info.AvailableVersion != "" {
132+
xlog.Info("Backend upgrade available",
133+
"backend", name,
134+
"installed", info.InstalledVersion,
135+
"available", info.AvailableVersion)
136+
} else {
137+
xlog.Info("Backend upgrade available (new build)",
138+
"backend", name)
139+
}
140+
}
141+
142+
// Auto-upgrade if enabled
143+
if uc.appConfig.AutoUpgradeBackends {
144+
for name, info := range upgrades {
145+
xlog.Info("Auto-upgrading backend", "backend", name,
146+
"from", info.InstalledVersion, "to", info.AvailableVersion)
147+
if err := gallery.UpgradeBackend(ctx, uc.systemState, uc.modelLoader,
148+
uc.galleries, name, nil); err != nil {
149+
xlog.Error("Failed to auto-upgrade backend",
150+
"backend", name, "error", err)
151+
} else {
152+
xlog.Info("Backend upgraded successfully", "backend", name,
153+
"version", info.AvailableVersion)
154+
}
155+
}
156+
// Re-check to update cache after upgrades
157+
if freshUpgrades, err := gallery.CheckBackendUpgrades(ctx, uc.galleries, uc.systemState); err == nil {
158+
uc.mu.Lock()
159+
uc.lastUpgrades = freshUpgrades
160+
uc.mu.Unlock()
161+
}
162+
}
163+
}

core/cli/backends.go

Lines changed: 98 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,17 @@ type BackendsUninstall struct {
4040
BackendsCMDFlags `embed:""`
4141
}
4242

43+
type BackendsUpgrade struct {
44+
BackendArgs []string `arg:"" optional:"" name:"backends" help:"Backend names to upgrade (empty = upgrade all)"`
45+
46+
BackendsCMDFlags `embed:""`
47+
}
48+
4349
type BackendsCMD struct {
4450
List BackendsList `cmd:"" help:"List the backends available in your galleries" default:"withargs"`
4551
Install BackendsInstall `cmd:"" help:"Install a backend from the gallery"`
4652
Uninstall BackendsUninstall `cmd:"" help:"Uninstall a backend"`
53+
Upgrade BackendsUpgrade `cmd:"" help:"Upgrade backends to latest versions"`
4754
}
4855

4956
func (bl *BackendsList) Run(ctx *cliContext.Context) error {
@@ -64,11 +71,27 @@ func (bl *BackendsList) Run(ctx *cliContext.Context) error {
6471
if err != nil {
6572
return err
6673
}
74+
75+
// Check for upgrades
76+
upgrades, _ := gallery.CheckBackendUpgrades(context.Background(), galleries, systemState)
77+
6778
for _, backend := range backends {
79+
versionStr := ""
80+
if backend.Version != "" {
81+
versionStr = " v" + backend.Version
82+
}
6883
if backend.Installed {
69-
fmt.Printf(" * %s@%s (installed)\n", backend.Gallery.Name, backend.Name)
84+
if info, ok := upgrades[backend.Name]; ok {
85+
upgradeStr := info.AvailableVersion
86+
if upgradeStr == "" {
87+
upgradeStr = "new build"
88+
}
89+
fmt.Printf(" * %s@%s%s (installed, upgrade available: %s)\n", backend.Gallery.Name, backend.Name, versionStr, upgradeStr)
90+
} else {
91+
fmt.Printf(" * %s@%s%s (installed)\n", backend.Gallery.Name, backend.Name, versionStr)
92+
}
7093
} else {
71-
fmt.Printf(" - %s@%s\n", backend.Gallery.Name, backend.Name)
94+
fmt.Printf(" - %s@%s%s\n", backend.Gallery.Name, backend.Name, versionStr)
7295
}
7396
}
7497
return nil
@@ -111,6 +134,79 @@ func (bi *BackendsInstall) Run(ctx *cliContext.Context) error {
111134
return nil
112135
}
113136

137+
func (bu *BackendsUpgrade) Run(ctx *cliContext.Context) error {
138+
var galleries []config.Gallery
139+
if err := json.Unmarshal([]byte(bu.BackendGalleries), &galleries); err != nil {
140+
xlog.Error("unable to load galleries", "error", err)
141+
}
142+
143+
systemState, err := system.GetSystemState(
144+
system.WithBackendSystemPath(bu.BackendsSystemPath),
145+
system.WithBackendPath(bu.BackendsPath),
146+
)
147+
if err != nil {
148+
return err
149+
}
150+
151+
upgrades, err := gallery.CheckBackendUpgrades(context.Background(), galleries, systemState)
152+
if err != nil {
153+
return fmt.Errorf("failed to check for upgrades: %w", err)
154+
}
155+
156+
if len(upgrades) == 0 {
157+
fmt.Println("All backends are up to date.")
158+
return nil
159+
}
160+
161+
// Filter to specified backends if args given
162+
toUpgrade := upgrades
163+
if len(bu.BackendArgs) > 0 {
164+
toUpgrade = make(map[string]gallery.UpgradeInfo)
165+
for _, name := range bu.BackendArgs {
166+
if info, ok := upgrades[name]; ok {
167+
toUpgrade[name] = info
168+
} else {
169+
fmt.Printf("Backend %s: no upgrade available\n", name)
170+
}
171+
}
172+
}
173+
174+
if len(toUpgrade) == 0 {
175+
fmt.Println("No upgrades to apply.")
176+
return nil
177+
}
178+
179+
modelLoader := model.NewModelLoader(systemState)
180+
for name, info := range toUpgrade {
181+
versionStr := ""
182+
if info.AvailableVersion != "" {
183+
versionStr = " to v" + info.AvailableVersion
184+
}
185+
fmt.Printf("Upgrading %s%s...\n", name, versionStr)
186+
187+
progressBar := progressbar.NewOptions(
188+
1000,
189+
progressbar.OptionSetDescription(fmt.Sprintf("downloading %s", name)),
190+
progressbar.OptionShowBytes(false),
191+
progressbar.OptionClearOnFinish(),
192+
)
193+
progressCallback := func(fileName string, current string, total string, percentage float64) {
194+
v := int(percentage * 10)
195+
if err := progressBar.Set(v); err != nil {
196+
xlog.Error("error updating progress bar", "error", err)
197+
}
198+
}
199+
200+
if err := gallery.UpgradeBackend(context.Background(), systemState, modelLoader, galleries, name, progressCallback); err != nil {
201+
fmt.Printf("Failed to upgrade %s: %v\n", name, err)
202+
} else {
203+
fmt.Printf("Backend %s upgraded successfully\n", name)
204+
}
205+
}
206+
207+
return nil
208+
}
209+
114210
func (bu *BackendsUninstall) Run(ctx *cliContext.Context) error {
115211
for _, backendName := range bu.BackendArgs {
116212
xlog.Info("uninstalling backend", "backend", backendName)

0 commit comments

Comments
 (0)