refactor(stores): resolve the store backend and its loader slot explicitly

Store loading picked the backend and the loader's process slot in one
function, which let an explicit `backend` argument collide with the
store's own config and made traces report a fixed "local-store".

This changes /stores/* behaviour for every store:

- An explicit `backend` that differs from the one named in the store's
  model config now gets its own loader slot, `<store>@<backend>`, so it
  no longer reuses (or evicts) the process of the configured backend.
  Naming the configured backend, or one of its aliases, keeps the
  store's own slot.
- When the config names a different backend than the explicit one, the
  config's `options` are not passed on, since they are meant for the
  configured backend. A config without `backend:` still passes its
  options to the explicit backend.
- Backend names are compared and reported in canonical form (aliases
  resolved, lower case) through the new model.CanonicalBackend, and
  traces record the backend that was actually used instead of always
  "local-store".

Backend loading itself uses CanonicalBackend for alias handling; type
aliases keep their existing effect.

Signed-off-by: Anush008 <mail@anush.sh>
Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
Anush008 authored and Ettore Di Giacinto committed 2026-10-13 08:13:55 +00:00
1 parent 5aabc5891f
commit dee4d8a399
3 files changed
+164 -51

No files matched your search

+56 -40
View File
@@ -32,8 +32,8 @@ type Neighbor struct {
Payload []byte
}
// NewVectorStore returns a VectorStore backed by the local-store
// gRPC backend, namespaced by storeName so two routers don't collide.
// NewVectorStore returns a VectorStore backed by the store's gRPC backend,
// namespaced by storeName so two routers don't collide.
// cl resolves the per-store model config (backend + options); it may be nil,
// in which case the store falls back to the default backend and its built-in
// defaults.
@@ -51,8 +51,8 @@ type localVectorStore struct {
storeName string
}
func (s *localVectorStore) backend(_ context.Context) (grpc.Backend, error) {
return StoreBackend(s.loader, s.appConfig, s.cl, s.storeName, "")
func (s *localVectorStore) backend(_ context.Context) (grpc.Backend, string, error) {
return storeBackend(s.loader, s.cl, s.storeName, "")
}
// Search is the top-1 special case of SearchK; delegating keeps the
@@ -69,11 +69,11 @@ func (s *localVectorStore) Search(ctx context.Context, vec []float32) (float64,
func (s *localVectorStore) SearchK(ctx context.Context, vec []float32, k int) (neighbors []Neighbor, err error) {
outcome := "hit"
sim := 0.0
be, berr := s.backend(ctx)
be, backendName, berr := s.backend(ctx)
if berr != nil {
outcome = "backend_load_error"
err = fmt.Errorf("vector store load: %w", berr)
s.recordTrace("", time.Now(), "search", len(vec), 0, outcome, err)
s.recordTrace("", backendName, time.Now(), "search", len(vec), 0, outcome, err)
return nil, err
}
release, err := AcquireGlobalBackendSlot()
@@ -82,9 +82,9 @@ func (s *localVectorStore) SearchK(ctx context.Context, vec []float32, k int) (n
}
defer release()
start := time.Now()
traceID := s.beginTrace(start, "search")
traceID := s.beginTrace(backendName, start, "search")
defer func() {
s.recordTrace(traceID, start, "search", len(vec), sim, outcome, err)
s.recordTrace(traceID, backendName, start, "search", len(vec), sim, outcome, err)
}()
_, values, similarities, ferr := store.Find(ctx, be, vec, k)
if ferr != nil {
@@ -105,11 +105,11 @@ func (s *localVectorStore) SearchK(ctx context.Context, vec []float32, k int) (n
func (s *localVectorStore) Insert(ctx context.Context, vec []float32, payload []byte) (err error) {
outcome := "ok"
be, berr := s.backend(ctx)
be, backendName, berr := s.backend(ctx)
if berr != nil {
outcome = "backend_load_error"
err = fmt.Errorf("vector store load: %w", berr)
s.recordTrace("", time.Now(), "insert", len(vec), 0, outcome, err)
s.recordTrace("", backendName, time.Now(), "insert", len(vec), 0, outcome, err)
return err
}
release, err := AcquireGlobalBackendSlot()
@@ -118,9 +118,9 @@ func (s *localVectorStore) Insert(ctx context.Context, vec []float32, payload []
}
defer release()
start := time.Now()
traceID := s.beginTrace(start, "insert")
traceID := s.beginTrace(backendName, start, "insert")
defer func() {
s.recordTrace(traceID, start, "insert", len(vec), 0, outcome, err)
s.recordTrace(traceID, backendName, start, "insert", len(vec), 0, outcome, err)
}()
if serr := store.SetSingle(ctx, be, vec, payload); serr != nil {
outcome = "insert_error"
@@ -138,11 +138,11 @@ func (s *localVectorStore) InsertBatch(ctx context.Context, vecs [][]float32, pa
if len(vecs) > 0 {
dim = len(vecs[0])
}
be, berr := s.backend(ctx)
be, backendName, berr := s.backend(ctx)
if berr != nil {
outcome = "backend_load_error"
err = fmt.Errorf("vector store load: %w", berr)
s.recordTrace("", time.Now(), "insert_batch", dim, 0, outcome, err)
s.recordTrace("", backendName, time.Now(), "insert_batch", dim, 0, outcome, err)
return err
}
release, err := AcquireGlobalBackendSlot()
@@ -151,9 +151,9 @@ func (s *localVectorStore) InsertBatch(ctx context.Context, vecs [][]float32, pa
}
defer release()
start := time.Now()
traceID := s.beginTrace(start, "insert_batch")
traceID := s.beginTrace(backendName, start, "insert_batch")
defer func() {
s.recordTrace(traceID, start, "insert_batch", dim, 0, outcome, err)
s.recordTrace(traceID, backendName, start, "insert_batch", dim, 0, outcome, err)
}()
if serr := store.SetCols(ctx, be, vecs, payloads); serr != nil {
outcome = "insert_error"
@@ -171,11 +171,11 @@ func (s *localVectorStore) Delete(ctx context.Context, vecs [][]float32) (err er
if len(vecs) > 0 {
dim = len(vecs[0])
}
be, berr := s.backend(ctx)
be, backendName, berr := s.backend(ctx)
if berr != nil {
outcome = "backend_load_error"
err = fmt.Errorf("vector store load: %w", berr)
s.recordTrace("", time.Now(), "delete", dim, 0, outcome, err)
s.recordTrace("", backendName, time.Now(), "delete", dim, 0, outcome, err)
return err
}
release, err := AcquireGlobalBackendSlot()
@@ -184,9 +184,9 @@ func (s *localVectorStore) Delete(ctx context.Context, vecs [][]float32) (err er
}
defer release()
start := time.Now()
traceID := s.beginTrace(start, "delete")
traceID := s.beginTrace(backendName, start, "delete")
defer func() {
s.recordTrace(traceID, start, "delete", dim, 0, outcome, err)
s.recordTrace(traceID, backendName, start, "delete", dim, 0, outcome, err)
}()
if serr := store.DeleteCols(ctx, be, vecs); serr != nil {
outcome = "delete_error"
@@ -198,17 +198,16 @@ func (s *localVectorStore) Delete(ctx context.Context, vecs [][]float32) (err er
// recordTrace surfaces vector-store calls in /api/backend-traces, including
// the backend-load-failure path that otherwise vanishes into an xlog.Warn.
// modelName uses the store namespace (e.g. "router-cache-smart-router") so
// admins can tell which router's cache misbehaved; the backend is always
// "local-store" and can't disambiguate.
func (s *localVectorStore) beginTrace(start time.Time, op string) string {
// admins can tell which router's cache misbehaved.
func (s *localVectorStore) beginTrace(backendName string, start time.Time, op string) string {
if s.appConfig == nil || !s.appConfig.EnableTracing {
return ""
}
trace.InitBackendTracingIfEnabled(s.appConfig.TracingMaxItems, s.appConfig.TracingMaxBodyBytes)
return trace.BeginBackendTrace(trace.BackendTrace{Timestamp: start, Type: trace.BackendTraceVectorStore, ModelName: s.storeName, Backend: model.LocalStoreBackend, Summary: op})
return trace.BeginBackendTrace(trace.BackendTrace{Timestamp: start, Type: trace.BackendTraceVectorStore, ModelName: s.storeName, Backend: backendName, Summary: op})
}
func (s *localVectorStore) recordTrace(traceID string, start time.Time, op string, vecDim int, sim float64, outcome string, err error) {
func (s *localVectorStore) recordTrace(traceID, backendName string, start time.Time, op string, vecDim int, sim float64, outcome string, err error) {
if s.appConfig == nil || !s.appConfig.EnableTracing {
return
}
@@ -237,7 +236,7 @@ func (s *localVectorStore) recordTrace(traceID string, start time.Time, op strin
Duration: time.Since(start),
Type: trace.BackendTraceVectorStore,
ModelName: s.storeName,
Backend: model.LocalStoreBackend,
Backend: backendName,
Summary: summary,
Error: errStr,
Data: data,
@@ -245,26 +244,43 @@ func (s *localVectorStore) recordTrace(traceID string, start time.Time, op strin
}
func StoreBackend(sl *model.ModelLoader, appConfig *config.ApplicationConfig, cl *config.ModelConfigLoader, storeName string, backend string) (grpc.Backend, error) {
// Resolve the per-store model config (keyed by the store namespace, which
// is the model ID for a store). This is the LocalAI-native config surface:
// a store's backend selection and its backend-specific settings live in a
// model YAML's `backend:` and `options:` fields, so different stores can
// point at different servers/indexes. When no config exists for the store,
// we fall back to the default backend and let the backend apply its own
// built-in defaults — preserving the zero-config experience.
var loadOpts []string
be, _, err := storeBackend(sl, cl, storeName, backend)
return be, err
}
func storeBackend(sl *model.ModelLoader, cl *config.ModelConfigLoader, storeName string, backend string) (grpc.Backend, string, error) {
backend, loadOpts, modelID := resolveStoreBackend(cl, storeName, backend)
be, err := loadStoreBackend(sl, modelID, storeName, backend, loadOpts)
return be, model.CanonicalBackend(backend), err
}
// The loader caches processes by ID alone, so an explicit backend other than
// the store's own needs its own ID.
func resolveStoreBackend(cl *config.ModelConfigLoader, storeName string, backend string) (string, []string, string) {
home := model.LocalStoreBackend
var cfgBackend string
var opts []string
if cl != nil {
if cfg, ok := cl.GetModelConfig(storeName); ok {
if backend == "" {
backend = cfg.Backend
cfgBackend, opts = cfg.Backend, cfg.Options
if cfgBackend != "" {
home = cfgBackend
}
loadOpts = cfg.Options
}
}
if backend == "" {
backend = model.LocalStoreBackend
backend = home
}
if model.CanonicalBackend(backend) == model.CanonicalBackend(home) {
return backend, opts, storeName
}
if cfgBackend != "" {
opts = nil
}
return backend, opts, storeName + "@" + model.CanonicalBackend(backend)
}
func loadStoreBackend(sl *model.ModelLoader, modelID, storeName, backend string, loadOpts []string) (grpc.Backend, error) {
// ModelLoader caches backend processes by `modelID`, not by the `model`
// passed via WithModel. Without a distinct modelID, every StoreBackend
// call collapses to the same `modelID=""` cache slot — face (512-D) and
@@ -281,7 +297,7 @@ func StoreBackend(sl *model.ModelLoader, appConfig *config.ApplicationConfig, cl
// convention upgrades in lockstep).
sc := []model.Option{
model.WithBackendString(backend),
model.WithModelID(storeName),
model.WithModelID(modelID),
model.WithModel(store.NamespacePrefix + storeName),
}
+97
View File
@@ -70,6 +70,36 @@ var _ = Describe("localVectorStore tracing", func() {
Expect(bt.Error).To(ContainSubstring("vector store load"))
})
It("records the backend selected by the store's model config, not local-store", func() {
appCfg := &config.ApplicationConfig{
EnableTracing: true,
TracingMaxItems: 16,
TracingMaxBodyBytes: 1024,
}
cl := config.NewModelConfigLoader(GinkgoT().TempDir())
cl.ReplaceModelConfigs([]config.ModelConfig{
{Name: "router-cache-valkey", Backend: "valkey"},
})
s := &localVectorStore{
loader: model.NewModelLoader(&system.SystemState{}),
appConfig: appCfg,
cl: cl,
storeName: "router-cache-valkey",
}
err := s.Insert(context.Background(), []float32{0.1, 0.2}, []byte("payload"))
Expect(err).To(HaveOccurred())
Eventually(func() *trace.BackendTrace {
return findVectorStoreTrace("router-cache-valkey")
}).ShouldNot(BeNil())
bt := findVectorStoreTrace("router-cache-valkey")
Expect(bt.Backend).To(Equal(model.ValkeyStoreBackend))
Expect(bt.Data["op"]).To(Equal("insert"))
Expect(bt.Data["outcome"]).To(Equal("backend_load_error"))
})
It("does not record a trace when tracing is disabled", func() {
// Opt-out path: appConfig.EnableTracing=false must short-circuit
// before InitBackendTracingIfEnabled, so a workload with tracing
@@ -86,3 +116,70 @@ var _ = Describe("localVectorStore tracing", func() {
}).Should(BeNil())
})
})
var _ = Describe("resolveStoreBackend", func() {
valkeyCfg := func(opts ...string) *config.ModelConfigLoader {
cl := config.NewModelConfigLoader(GinkgoT().TempDir())
cl.ReplaceModelConfigs([]config.ModelConfig{{
Name: "faces",
Backend: model.ValkeyStoreBackend,
Options: opts,
}})
return cl
}
It("defaults to local-store with no config and no explicit backend", func() {
name, opts, id := resolveStoreBackend(nil, "some-store", "")
Expect(name).To(Equal(model.LocalStoreBackend))
Expect(opts).To(BeEmpty())
Expect(id).To(Equal("some-store"))
})
It("uses the store's model config backend and options", func() {
name, opts, id := resolveStoreBackend(valkeyCfg("addr:valkey:6379"), "faces", "")
Expect(name).To(Equal(model.ValkeyStoreBackend))
Expect(opts).To(Equal([]string{"addr:valkey:6379"}))
Expect(id).To(Equal("faces"))
})
It("keeps the config's options and slot when the explicit backend is the configured one (aliases included)", func() {
name, opts, id := resolveStoreBackend(valkeyCfg("addr:valkey:6379"), "faces", "valkey")
Expect(name).To(Equal("valkey"))
Expect(opts).To(Equal([]string{"addr:valkey:6379"}))
Expect(id).To(Equal("faces"))
})
It("gives an explicit override of another backend its own slot and not the config's options", func() {
name, opts, id := resolveStoreBackend(valkeyCfg("addr:valkey:6379"), "faces", model.LocalStoreBackend)
Expect(name).To(Equal(model.LocalStoreBackend))
Expect(opts).To(BeEmpty())
Expect(id).To(Equal("faces@local-store"))
})
It("passes the options of a config without backend: to an explicit backend", func() {
cl := config.NewModelConfigLoader(GinkgoT().TempDir())
cl.ReplaceModelConfigs([]config.ModelConfig{{
Name: "vectors",
Options: []string{"addr:valkey:6379"},
}})
name, opts, id := resolveStoreBackend(cl, "vectors", model.ValkeyStoreBackend)
Expect(name).To(Equal(model.ValkeyStoreBackend))
Expect(opts).To(Equal([]string{"addr:valkey:6379"}))
Expect(id).To(Equal("vectors@valkey-store"))
})
It("separates an override from the default local-store slot when there is no config", func() {
_, _, id := resolveStoreBackend(nil, "mix", "valkey")
Expect(id).To(Equal("mix@valkey-store"))
_, _, id = resolveStoreBackend(nil, "mix", "embedded-store")
Expect(id).To(Equal("mix"))
})
It("compares backends by canonical name (aliases, case)", func() {
Expect(model.CanonicalBackend("valkey")).To(Equal(model.ValkeyStoreBackend))
Expect(model.CanonicalBackend("Valkey")).To(Equal(model.ValkeyStoreBackend))
Expect(model.CanonicalBackend("embedded-store")).To(Equal(model.LocalStoreBackend))
Expect(model.CanonicalBackend(model.ValkeyStoreBackend)).To(Equal(model.ValkeyStoreBackend))
Expect(model.CanonicalBackend("Valkey-Store")).To(Equal(model.ValkeyStoreBackend))
})
})
+11 -11
View File
@@ -37,6 +37,14 @@ var Aliases = map[string]string{
"stablediffusion": StableDiffusionGGMLBackend,
}
func CanonicalBackend(name string) string {
b := strings.ToLower(name)
if real, ok := Aliases[b]; ok {
return real
}
return b
}
var TypeAlias = map[string]string{
"sentencetransformers": "SentenceTransformer",
"huggingface-embeddings": "SentenceTransformer",
@@ -337,17 +345,9 @@ func (ml *ModelLoader) backendLoader(opts ...Option) (client grpc.Backend, err e
"f16", opt.F16Memory)
}
backend := strings.ToLower(o.backendString)
if realBackend, exists := Aliases[backend]; exists {
typeAlias, exists := TypeAlias[backend]
if exists {
xlog.Debug("alias is a type alias", "alias", backend, "realBackend", realBackend, "type", typeAlias)
o.gRPCOptions.Type = typeAlias
} else {
xlog.Debug("alias", "alias", backend, "realBackend", realBackend)
}
backend = realBackend
backend := CanonicalBackend(o.backendString)
if typeAlias, ok := TypeAlias[strings.ToLower(o.backendString)]; ok {
o.gRPCOptions.Type = typeAlias
}
modelFileName := o.modelFile