diff --git a/core/http/endpoints/localai/systemone.go b/core/http/endpoints/localai/systemone.go index e272e1b8b..435414d39 100644 --- a/core/http/endpoints/localai/systemone.go +++ b/core/http/endpoints/localai/systemone.go @@ -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()) diff --git a/core/http/endpoints/localai/systemone_gate_test.go b/core/http/endpoints/localai/systemone_gate_test.go new file mode 100644 index 000000000..b7857da56 --- /dev/null +++ b/core/http/endpoints/localai/systemone_gate_test.go @@ -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]"))) + }) +})