Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 1 addition & 65 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,6 @@ updates:
schedule:
# Check for updates to GitHub Actions every weekday
interval: "weekly"
- package-ecosystem: "pip"
directory: "/backend/python/bark"
schedule:
interval: "weekly"
- package-ecosystem: "pip"
directory: "/backend/python/common/template"
schedule:
Expand All @@ -55,75 +51,15 @@ updates:
ignore:
- dependency-name: "torch"
- dependency-name: "transformers"
- package-ecosystem: "pip"
directory: "/backend/python/exllama"
schedule:
interval: "weekly"
- package-ecosystem: "pip"
directory: "/backend/python/exllama2"
schedule:
interval: "weekly"
- package-ecosystem: "pip"
directory: "/backend/python/mamba"
schedule:
interval: "weekly"
- package-ecosystem: "pip"
directory: "/backend/python/openvoice"
schedule:
interval: "weekly"
- package-ecosystem: "pip"
directory: "/backend/python/rerankers"
schedule:
interval: "weekly"
- package-ecosystem: "pip"
directory: "/backend/python/sentencetransformers"
schedule:
interval: "weekly"
- package-ecosystem: "pip"
directory: "/backend/python/transformers"
schedule:
interval: "weekly"
- package-ecosystem: "pip"
directory: "/backend/python/vllm"
schedule:
interval: "weekly"
- package-ecosystem: "pip"
directory: "/examples/chainlit"
schedule:
interval: "weekly"
- package-ecosystem: "pip"
directory: "/examples/functions"
schedule:
interval: "weekly"
- package-ecosystem: "pip"
directory: "/examples/langchain/langchainpy-localai-example"
schedule:
interval: "weekly"
- package-ecosystem: "pip"
directory: "/examples/langchain-chroma"
schedule:
interval: "weekly"
- package-ecosystem: "pip"
directory: "/examples/streamlit-bot"
schedule:
interval: "weekly"
- package-ecosystem: "docker"
directory: "/examples/k8sgpt"
schedule:
interval: "weekly"
- package-ecosystem: "docker"
directory: "/examples/kubernetes"
schedule:
interval: "weekly"
- package-ecosystem: "docker"
directory: "/examples/langchain"
schedule:
interval: "weekly"
- package-ecosystem: "gomod"
directory: "/examples/semantic-todo"
schedule:
interval: "weekly"
- package-ecosystem: "docker"
directory: "/examples/telegram-bot"
schedule:
interval: "weekly"
interval: "weekly"
7 changes: 4 additions & 3 deletions .github/workflows/notify-releases.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,14 @@ jobs:
messages: [
{
role: "system",
content: "Write a discord message with a bullet point summary of the release notes."
content: "Write a Discord message with a bullet point summary of the release notes. Keep the complete message under 1800 characters."
},
{
role: "user",
content: $input
}
]
],
max_tokens: 450
}')

# Send the request to LocalAI API
Expand All @@ -46,7 +47,7 @@ jobs:
-d "$json_payload")

# Extract the summary from the response
summary=$(echo $response | jq -r '.choices[0].message.content')
summary=$(printf '%s' "$response" | jq -er '.choices[0].message.content | strings | .[0:1800]')

# Print the summary
# -H "Authorization: Bearer $API_KEY" \
Expand Down
2 changes: 1 addition & 1 deletion backend/cpp/audio-cpp/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
# recipe is a make target (not a prepare.sh) so 'make purge && make' is a clean
# rebuild and so the bump bot can see the pin.

AUDIO_CPP_VERSION?=4d383be1bff107e823ffc19120dcb6c78d493c0f
AUDIO_CPP_VERSION?=288a2712316470847a730e55db9ac9e5062a2b03
AUDIO_CPP_REPO?=https://github.com/0xShug0/audio.cpp

CURRENT_MAKEFILE_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
Expand Down
2 changes: 1 addition & 1 deletion backend/go/crispasr/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ JOBS?=$(shell nproc --ignore=1)

