diff --git a/core/config/model_config_loader.go b/core/config/model_config_loader.go index 89f4bc5cb..e2f43e83f 100644 --- a/core/config/model_config_loader.go +++ b/core/config/model_config_loader.go @@ -294,6 +294,44 @@ func (bcl *ModelConfigLoader) UpdateModelConfig(m string, updater func(*ModelCon } } +// ResolveAlias follows a one-hop alias to its target config. Returns +// (resolved, wasAlias, err). Non-alias configs return (cfg, false, nil) +// unchanged. Strict: the target must exist and must not itself be an alias +// (chains are rejected). The returned config is a copy of the target. +func (bcl *ModelConfigLoader) ResolveAlias(cfg *ModelConfig) (*ModelConfig, bool, error) { + if cfg == nil || !cfg.IsAlias() { + return cfg, false, nil + } + target, exists := bcl.GetModelConfig(cfg.Alias) + if !exists { + return nil, true, fmt.Errorf("alias %q points to unknown model %q", cfg.Name, cfg.Alias) + } + if target.IsAlias() { + return nil, true, fmt.Errorf("alias %q points to another alias %q (chains are not allowed)", cfg.Name, cfg.Alias) + } + return &target, true, nil +} + +// ValidateAliasTarget checks an alias config's target at create/swap time: +// the target must exist, must not be an alias, and must not be disabled. +// Returns nil for non-alias configs. +func (bcl *ModelConfigLoader) ValidateAliasTarget(cfg *ModelConfig) error { + if cfg == nil || !cfg.IsAlias() { + return nil + } + target, exists := bcl.GetModelConfig(cfg.Alias) + if !exists { + return fmt.Errorf("alias target %q does not exist", cfg.Alias) + } + if target.IsAlias() { + return fmt.Errorf("alias target %q is itself an alias (chains are not allowed)", cfg.Alias) + } + if target.IsDisabled() { + return fmt.Errorf("alias target %q is disabled", cfg.Alias) + } + return nil +} + // Preload prepare models if they are not local but url or huggingface repositories func (bcl *ModelConfigLoader) Preload(modelPath string) error { bcl.Lock() @@ -475,5 +513,21 @@ func (bcl *ModelConfigLoader) LoadModelConfigsFromPath(path string, opts ...Conf } } + // Surface aliases whose targets are missing or themselves aliases. These + // resolve to a clear request-time error; warning here gives operators + // visibility without failing startup. + for name, c := range bcl.configs { + if !c.IsAlias() { + continue + } + target, ok := bcl.configs[c.Alias] + switch { + case !ok: + xlog.Warn("alias points to unknown model", "alias", name, "target", c.Alias) + case target.IsAlias(): + xlog.Warn("alias points to another alias (chains are not allowed)", "alias", name, "target", c.Alias) + } + } + return nil } diff --git a/core/config/model_config_loader_test.go b/core/config/model_config_loader_test.go index 924a4d1e4..06ab65a20 100644 --- a/core/config/model_config_loader_test.go +++ b/core/config/model_config_loader_test.go @@ -61,3 +61,51 @@ var _ = Describe("ModelConfigLoader.GetModelsConflictingWith", func() { Expect(bcl.GetModelsConflictingWith("a")).To(ConsistOf("b")) }) }) + +var _ = Describe("ModelConfigLoader alias resolution", func() { + var loader *ModelConfigLoader + + BeforeEach(func() { + loader = NewModelConfigLoader("") + loader.configs["real"] = ModelConfig{Name: "real", Backend: "llama-cpp"} + loader.configs["gpt-4"] = ModelConfig{Name: "gpt-4", Alias: "real"} + loader.configs["chain"] = ModelConfig{Name: "chain", Alias: "gpt-4"} + loader.configs["dangling"] = ModelConfig{Name: "dangling", Alias: "nope"} + }) + + It("returns non-alias configs unchanged", func() { + cfg := loader.configs["real"] + got, was, err := loader.ResolveAlias(&cfg) + Expect(err).ToNot(HaveOccurred()) + Expect(was).To(BeFalse()) + Expect(got.Name).To(Equal("real")) + }) + + It("resolves an alias to its target", func() { + cfg := loader.configs["gpt-4"] + got, was, err := loader.ResolveAlias(&cfg) + Expect(err).ToNot(HaveOccurred()) + Expect(was).To(BeTrue()) + Expect(got.Name).To(Equal("real")) + }) + + It("rejects an alias chain", func() { + cfg := loader.configs["chain"] + _, was, err := loader.ResolveAlias(&cfg) + Expect(was).To(BeTrue()) + Expect(err).To(MatchError(ContainSubstring("chains are not allowed"))) + }) + + It("rejects a dangling alias", func() { + cfg := loader.configs["dangling"] + _, _, err := loader.ResolveAlias(&cfg) + Expect(err).To(MatchError(ContainSubstring("unknown model"))) + }) + + It("ValidateAliasTarget passes for a real target and fails for a chain", func() { + good := loader.configs["gpt-4"] + Expect(loader.ValidateAliasTarget(&good)).ToNot(HaveOccurred()) + bad := loader.configs["chain"] + Expect(loader.ValidateAliasTarget(&bad)).To(MatchError(ContainSubstring("itself an alias"))) + }) +})