mirror of
https://github.com/mudler/LocalAI.git
synced 2026-06-07 08:16:53 -04:00
* feat: forward reasoning_effort to the backend so jinja models honor it reasoning_effort was only mapped to the binary enable_thinking toggle and otherwise reached Go-side templates — it was never sent to the backend. So jinja-templated models whose chat template keys on reasoning_effort (gpt-oss Harmony, LFM2.5) could not be driven by it: LFM2.5 ignores enable_thinking and kept emitting <think>. Forward the effective reasoning_effort to the backend as a chat_template_kwarg (mirroring enable_thinking) in grpc-server.cpp, and put it in PredictOptions metadata (gRPCPredictOpts). Add a config-level default: ModelConfig.reasoning_effort and Pipeline.reasoning_effort, resolved by ModelConfig.ApplyReasoningEffort (request value overrides config default, none->disable / level->enable, an operator's reasoning.disable wins). request.go now uses that helper. Assisted-by: Claude:claude-opus-4-8 go test, golangci-lint Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(realtime): set the pipeline LLM's reasoning_effort Apply Pipeline.ReasoningEffort to the pipeline's LLM config when the realtime model is built (per-session copy, overrides the LLM's own reasoning_effort), and surface the resolved effort on the template input so Go-templated models get it too. jinja models receive it via the backend metadata. This lets a realtime pipeline disable thinking on models that only honor reasoning_effort (e.g. LFM2.5), which enable_thinking can't. Assisted-by: Claude:claude-opus-4-8 go test, golangci-lint Signed-off-by: Ettore Di Giacinto <mudler@localai.io> --------- Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
100 lines
3.3 KiB
Go
100 lines
3.3 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"))
|
|
})
|
|
})
|