# CrispASR version (release tag)
CRISPASR_REPO?=https://github.com/CrispStrobe/CrispASR
CRISPASR_VERSION?=74bb374a8cc74284348d76a0a6e944180fbe6b07
CRISPASR_VERSION?=ae4474dd8306384a0e697183d863dfc52e69a2fb
SO_TARGET?=libgocrispasr.so

CMAKE_ARGS+=-DBUILD_SHARED_LIBS=OFF
Expand Down
2 changes: 1 addition & 1 deletion core/application/startup.go
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,7 @@ func New(opts ...config.AppOption) (*Application, error) {
// the load above: the loader is empty until then, and a resync against an
// empty loader silently reconciles nothing.
if revisionStore != nil {
if err := modeladmin.ResyncModelConfigRevisions(options.Context, application.ModelConfigLoader(), revisionStore); err != nil {
if err := modeladmin.ResyncModelConfigRevisions(options.Context, application.ModelConfigLoader(), options, revisionStore); err != nil {
xlog.Warn("Failed to resync model config revisions", "error", err)
}
}
Expand Down
30 changes: 21 additions & 9 deletions core/config/model_config_loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,14 @@ func readModelConfigsFromFile(file string, opts ...ConfigLoaderOption) ([]*Model
if err := yaml.Unmarshal(f, &configs); err == nil && len(configs) > 0 {
for _, cc := range configs {
cc.modelConfigFile = file
// Stamp before SetDefaults: the revision describes what is on disk.
// SetDefaults folds in the GGUF guess, hardware defaults and
// app-level options, none of which are persisted configuration, and
// the GGUF guess in particular depends on whether the model file
// parses at that moment.
if err := cc.StampPersistedConfigRevision(); err != nil {
return nil, fmt.Errorf("stamping config revision for %q: %w", cc.Name, err)
}
cc.SetDefaults(opts...)
cc.syncKnownUsecasesFromString()
}
Expand All @@ -182,6 +190,9 @@ func readModelConfigsFromFile(file string, opts ...ConfigLoaderOption) ([]*Model

c.modelConfigFile = file
c.syncKnownUsecasesFromString()
if err := c.StampPersistedConfigRevision(); err != nil {
return nil, fmt.Errorf("stamping config revision for %q: %w", c.Name, err)
}
c.SetDefaults(opts...)

return []*ModelConfig{c}, nil
Expand Down Expand Up @@ -218,17 +229,18 @@ func (bcl *ModelConfigLoader) LoadModelConfigFileByName(modelName, modelPath str
}
}

cfg.SetDefaults(append(opts, ModelPath(modelPath))...)

// Stamp the revision here, at the boundary between the persisted
// configuration and the request that is about to override parts of it.
// Everything downstream of this point (the request middleware) merges
// per-request prediction parameters into cfg, so a revision computed later
// would identify the request rather than the configuration.
if err := cfg.StampPersistedConfigRevision(); err != nil {
return nil, fmt.Errorf("stamping config revision for %q: %w", modelName, err)
// Stamp before SetDefaults, and only when this config did not come from
// disk already carrying one (a name with no config file on disk is
// synthesized above). Re-stamping a loaded config here would hash it after
// SetDefaults and reintroduce the dependency on the GGUF guess.
if cfg.PersistedConfigRevision() == "" {
if err := cfg.StampPersistedConfigRevision(); err != nil {
return nil, fmt.Errorf("stamping config revision for %q: %w", modelName, err)
}
}

cfg.SetDefaults(append(opts, ModelPath(modelPath))...)

return cfg, nil
}

Expand Down
67 changes: 62 additions & 5 deletions core/config/model_config_revision_stability_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,18 +73,75 @@ template:
})

// The request pipeline reloads the config through LoadModelConfigFileByName,
// which applies SetDefaults a second time. That must not move the revision
// away from the one model administration publishes from the loader map.
// which applies SetDefaults a second time. The stamp is taken before those
// defaults, so both the stored config and the one a request resolves carry
// the same revision.
It("survives the extra SetDefaults the request path applies", func() {
loader := config.NewModelConfigLoader(dir)
Expect(loader.LoadModelConfigsFromPath(dir, appConfig.ToConfigLoaderOptions()...)).To(Succeed())
stored, ok := loader.GetModelConfig("example")
Expect(ok).To(BeTrue())
adminRevision, err := config.ModelConfigRevision(&stored)
Expect(err).ToNot(HaveOccurred())
Expect(stored.PersistedConfigRevision()).ToNot(BeEmpty())

requestCfg, err := loader.LoadModelConfigFileByNameDefaultOptions("example", appConfig)
Expect(err).ToNot(HaveOccurred())
Expect(requestCfg.PersistedConfigRevision()).To(Equal(adminRevision))
Expect(requestCfg.PersistedConfigRevision()).To(Equal(stored.PersistedConfigRevision()))
})
})

