mirror of
https://github.com/mudler/LocalAI.git
synced 2026-06-14 19:58:44 -04:00
* fix(router): score classifier production-readiness Conversation trimming runs through the classifier model's chat template and trims by exact token count, sized to the model's n_batch which is now scaled to context so long probes can't crash the backend. Missing chat_message templates are a hard error at router build time. Router- facing factories (Embedder/Scorer/Reranker/TokenCounter) re-resolve ModelConfig per call so a model installed post-startup doesn't bind a stub Backend="" config and silently fall into the loader's auto- iterate path. New 'vector_store' backend trace recorded inside localVectorStore on every Search/Insert — including the backend-load-failure path that previously vanished into an xlog.Warn — with outcome tagging (hit/miss/empty_store/backend_load_error/find_error/insert_error/ok). Companion cleanup drops misleading similarity:0 and input_tokens_count:0 from non-hit and text-mode traces. Gallery local-store-development aliases to 'local-store' so the master image satisfies pkg/model.LocalStoreBackend lookups from the embedding cache. Misc: llama-cpp TokenizeString reads the correct 'prompt' JSON key (the original bug); ModelTokenize nil-guard; non-fatal mitm proxy startup; PII 'route_local' renamed to 'allow' with docs/UI in sync; model-editor footer no longer eats the edit area on small screens; several config-editor template/dropdown/section fixes. Tests: e2e router specs (casual/code-hint + long-conversation trim), vector_store trace specs, lazy-factory specs, gallery dev-alias resolution, Playwright trace badge + scroll regression. Assisted-by: Claude:claude-opus-4-7 [Claude Code] Signed-off-by: Richard Palethorpe <io@richiejp.com> * feat(backend): auto-size batch to context for embedding and rerank models Embedding and rerank models pool over the whole input in a single physical batch (n_ubatch). With batch left at the 512 default, the backend rejects longer inputs with "input is too large to process", silently capping a large-context embedder (e.g. 8k/32k) at 512 tokens. Size n_batch to the context for these single-pass usecases, mirroring the existing FLAG_SCORE behaviour; an explicit batch: still wins. Extracts EffectiveContextSize/EffectiveBatchSize from grpcModelOpts so the effective decode window has one home for other callers to reuse. Adds an e2e-aio regression test that embeds a >512-token input. The AIO embedding model is switched to nomic-embed-text-v1.5 (2048 context) because the previous granite model was capped at 512 tokens and could not exercise the larger batch. Assisted-by: claude-code:claude-opus-4-8 [Claude Code] Signed-off-by: Richard Palethorpe <io@richiejp.com> * fix(gallery): raise arch-router scoring output cap via parallel:64 Scoring decodes the whole prompt+candidate in a single llama_decode and reads one logit row per candidate token. The vendored llama.cpp server caps causal output rows at n_parallel, so the default of 1 aborts with GGML_ASSERT(n_outputs_max <= cparams.n_outputs_max) on multi-token route labels. Set options: [parallel:64] on both arch-router quant entries to lift the cap; kv_unified (the grpc-server default) keeps the full context per sequence, so this does not split the KV cache. Assisted-by: claude-code:claude-opus-4-8 [Claude Code] Signed-off-by: Richard Palethorpe <io@richiejp.com> --------- Signed-off-by: Richard Palethorpe <io@richiejp.com>
121 lines
4.1 KiB
Go
121 lines
4.1 KiB
Go
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
|
|
// 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 || cfg.Name == "" {
|
|
return nil
|
|
}
|
|
return cfg
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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 {
|
|
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 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
|
|
}
|
|
}
|
|
|
|
func (a *Application) Reranker(modelName string) backend.Reranker {
|
|
if a.adapterConfig(modelName) == nil {
|
|
return nil
|
|
}
|
|
return &lazyReranker{app: a, modelName: 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 &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 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)
|
|
}
|