mirror of
https://github.com/mudler/LocalAI.git
synced 2026-07-30 18:09:05 -04:00
* feat(settings): add declarative runtime-settings field registry One fieldSpec row per RuntimeSettings field, with a reflection completeness spec so a field added without a registry row is a red test instead of a silently-dropped setting (the #10845 bug class). Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude Code:claude-fable-5 * refactor(settings): drive ToRuntimeSettings/ApplyRuntimeSettings from the field registry Behavior-preserving: ~350 hand-written per-field lines become two loops over runtimeSettingsFields, gated by a To->Apply->To round-trip spec. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude Code:claude-fable-5 * feat(settings): baseline-driven startup merge for persisted runtime settings ApplyRuntimeSettingsAtStartup compares the live config against DefaultRuntimeBaseline (option-less-run defaults incl. kong-injected flag defaults) instead of per-field == 0 guards. Fixes persisted lru_eviction_max_retries, tracing_max_items, agent_job_retention_days, memory_reclaimer_threshold, galleries and autoload flags being silently ignored at boot. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude Code:claude-fable-5 * fix(settings): registry-driven startup merge, applied before consumers loadRuntimeSettingsFromFile becomes a thin wrapper over ApplyRuntimeSettingsAtStartup and runs at the top of New(), before model configs capture app-level defaults. WithThreads stops eagerly resolving 0 so a persisted thread count survives restart while LOCALAI_THREADS still wins (#10845); the physical-core fallback moves after the merge. Also: run.go now injects the memory-reclaimer threshold unconditionally so the option-less boot matches DefaultRuntimeBaseline and a UI-saved threshold survives restart. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude Code:claude-fable-5 * refactor(settings): file watcher delegates to the registry merge; shared API-key merge Manual edits to runtime_settings.json now behave like a boot-time load (env still wins) instead of the inverted diverged-from-startup guard that ignored most manual edits. MergeAPIKeys dedups env keys in one place for the endpoint and the watcher. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude Code:claude-fable-5 * docs(settings): document unified runtime-settings precedence Document the single env/CLI > runtime_settings.json > defaults rule, applied identically at boot, on POST /api/settings, and on manual file edits, plus the two known limitations (default-valued env vars are indistinguishable from unset; API-changed fields hot-apply on the next restart only). Also add a completion debug log when the watcher applies runtime_settings.json. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude Code:claude-fable-5 * test(settings): reset the global VRAM cap leaked by the round-trip spec The round-trip spec applies vram_budget=12GiB, whose post-loop hook installs a process-global default cap; without a reset every spec ordered after it runs under that phantom budget. Also drop a stale enumeration in the ApplyRuntimeSettings doc comment. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude Code:claude-fable-5 --------- Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
97 lines
4.2 KiB
Go
97 lines
4.2 KiB
Go
package config
|
|
|
|
import (
|
|
"encoding/json"
|
|
|
|
"github.com/mudler/LocalAI/pkg/vrambudget"
|
|
"github.com/mudler/LocalAI/pkg/xsysinfo"
|
|
)
|
|
|
|
// DefaultGalleriesJSON / DefaultBackendGalleriesJSON are the gallery lists
|
|
// an option-less `local-ai run` gets. cmd/local-ai feeds them to kong as
|
|
// the "${galleries}" / "${backends}" vars, and DefaultRuntimeBaseline uses
|
|
// them to tell "kong default" apart from "user-configured" at settings-load
|
|
// time - they must stay the single source for both.
|
|
const DefaultGalleriesJSON = `[{"name":"localai", "url":"github:mudler/LocalAI/gallery/index.yaml@master"}]`
|
|
const DefaultBackendGalleriesJSON = `[{"name":"localai", "url":"github:mudler/LocalAI/backend/index.yaml@master"}]`
|
|
|
|
func mustGalleries(jsonList string) []Gallery {
|
|
var g []Gallery
|
|
if err := json.Unmarshal([]byte(jsonList), &g); err != nil {
|
|
// Both inputs are compile-time constants above; failing loudly at
|
|
// init beats silently mis-detecting every gallery as env-set.
|
|
panic("invalid built-in gallery JSON: " + err.Error())
|
|
}
|
|
return g
|
|
}
|
|
|
|
// DefaultRuntimeBaseline is the ApplicationConfig an option-less `local-ai
|
|
// run` produces: NewApplicationConfig plus the flag defaults kong injects
|
|
// even when the user passes nothing (see the default:"..." tags in
|
|
// core/cli/run.go). ApplyRuntimeSettingsAtStartup compares the live config
|
|
// against it to decide whether env/CLI claimed a field (env > file).
|
|
func DefaultRuntimeBaseline() *ApplicationConfig {
|
|
o := NewApplicationConfig()
|
|
// WithDebug(log level) is always applied by run.go; the default level
|
|
// is not debug, so an option-less run boots with Debug=false even
|
|
// though the NewApplicationConfig literal says true.
|
|
o.Debug = false
|
|
o.Galleries = mustGalleries(DefaultGalleriesJSON)
|
|
o.BackendGalleries = mustGalleries(DefaultBackendGalleriesJSON)
|
|
o.AutoloadGalleries = true
|
|
o.AutoloadBackendGalleries = true
|
|
// core/cli/run.go injects WithMemoryReclaimer(enabled, threshold)
|
|
// unconditionally, so the kong threshold default (0.95) reaches the
|
|
// config even when the reclaimer flag is off - this overlay must match
|
|
// that contract or a UI-persisted threshold looks env-set and is
|
|
// skipped at boot.
|
|
o.MemoryReclaimerThreshold = 0.95
|
|
return o
|
|
}
|
|
|
|
// ApplyRuntimeSettingsAtStartup merges persisted settings into o with
|
|
// env-over-file precedence: a field is taken from the file only when the
|
|
// live value still equals the option-less baseline (env/CLI did not touch
|
|
// it) or when the file is that field's only source (fileAuthoritative).
|
|
// Used at boot (application.New) and by the runtime_settings.json file
|
|
// watcher, so a manual file edit behaves exactly like a boot-time load.
|
|
//
|
|
// Known limitation (accepted): an env var explicitly set to its default
|
|
// value is indistinguishable from "not set", so the file wins there; and a
|
|
// field previously changed via the API looks env-set to the watcher, so a
|
|
// manual file edit of that field lands on the next restart.
|
|
func (o *ApplicationConfig) ApplyRuntimeSettingsAtStartup(settings *RuntimeSettings) {
|
|
if settings == nil {
|
|
return
|
|
}
|
|
baseline := DefaultRuntimeBaseline()
|
|
for _, f := range runtimeSettingsFields {
|
|
if f.snapshotOnly || !f.isSet(settings) {
|
|
continue
|
|
}
|
|
if !f.fileAuthoritative && f.envSet(o, baseline) {
|
|
continue
|
|
}
|
|
f.apply(o, settings)
|
|
}
|
|
// Startup invariant, mirroring ApplyRuntimeSettings and the gating in
|
|
// startWatchdog: enabled idle/busy checks or the memory reclaimer imply
|
|
// the watchdog master flag. Never forced off here - an explicit
|
|
// watchdog_enabled=false row already applied above when unclaimed.
|
|
if o.WatchDogIdle || o.WatchDogBusy || o.MemoryReclaimerEnabled {
|
|
o.WatchDog = true
|
|
}
|
|
// VRAM budget post-processing, mirroring ApplyRuntimeSettings: at boot
|
|
// only run.go's env path installs the process-wide cap, so a
|
|
// file-persisted budget applied by the loop above would set
|
|
// o.VRAMBudget without ever capping allocations. When env set the
|
|
// budget the loop skipped the file value and this re-installs the env
|
|
// value - idempotent. Fail-open on a malformed persisted value so it
|
|
// cannot wedge startup.
|
|
if settings.VRAMBudget != nil {
|
|
if b, err := vrambudget.Parse(o.VRAMBudget); err == nil {
|
|
xsysinfo.SetDefaultVRAMBudget(b)
|
|
}
|
|
}
|
|
}
|