// The revision must describe the configuration as persisted, and nothing else.
// SetDefaults folds in values that are not persisted config: the GGUF guess
// (which reads the model file and can fail on slow or remote storage), the
// hardware defaults, and app-level options like threads. Hashing after that
// made the revision a function of whether a multi-gigabyte file happened to
// parse, so one unchanged YAML produced two different revisions depending on
// the moment, and the controller rejected every request carrying the other one.
var _ = Describe("Model config revision independence from runtime defaults", func() {
It("does not change when SetDefaults is applied", func() {
dir := GinkgoT().TempDir()
body := "backend: llama-cpp\ncontext_size: 50000\nknown_usecases:\n - chat\n" +
"mmproj: llama-cpp/mmproj/example/mmproj.gguf\nname: example\n" +
"parameters:\n model: llama-cpp/models/example/example.gguf\n"
Expect(os.WriteFile(filepath.Join(dir, "example.yaml"), []byte(body), 0o600)).To(Succeed())

appConfig := config.NewApplicationConfig()
appConfig.SystemState = &system.SystemState{Model: system.Model{ModelsPath: dir}}
loader := config.NewModelConfigLoader(dir)
Expect(loader.LoadModelConfigsFromPath(dir, appConfig.ToConfigLoaderOptions()...)).To(Succeed())

stored, ok := loader.GetModelConfig("example")
Expect(ok).To(BeTrue())
before := stored.PersistedConfigRevision()
Expect(before).ToNot(BeEmpty())

// Applying defaults again is what the request path does.
stored.SetDefaults(appConfig.ToConfigLoaderOptions()...)
Expect(stored.PersistedConfigRevision()).To(Equal(before))

resolved, err := loader.LoadModelConfigFileByNameDefaultOptions("example", appConfig)
Expect(err).ToNot(HaveOccurred())
Expect(resolved.PersistedConfigRevision()).To(Equal(before),
"the request path must carry the same revision as the stored config")
})

It("does not change when app-level defaults differ", func() {
dir := GinkgoT().TempDir()
Expect(os.WriteFile(filepath.Join(dir, "example.yaml"),
[]byte("name: example\nbackend: llama-cpp\nparameters:\n model: m.gguf\n"), 0o600)).To(Succeed())

revWith := func(threads int, f16 bool) string {
appConfig := config.NewApplicationConfig()
appConfig.SystemState = &system.SystemState{Model: system.Model{ModelsPath: dir}}
appConfig.Threads = threads
appConfig.F16 = f16
loader := config.NewModelConfigLoader(dir)
Expect(loader.LoadModelConfigsFromPath(dir, appConfig.ToConfigLoaderOptions()...)).To(Succeed())
cfg, err := loader.LoadModelConfigFileByNameDefaultOptions("example", appConfig)
Expect(err).ToNot(HaveOccurred())
return cfg.PersistedConfigRevision()
}

Expect(revWith(8, false)).To(Equal(revWith(1, true)),
"an operator changing threads must not make every model unroutable")
})
})
2 changes: 2 additions & 0 deletions core/http/endpoints/localai/nodes_backends_list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ func (s *stubNodeCommandSender) StopBackend(_, _ string) error { return nil }

func (s *stubNodeCommandSender) UnloadModelOnNode(_, _ string) error { return nil }

func (s *stubNodeCommandSender) PingNode(_ string) error { return nil }

