mirror of
https://github.com/mudler/LocalAI.git
synced 2026-06-13 03:09:03 -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>
164 lines
6.1 KiB
Go
164 lines
6.1 KiB
Go
package backend
|
|
|
|
import (
|
|
"encoding/json"
|
|
|
|
"github.com/mudler/LocalAI/core/config"
|
|
"github.com/mudler/LocalAI/pkg/reasoning"
|
|
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
)
|
|
|
|
var _ = Describe("grpcModelOpts EngineArgs", func() {
|
|
It("serialises engine_args as JSON preserving nested values", func() {
|
|
threads := 1
|
|
cfg := config.ModelConfig{
|
|
Threads: &threads,
|
|
LLMConfig: config.LLMConfig{
|
|
EngineArgs: map[string]any{
|
|
"data_parallel_size": 8,
|
|
"enable_expert_parallel": true,
|
|
"speculative_config": map[string]any{
|
|
"method": "ngram",
|
|
"num_speculative_tokens": 4,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
opts := grpcModelOpts(cfg, "/tmp/models")
|
|
Expect(opts.EngineArgs).NotTo(BeEmpty())
|
|
|
|
var round map[string]any
|
|
Expect(json.Unmarshal([]byte(opts.EngineArgs), &round)).To(Succeed())
|
|
Expect(round["data_parallel_size"]).To(BeEquivalentTo(8))
|
|
Expect(round["enable_expert_parallel"]).To(BeTrue())
|
|
Expect(round["speculative_config"]).To(HaveKeyWithValue("method", "ngram"))
|
|
})
|
|
|
|
It("leaves EngineArgs empty when unset", func() {
|
|
threads := 1
|
|
opts := grpcModelOpts(config.ModelConfig{Threads: &threads}, "/tmp/models")
|
|
Expect(opts.EngineArgs).To(BeEmpty())
|
|
})
|
|
})
|
|
|
|
// Guards the DisableReasoning -> enable_thinking metadata conversion that the
|
|
// per-request reasoning_effort feature (issue #10072) relies on: the request
|
|
// merge sets ReasoningConfig.DisableReasoning, and gRPCPredictOpts is where it
|
|
// becomes the gRPC PredictOptions.Metadata the backend reads.
|
|
var _ = Describe("gRPCPredictOpts enable_thinking metadata", func() {
|
|
// withReasoning builds a fully-defaulted config (gRPCPredictOpts dereferences
|
|
// many pointer fields) and overrides only the reasoning toggle.
|
|
withReasoning := func(disable *bool) config.ModelConfig {
|
|
cfg := config.ModelConfig{}
|
|
cfg.SetDefaults()
|
|
cfg.ReasoningConfig = reasoning.Config{DisableReasoning: disable}
|
|
return cfg
|
|
}
|
|
disabled := true
|
|
enabled := false
|
|
|
|
It("emits enable_thinking=false when reasoning is disabled", func() {
|
|
opts := gRPCPredictOpts(withReasoning(&disabled), "/tmp/models")
|
|
Expect(opts.Metadata).To(HaveKeyWithValue("enable_thinking", "false"))
|
|
})
|
|
|
|
It("emits enable_thinking=true when reasoning is enabled", func() {
|
|
opts := gRPCPredictOpts(withReasoning(&enabled), "/tmp/models")
|
|
Expect(opts.Metadata).To(HaveKeyWithValue("enable_thinking", "true"))
|
|
})
|
|
|
|
It("omits enable_thinking when reasoning is unset", func() {
|
|
opts := gRPCPredictOpts(withReasoning(nil), "/tmp/models")
|
|
Expect(opts.Metadata).ToNot(HaveKey("enable_thinking"))
|
|
})
|
|
})
|
|
|
|
// Guards forwarding the effective reasoning_effort into PredictOptions.Metadata,
|
|
// where the backend passes it to the jinja chat template (chat_template_kwargs)
|
|
// so models like gpt-oss / LFM2.5 honor it.
|
|
var _ = Describe("gRPCPredictOpts reasoning_effort metadata", func() {
|
|
withEffort := func(effort string) config.ModelConfig {
|
|
cfg := config.ModelConfig{}
|
|
cfg.SetDefaults()
|
|
cfg.ReasoningEffort = effort
|
|
return cfg
|
|
}
|
|
|
|
It("forwards reasoning_effort when set", func() {
|
|
opts := gRPCPredictOpts(withEffort("none"), "/tmp/models")
|
|
Expect(opts.Metadata).To(HaveKeyWithValue("reasoning_effort", "none"))
|
|
})
|
|
|
|
It("omits reasoning_effort when empty", func() {
|
|
opts := gRPCPredictOpts(withEffort(""), "/tmp/models")
|
|
Expect(opts.Metadata).ToNot(HaveKey("reasoning_effort"))
|
|
})
|
|
})
|
|
|
|
var _ = Describe("grpcModelOpts NBatch", func() {
|
|
scoreUsecase := config.FLAG_SCORE
|
|
threads := 1
|
|
ctx := 4096
|
|
|
|
It("defaults to 512 for an ordinary model", func() {
|
|
cfg := config.ModelConfig{Threads: &threads, LLMConfig: config.LLMConfig{ContextSize: &ctx}}
|
|
opts := grpcModelOpts(cfg, "/tmp/models")
|
|
Expect(opts.NBatch).To(BeEquivalentTo(512))
|
|
})
|
|
|
|
It("sizes the batch to the context window for score models", func() {
|
|
// Score models decode the whole prompt+candidate in one
|
|
// llama_decode; n_batch must cover it or the backend aborts.
|
|
cfg := config.ModelConfig{Threads: &threads, LLMConfig: config.LLMConfig{ContextSize: &ctx}, KnownUsecases: &scoreUsecase}
|
|
opts := grpcModelOpts(cfg, "/tmp/models")
|
|
Expect(opts.NBatch).To(BeEquivalentTo(4096))
|
|
})
|
|
|
|
It("keeps an explicit batch over the score default", func() {
|
|
cfg := config.ModelConfig{Threads: &threads, LLMConfig: config.LLMConfig{ContextSize: &ctx}, KnownUsecases: &scoreUsecase}
|
|
cfg.Batch = 1024
|
|
opts := grpcModelOpts(cfg, "/tmp/models")
|
|
Expect(opts.NBatch).To(BeEquivalentTo(1024))
|
|
})
|
|
|
|
It("sizes the batch to the context window for embedding models", func() {
|
|
// Embedding/rerank pool over the whole sequence in one physical batch
|
|
// (n_ubatch); without this the input is capped at the 512 default and
|
|
// the backend returns "input is too large to process".
|
|
embeddings := true
|
|
cfg := config.ModelConfig{Threads: &threads, LLMConfig: config.LLMConfig{ContextSize: &ctx}}
|
|
cfg.Embeddings = &embeddings
|
|
opts := grpcModelOpts(cfg, "/tmp/models")
|
|
Expect(opts.NBatch).To(BeEquivalentTo(4096))
|
|
})
|
|
|
|
It("sizes the batch to the context window for rerank models", func() {
|
|
reranking := true
|
|
cfg := config.ModelConfig{Threads: &threads, LLMConfig: config.LLMConfig{ContextSize: &ctx}}
|
|
cfg.Reranking = &reranking
|
|
opts := grpcModelOpts(cfg, "/tmp/models")
|
|
Expect(opts.NBatch).To(BeEquivalentTo(4096))
|
|
})
|
|
|
|
It("does not raise the batch when a score model's context is below the default", func() {
|
|
small := 256
|
|
cfg := config.ModelConfig{Threads: &threads, LLMConfig: config.LLMConfig{ContextSize: &small}, KnownUsecases: &scoreUsecase}
|
|
opts := grpcModelOpts(cfg, "/tmp/models")
|
|
Expect(opts.NBatch).To(BeEquivalentTo(512))
|
|
})
|
|
|
|
It("sizes the batch to the effective 4096 default for a score model with no explicit context_size", func() {
|
|
// The crash case: the backend defaults n_ctx to 4096, so n_batch must
|
|
// follow even when context_size is unset — otherwise n_batch stays 512
|
|
// against a 4096 window and the score decode hits the GGML_ASSERT.
|
|
cfg := config.ModelConfig{Threads: &threads, KnownUsecases: &scoreUsecase}
|
|
Expect(cfg.ContextSize).To(BeNil())
|
|
opts := grpcModelOpts(cfg, "/tmp/models")
|
|
Expect(opts.NBatch).To(BeEquivalentTo(4096))
|
|
Expect(opts.ContextSize).To(BeEquivalentTo(4096), "n_batch must match the effective n_ctx the backend receives")
|
|
})
|
|
})
|