Skip to content
Open
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
2 changes: 1 addition & 1 deletion .github/workflows/test-extra.yml
Original file line number Diff line number Diff line change
Expand Up @@ -563,7 +563,7 @@ jobs:
- name: Run e2e-backends smoke
env:
BACKEND_IMAGE: quay.io/go-skynet/local-ai-backends:master-cpu-llama-cpp
BACKEND_TEST_CAPS: health,load,predict,stream,logprobs,logit_bias
BACKEND_TEST_CAPS: health,load,predict,stream,logprobs,logit_bias,tokenize
run: |
make test-extra-backend
# Realtime e2e with sherpa-onnx driving VAD + STT + TTS against a mocked LLM.
Expand Down
2 changes: 1 addition & 1 deletion backend/cpp/llama-cpp/grpc-server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3436,7 +3436,7 @@ class BackendServiceImpl final : public backend::Backend::Service {
if (body.count("prompt") != 0) {
const bool add_special = json_value(body, "add_special", false);

llama_tokens tokens = tokenize_mixed(ctx_server.impl->vocab, body.at("content"), add_special, true);
llama_tokens tokens = tokenize_mixed(ctx_server.impl->vocab, body.at("prompt"), add_special, true);


for (const auto& token : tokens) {
Expand Down
2 changes: 2 additions & 0 deletions backend/index.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@
capabilities:
nvidia: "cuda12-rfdetr"
intel: "intel-rfdetr"
#amd: "rocm-rfdetr"

Check warning on line 243 in backend/index.yaml

View workflow job for this annotation

GitHub Actions / Yamllint

243:6 [comments] missing starting space in comment
nvidia-l4t: "nvidia-l4t-arm64-rfdetr"
metal: "metal-rfdetr"
default: "cpu-rfdetr"
Expand Down Expand Up @@ -1557,6 +1557,7 @@
- localai/localai-backends:master-metal-darwin-arm64-kitten-tts
- !!merge <<: *local-store
name: "local-store-development"
alias: "local-store"
uri: "quay.io/go-skynet/local-ai-backends:master-cpu-local-store"
mirrors:
- localai/localai-backends:master-cpu-local-store
Expand All @@ -1567,6 +1568,7 @@
- localai/localai-backends:latest-metal-darwin-arm64-local-store
- !!merge <<: *local-store
name: "metal-local-store-development"
alias: "local-store"
uri: "quay.io/go-skynet/local-ai-backends:master-metal-darwin-arm64-local-store"
mirrors:
- localai/localai-backends:master-metal-darwin-arm64-local-store
Expand Down Expand Up @@ -2510,7 +2512,7 @@
capabilities:
nvidia: "cuda12-rfdetr-development"
intel: "intel-rfdetr-development"
#amd: "rocm-rfdetr-development"

Check warning on line 2515 in backend/index.yaml

View workflow job for this annotation

GitHub Actions / Yamllint

2515:6 [comments] missing starting space in comment
nvidia-l4t: "nvidia-l4t-arm64-rfdetr-development"
metal: "metal-rfdetr-development"
default: "cpu-rfdetr-development"
Expand Down
23 changes: 23 additions & 0 deletions core/application/mitm.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,29 @@ import (
"github.com/mudler/xlog"
)

// startMITMIfConfigured brings up the cloudproxy MITM listener when an
// address is configured, treating any startup failure as non-fatal.
//
// The listener is opt-in middleware whose address is persisted in runtime
// settings (/api/settings → runtime_settings.json) and replayed on every
// boot. A bad value — e.g. a host the process can't bind, like a LAN IP
// inside a container — must NOT abort the whole server: doing so crash-loops
// with no way out, because the Settings UI used to correct the address can't
// load if startup never completes. So on failure we log loudly and carry on;
// the admin fixes the address via /api/settings, which calls RestartMITM.
func startMITMIfConfigured(app *Application, options *config.ApplicationConfig) {
if options.MITMListen == "" {
return
}
if err := startMITMProxy(app, options); err != nil {
xlog.Error("mitm: cloudproxy listener failed to start — continuing without it",
"listen", options.MITMListen,
"error", err,
"hint", "fix the address via Settings (e.g. \":8082\" to bind all interfaces) and the listener will restart",
)
}
}

func startMITMProxy(app *Application, options *config.ApplicationConfig) error {
app.mitmMutex.Lock()
defer app.mitmMutex.Unlock()
Expand Down
58 changes: 58 additions & 0 deletions core/application/mitm_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package application

import (
"github.com/mudler/LocalAI/core/config"
"github.com/mudler/LocalAI/pkg/system"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

// minimal Application wired enough for startMITMProxy: an empty model
// config loader (no host claims), CA written under a temp DataPath.
func newMITMTestApp(dataPath string) (*Application, *config.ApplicationConfig) {
state, err := system.GetSystemState()
Expect(err).NotTo(HaveOccurred())
state.Model.ModelsPath = dataPath
opts := config.NewApplicationConfig(
config.WithSystemState(state),
config.WithDataPath(dataPath),
)
return newApplication(opts), opts
}

var _ = Describe("startMITMIfConfigured", func() {
It("does nothing when no listen address is configured", func() {
app, opts := newMITMTestApp(GinkgoT().TempDir())
opts.MITMListen = ""

Expect(func() { startMITMIfConfigured(app, opts) }).NotTo(Panic())
Expect(app.mitmServer.Load()).To(BeNil(), "no listener should be stored when disabled")
})

// Regression: a persisted-but-unbindable MITM address (e.g. a LAN host
// inside a container) must not abort startup. startMITMIfConfigured
// swallows the bind error so the rest of LocalAI still comes up and the
// admin can fix the address via the Settings UI.
It("logs and continues when the listen address cannot be bound", func() {
app, opts := newMITMTestApp(GinkgoT().TempDir())
// 192.0.2.1 is TEST-NET-1 (RFC 5737): guaranteed not assigned to any
// local interface, so bind fails deterministically without DNS.
opts.MITMListen = "192.0.2.1:8082"

Expect(func() { startMITMIfConfigured(app, opts) }).NotTo(Panic())
Expect(app.mitmServer.Load()).To(BeNil(), "failed listener must not be stored")
})

It("starts and stores the listener on a bindable address", func() {
app, opts := newMITMTestApp(GinkgoT().TempDir())
opts.MITMListen = "127.0.0.1:0" // OS-assigned free port

startMITMIfConfigured(app, opts)

srv := app.mitmServer.Load()
Expect(srv).NotTo(BeNil(), "listener should be stored on success")
DeferCleanup(srv.Stop)
Expect(srv.Addr()).NotTo(BeEmpty())
})
})
109 changes: 83 additions & 26 deletions core/application/router_factories.go
Original file line number Diff line number Diff line change
@@ -1,63 +1,120 @@
package application

import (
"context"
"fmt"

"github.com/mudler/LocalAI/core/backend"
"github.com/mudler/LocalAI/core/config"
)

// adapterConfig resolves a model name to its runtime ModelConfig, or
// nil when the name is unknown. Shared by the router-facing factories
// below and by ModelConfigLookup.
// adapterConfig resolves a model name to its runtime ModelConfig, or nil when
// unknown. LoadModelConfigFileByNameDefaultOptions never returns nil — for an
// unknown name it returns a defaults-filled stub with an empty Name (the YAML
// `name:` field is required by Validate), which is how we tell the two apart.
func (a *Application) adapterConfig(modelName string) *config.ModelConfig {
cfg, err := a.backendLoader.LoadModelConfigFileByNameDefaultOptions(modelName, a.applicationConfig)
if err != nil || cfg == nil {
if err != nil || cfg == nil || cfg.Name == "" {
return nil
}
return cfg
}

// ModelConfigLookup is the lookup function the router middleware's
// classifier validator uses to confirm classifier_model declares
// FLAG_SCORE before binding it.
// ModelConfigLookup is the lookup the router middleware's classifier validator
// uses to confirm classifier_model declares FLAG_SCORE before binding it.
func (a *Application) ModelConfigLookup() func(modelName string) *config.ModelConfig {
return a.adapterConfig
}

// Scorer returns a backend.Scorer bound to the named model, or nil
// when the model is unknown. Used as a method value (app.Scorer) by
// router.ClassifierDeps — no factory-of-factory wrapper needed.
// The router-facing factories below (Scorer, Embedder, Reranker, TokenCounter)
// bind a model NAME at construction and re-resolve the CONFIG on every call.
// Capturing the config at construction would bake in whatever state
// adapterConfig saw first — including a stub returned before the YAML reached
// bcl.configs (e.g. /import-model or gallery install racing startup). The
// classifier registry caches factories by router-config fingerprint, so a
// once-stale capture stays stale until the router config is edited.

func (a *Application) Scorer(modelName string) backend.Scorer {
cfg := a.adapterConfig(modelName)
if a.adapterConfig(modelName) == nil {
return nil
}
return &lazyScorer{app: a, modelName: modelName}
}

type lazyScorer struct {
app *Application
modelName string
}

func (l *lazyScorer) Score(ctx context.Context, prompt string, candidates []string) ([]backend.CandidateScore, error) {
cfg := l.app.adapterConfig(l.modelName)
if cfg == nil {
return nil, fmt.Errorf("scorer: model %q no longer available", l.modelName)
}
return backend.NewScorer(l.app.modelLoader, *cfg, l.app.applicationConfig).Score(ctx, prompt, candidates)
}

// TokenCounter returns a func so the middleware's literal field type accepts
// it as a method value without importing core/http/middleware from here.
func (a *Application) TokenCounter(modelName string) func(string) (int, error) {
if a.adapterConfig(modelName) == nil {
return nil
}
return backend.NewScorer(a.modelLoader, *cfg, a.applicationConfig)
return func(text string) (int, error) {
cfg := a.adapterConfig(modelName)
if cfg == nil {
return 0, fmt.Errorf("token counter: model %q no longer available", modelName)
}
resp, err := backend.ModelTokenize(text, a.modelLoader, *cfg, a.applicationConfig)
if err != nil {
return 0, err
}
return len(resp.Tokens), nil
}
}

// Reranker returns a backend.Reranker bound to the named model, or
// nil when unknown. The reranker model's `type:` (e.g. "colbert")
// selects the scoring head inside the rerankers backend.
func (a *Application) Reranker(modelName string) backend.Reranker {
cfg := a.adapterConfig(modelName)
if cfg == nil {
if a.adapterConfig(modelName) == nil {
return nil
}
return backend.NewReranker(a.modelLoader, *cfg, a.applicationConfig)
return &lazyReranker{app: a, modelName: modelName}
}

// Embedder returns a backend.Embedder bound to the named model, or
// nil when unknown. Used by the router's L2 embedding cache.
func (a *Application) Embedder(modelName string) backend.Embedder {
cfg := a.adapterConfig(modelName)
type lazyReranker struct {
app *Application
modelName string
}

func (l *lazyReranker) Rerank(ctx context.Context, query string, documents []string) ([]backend.RerankResult, error) {
cfg := l.app.adapterConfig(l.modelName)
if cfg == nil {
return nil, fmt.Errorf("reranker: model %q no longer available", l.modelName)
}
return backend.NewReranker(l.app.modelLoader, *cfg, l.app.applicationConfig).Rerank(ctx, query, documents)
}

func (a *Application) Embedder(modelName string) backend.Embedder {
if a.adapterConfig(modelName) == nil {
return nil
}
return backend.NewEmbedder(a.modelLoader, *cfg, a.applicationConfig)
return &lazyEmbedder{app: a, modelName: modelName}
}

type lazyEmbedder struct {
app *Application
modelName string
}

func (l *lazyEmbedder) Embed(ctx context.Context, text string) ([]float32, error) {
cfg := l.app.adapterConfig(l.modelName)
if cfg == nil {
return nil, fmt.Errorf("embedder: model %q no longer available", l.modelName)
}
return backend.NewEmbedder(l.app.modelLoader, *cfg, l.app.applicationConfig).Embed(ctx, text)
}

// VectorStore returns a backend.VectorStore for the named collection,
// or nil when the name is empty. Each router model gets its own
// backend process via the model loader's cache keyed by storeName.
// VectorStore takes a store name, not a model name — no adapterConfig, no
// staleness to avoid.
func (a *Application) VectorStore(storeName string) backend.VectorStore {
return backend.NewVectorStore(a.modelLoader, a.applicationConfig, storeName)
}
Loading
Loading