var _ = Describe("ListBackendsOnNodeEndpoint", func() {
var registry *nodes.NodeRegistry

Expand Down
4 changes: 2 additions & 2 deletions core/http/middleware/request_config_revision_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,8 @@ var _ = Describe("Model config revision seen by inference requests", func() {
Expect(admin.LoadModelConfigsFromPath(modelDir, appConfig.ToConfigLoaderOptions()...)).To(Succeed())
loaded, ok := admin.GetModelConfig("test-model")
Expect(ok).To(BeTrue())
adminRevision, err := config.ModelConfigRevision(&loaded)
Expect(err).ToNot(HaveOccurred())
adminRevision := loaded.PersistedConfigRevision()
Expect(adminRevision).ToNot(BeEmpty())

Expect(revisionFor(`{"model":"test-model","temperature":0.7,"messages":[{"role":"user","content":"hi"}]}`)).
To(Equal(adminRevision))
Expand Down
16 changes: 12 additions & 4 deletions core/services/modeladmin/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,13 +177,21 @@ func (s *ConfigService) patchConfig(ctx context.Context, name string, patch map[
if err := s.Loader.LoadModelConfigsFromPath(s.modelsPath(), s.AppConfig.ToConfigLoaderOptions()...); err != nil {
return fmt.Errorf("reload configs: %w", err)
}
loaded, ok := s.Loader.GetModelConfig(updated.Name)
if !ok {
if _, ok := s.Loader.GetModelConfig(updated.Name); !ok {
return fmt.Errorf("reload configs: model %q missing", updated.Name)
}
revision, err := config.ModelConfigRevision(&loaded)
// Resolve the revision the way an inference request does. Hashing the
// stored config instead publishes a value no request will ever carry,
// because SetDefaults runs again on the request path and is not
// idempotent for every model, and the edit would leave the model
// unroutable.
resolved, err := s.Loader.LoadModelConfigFileByNameDefaultOptions(updated.Name, s.AppConfig)
if err != nil {
return fmt.Errorf("compute config revision: %w", err)
return fmt.Errorf("resolve config revision: %w", err)
}
revision := resolved.PersistedConfigRevision()
if revision == "" {
return fmt.Errorf("no config revision stamped for %q", updated.Name)
}
_ = s.Loader.Preload(s.modelsPath())
pending, err := s.applyRevision(ctx, name, updated.Name, revision, updated.IsDisabled())
Expand Down
18 changes: 14 additions & 4 deletions core/services/modeladmin/revision_resync.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ func NewRevisionStore(reader RevisionReader, lifecycle ModelRevisionLifecycle) R
// A model with no stored revision is left alone. It has never been served, and
// inventing controller state for it here would quarantine nothing and describe
// a model that may never be requested.
func ResyncModelConfigRevisions(ctx context.Context, loader *config.ModelConfigLoader, store RevisionStore) error {
if loader == nil || store == nil {
func ResyncModelConfigRevisions(ctx context.Context, loader *config.ModelConfigLoader, appConfig *config.ApplicationConfig, store RevisionStore) error {
if loader == nil || store == nil || appConfig == nil {
return nil
}

Expand All @@ -81,9 +81,19 @@ func ResyncModelConfigRevisions(ctx context.Context, loader *config.ModelConfigL

var transitions []ModelRevisionTransition
for _, cfg := range configs {
want, err := config.ModelConfigRevision(&cfg)
// Resolve the revision the way an inference request does, through the
// loader, rather than hashing the stored config directly. SetDefaults
// is applied again on that path and is not idempotent for every model
// (it re-runs the GGUF guess and hardware defaults), so hashing the
// stored config yields a value no request will ever carry, and
// publishing it would wedge the model this resync exists to unwedge.
resolved, err := loader.LoadModelConfigFileByNameDefaultOptions(cfg.Name, appConfig)
if err != nil {
return fmt.Errorf("compute config revision for %q: %w", cfg.Name, err)
return fmt.Errorf("resolve config for %q: %w", cfg.Name, err)
}
want := resolved.PersistedConfigRevision()
if want == "" {
return fmt.Errorf("no config revision stamped for %q", cfg.Name)
}

stored, err := store.GetModelConfigRevision(ctx, cfg.Name)
Expand Down
Loading
Loading