From 882d320020d49719592dce2df1b2aea44a7b42d4 Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Sat, 20 Jun 2026 09:45:45 +0000 Subject: [PATCH] feat(modeladmin): reject alias configs with invalid targets on create/edit Validate alias targets at create/swap entry points (ImportModelEndpoint, EditYAML, PatchConfig) so a dangling, chained, or disabled alias target is rejected at save time rather than surfacing as a runtime error. Assisted-by: Claude:opus-4-8 [Claude Code] Signed-off-by: Ettore Di Giacinto --- core/http/endpoints/localai/import_model.go | 6 ++++++ core/services/modeladmin/config.go | 6 ++++++ core/services/modeladmin/config_test.go | 18 ++++++++++++++++++ 3 files changed, 30 insertions(+) diff --git a/core/http/endpoints/localai/import_model.go b/core/http/endpoints/localai/import_model.go index dc225abdd..54a80a9cc 100644 --- a/core/http/endpoints/localai/import_model.go +++ b/core/http/endpoints/localai/import_model.go @@ -181,6 +181,12 @@ func ImportModelEndpoint(cl *config.ModelConfigLoader, appConfig *config.Applica return c.JSON(http.StatusBadRequest, ModelResponse{Success: false, Error: msg}) } + // Reject aliases whose target is missing, chained, or disabled so a + // dangling alias can't be persisted and surface as a runtime error later. + if err := cl.ValidateAliasTarget(&modelConfig); err != nil { + return c.JSON(http.StatusBadRequest, ModelResponse{Success: false, Error: err.Error()}) + } + // Create the configuration file configPath := filepath.Join(appConfig.SystemState.Model.ModelsPath, modelConfig.Name+".yaml") if err := utils.VerifyPath(modelConfig.Name+".yaml", appConfig.SystemState.Model.ModelsPath); err != nil { diff --git a/core/services/modeladmin/config.go b/core/services/modeladmin/config.go index c01e2fb4c..f4fc53d97 100644 --- a/core/services/modeladmin/config.go +++ b/core/services/modeladmin/config.go @@ -130,6 +130,9 @@ func (s *ConfigService) PatchConfig(_ context.Context, name string, patch map[st } return nil, ErrInvalidConfig } + if err := s.Loader.ValidateAliasTarget(&updated); err != nil { + return nil, fmt.Errorf("%w: %v", ErrInvalidConfig, err) + } if err := writeFileAtomic(configPath, yamlData, 0644); err != nil { return nil, fmt.Errorf("write config file: %w", err) } @@ -215,6 +218,9 @@ func (s *ConfigService) EditYAML(_ context.Context, name string, body []byte, ml if valid, _ := req.Validate(); !valid { return nil, ErrInvalidConfig } + if err := s.Loader.ValidateAliasTarget(&req); err != nil { + return nil, fmt.Errorf("%w: %v", ErrInvalidConfig, err) + } configPath := existing.GetModelConfigFile() modelsPath := s.modelsPath() diff --git a/core/services/modeladmin/config_test.go b/core/services/modeladmin/config_test.go index d4157047d..36569c19b 100644 --- a/core/services/modeladmin/config_test.go +++ b/core/services/modeladmin/config_test.go @@ -211,5 +211,23 @@ var _ = Describe("ConfigService", func() { _, err := svc.EditYAML(ctx, "alpha", nil, nil) Expect(err).To(MatchError(ErrEmptyBody)) }) + + It("rejects editing a config into an alias with a missing target", func() { + writeModelYAML(svc, dir, "base", map[string]any{"backend": "llama-cpp"}) + + body := []byte("name: base\nalias: ghost\n") + _, err := svc.EditYAML(ctx, "base", body, nil) + Expect(err).To(MatchError(ErrInvalidConfig)) + Expect(err.Error()).To(ContainSubstring("ghost")) + }) + + It("accepts editing a config into an alias with a real target", func() { + writeModelYAML(svc, dir, "base", map[string]any{"backend": "llama-cpp"}) + writeModelYAML(svc, dir, "target", map[string]any{"backend": "llama-cpp"}) + + body := []byte("name: base\nalias: target\n") + _, err := svc.EditYAML(ctx, "base", body, nil) + Expect(err).ToNot(HaveOccurred()) + }) }) })