feat(systemone): refuse models that do not declare the usecase

A chat-only model now gets a 400 naming known_usecases: [systemone] instead
of a backend error. Configs declaring no usecases and token_classify models
stay allowed so existing laya and GLiNER setups keep working.

Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
Ettore Di Giacinto committed 2026-09-30 11:27:40 +00:00
1 parent 84c83a70dd
commit e03cf8dac8
2 files changed
+72

No files matched your search

+38
View File
@@ -371,6 +371,35 @@ func systemOneError(c echo.Context, status int, msg string) error {
})
}
// systemOneModelAllowed keeps chat and embedding models out of the decision
// API with an actionable error instead of a backend failure. A config that
// declares no usecases predates the flag and stays allowed, and a
// token_classify model is allowed because the NER path serves it.
func systemOneModelAllowed(cfg config.ModelConfig) error {
if cfg.KnownUsecases == nil {
return nil
}
if *cfg.KnownUsecases&(config.FLAG_SYSTEMONE|config.FLAG_TOKEN_CLASSIFY) != 0 {
return nil
}
return fmt.Errorf("model %q does not declare the systemone usecase (known_usecases: [systemone])", cfg.Name)
}
// checkSystemOneModel applies systemOneModelAllowed to a model looked up by
// name. An unknown model passes here so the existing not-found handling
// downstream keeps its status code.
func checkSystemOneModel(app *application.Application, modelName string) error {
cl := app.ModelConfigLoader()
if cl == nil {
return nil
}
cfg, ok := cl.GetModelConfig(modelName)
if !ok {
return nil
}
return systemOneModelAllowed(cfg)
}
// backendSupportsScore reports whether the named backend implements the
// Score gRPC RPC. vllm-cpp does (kev/laya decision pipeline and cua-s1-forms
// scoring via the unified vllm_decide C ABI); other backends fall through to
@@ -408,6 +437,9 @@ func SystemOneEndpoint(app *application.Application) echo.HandlerFunc {
if req.Model == "" {
return systemOneError(c, http.StatusBadRequest, "model is required")
}
if err := checkSystemOneModel(app, req.Model); err != nil {
return systemOneError(c, http.StatusBadRequest, err.Error())
}
// vllm-cpp models (kev/laya) implement the decision pipeline natively
// via the vllm_decide C ABI. Forward the raw request JSON through the
// Score RPC and return the backend's response as-is.
@@ -474,6 +506,9 @@ func SystemOnePermuteEndpoint(app *application.Application) echo.HandlerFunc {
if req.Request.Model == "" {
return systemOneError(c, http.StatusBadRequest, "model is required")
}
if err := checkSystemOneModel(app, req.Request.Model); err != nil {
return systemOneError(c, http.StatusBadRequest, err.Error())
}
if req.Question == "" {
return systemOneError(c, http.StatusBadRequest, "question is required")
}
@@ -610,6 +645,9 @@ func SystemOneSeparateEndpoint(app *application.Application) echo.HandlerFunc {
if req.Model == "" {
return systemOneError(c, http.StatusBadRequest, "model is required")
}
if err := checkSystemOneModel(app, req.Model); err != nil {
return systemOneError(c, http.StatusBadRequest, err.Error())
}
parsed, err := parseSystemOneRequest(&req)
if err != nil {
return systemOneError(c, http.StatusBadRequest, err.Error())
@@ -0,0 +1,34 @@
package localai
import (
"github.com/mudler/LocalAI/core/config"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("systemOneModelAllowed", func() {
mk := func(usecases ...string) config.ModelConfig {
return config.ModelConfig{
Name: "m",
Backend: "vllm-cpp",
KnownUsecases: config.GetUsecasesFromYAML(usecases),
}
}
It("accepts a declared systemone model", func() {
Expect(systemOneModelAllowed(mk("systemone"))).To(Succeed())
})
It("accepts a token_classify model, which the NER path serves", func() {
Expect(systemOneModelAllowed(mk("token_classify"))).To(Succeed())
})
It("keeps configs that declare no usecases working", func() {
Expect(systemOneModelAllowed(config.ModelConfig{Name: "laya", Backend: "vllm-cpp"})).To(Succeed())
})
It("refuses a chat-only model with an actionable message", func() {
Expect(systemOneModelAllowed(mk("chat"))).To(MatchError(ContainSubstring("known_usecases: [systemone]")))
})
})