mirror of
https://github.com/mudler/LocalAI.git
synced 2026-06-22 15:49:12 -04:00
Compare commits
1 Commits
feat/recon
...
fix/model-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b451058cc |
@@ -152,9 +152,15 @@ func (re *RequestExtractor) SetModelAndConfig(initializer func() schema.LocalAIR
|
|||||||
|
|
||||||
// If a model name was specified, verify it actually exists before proceeding.
|
// If a model name was specified, verify it actually exists before proceeding.
|
||||||
// Check both configured models and loose model files in the model path.
|
// Check both configured models and loose model files in the model path.
|
||||||
// Skip the check for HuggingFace model IDs (contain "/") since backends
|
// Skip the check only for HuggingFace-style model IDs ("org/repo") that
|
||||||
// like diffusers may download these on the fly.
|
// backends like diffusers may download on the fly. A name that points at a
|
||||||
if modelName != "" && !strings.Contains(modelName, "/") {
|
// concrete weight file (e.g. "local/model.gguf") is NOT such an ID: it must
|
||||||
|
// still be verified, otherwise a wrong name silently falls through to the
|
||||||
|
// gallery autoloader and triggers a surprising download (issue #10162).
|
||||||
|
// CheckIfModelExists resolves relative paths against the models dir, so a
|
||||||
|
// loose weight file addressed by path still passes.
|
||||||
|
isRemoteModelID := strings.Contains(modelName, "/") && !model.HasKnownModelFileExtension(modelName)
|
||||||
|
if modelName != "" && !isRemoteModelID {
|
||||||
exists, existsErr := galleryop.CheckIfModelExists(re.modelConfigLoader, re.modelLoader, modelName, galleryop.ALWAYS_INCLUDE)
|
exists, existsErr := galleryop.CheckIfModelExists(re.modelConfigLoader, re.modelLoader, modelName, galleryop.ALWAYS_INCLUDE)
|
||||||
if existsErr == nil && !exists {
|
if existsErr == nil && !exists {
|
||||||
return c.JSON(http.StatusNotFound, schema.ErrorResponse{
|
return c.JSON(http.StatusNotFound, schema.ErrorResponse{
|
||||||
|
|||||||
@@ -140,6 +140,40 @@ var _ = Describe("SetModelAndConfig middleware", func() {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Context("when the model name is a file path to a weight that does not exist", func() {
|
||||||
|
// A name like "local/model.gguf" is the parameters.model weight path, not a
|
||||||
|
// HuggingFace org/repo ID. The slash must not exempt it from the existence
|
||||||
|
// check, otherwise a wrong name silently falls through to the gallery
|
||||||
|
// autoloader and triggers a surprising download (issue #10162).
|
||||||
|
It("returns 404 instead of passing through", func() {
|
||||||
|
rec := postJSON(app, "/v1/chat/completions",
|
||||||
|
`{"model":"local/missing-model.gguf","messages":[{"role":"user","content":"hi"}]}`)
|
||||||
|
|
||||||
|
Expect(rec.Code).To(Equal(http.StatusNotFound))
|
||||||
|
|
||||||
|
var resp schema.ErrorResponse
|
||||||
|
Expect(json.Unmarshal(rec.Body.Bytes(), &resp)).To(Succeed())
|
||||||
|
Expect(resp.Error).ToNot(BeNil())
|
||||||
|
Expect(resp.Error.Message).To(ContainSubstring("local/missing-model.gguf"))
|
||||||
|
Expect(resp.Error.Message).To(ContainSubstring("not found"))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Context("when the model name is a file path to a weight that exists on disk", func() {
|
||||||
|
// The same path, but the loose weight file is actually present in a
|
||||||
|
// subdirectory of the models path: the request must pass through so users
|
||||||
|
// can address a raw weight file by its relative path.
|
||||||
|
It("passes through to the handler", func() {
|
||||||
|
Expect(os.MkdirAll(filepath.Join(modelDir, "local"), 0755)).To(Succeed())
|
||||||
|
Expect(os.WriteFile(filepath.Join(modelDir, "local", "present-model.gguf"), []byte("weights"), 0644)).To(Succeed())
|
||||||
|
|
||||||
|
rec := postJSON(app, "/v1/chat/completions",
|
||||||
|
`{"model":"local/present-model.gguf","messages":[{"role":"user","content":"hi"}]}`)
|
||||||
|
|
||||||
|
Expect(rec.Code).To(Equal(http.StatusOK))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
Context("when no model is specified", func() {
|
Context("when no model is specified", func() {
|
||||||
It("passes through without checking", func() {
|
It("passes through without checking", func() {
|
||||||
rec := postJSON(app, "/v1/chat/completions",
|
rec := postJSON(app, "/v1/chat/completions",
|
||||||
|
|||||||
@@ -207,6 +207,28 @@ var knownModelsNameSuffixToSkip []string = []string{
|
|||||||
".tar.gz",
|
".tar.gz",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HasKnownModelFileExtension reports whether name ends in a file extension that
|
||||||
|
// LocalAI recognizes as a model weight or asset file (e.g. ".gguf",
|
||||||
|
// ".safetensors", ".json"). It is used to tell a concrete file path such as
|
||||||
|
// "local/model.gguf" apart from a HuggingFace-style repository ID like
|
||||||
|
// "org/repo": only the former carries a recognized suffix. A version-style
|
||||||
|
// suffix such as the ".0" in "stabilityai/stable-diffusion-xl-base-1.0" is not
|
||||||
|
// in the list, so such repo IDs are correctly treated as non-files.
|
||||||
|
func HasKnownModelFileExtension(name string) bool {
|
||||||
|
lower := strings.ToLower(name)
|
||||||
|
for _, suffix := range knownModelsNameSuffixToSkip {
|
||||||
|
// "." is a guard entry consumed by ListFilesInModelPath, not a real
|
||||||
|
// extension; skip it so it doesn't match every dotted name.
|
||||||
|
if suffix == "." {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(lower, strings.ToLower(suffix)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
const retryTimeout = time.Duration(2 * time.Minute)
|
const retryTimeout = time.Duration(2 * time.Minute)
|
||||||
|
|
||||||
func (ml *ModelLoader) ListFilesInModelPath() ([]string, error) {
|
func (ml *ModelLoader) ListFilesInModelPath() ([]string, error) {
|
||||||
|
|||||||
@@ -58,6 +58,23 @@ var _ = Describe("ModelLoader", func() {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Context("HasKnownModelFileExtension", func() {
|
||||||
|
It("returns true for concrete weight/asset file paths", func() {
|
||||||
|
Expect(model.HasKnownModelFileExtension("local/model.gguf")).To(BeTrue())
|
||||||
|
Expect(model.HasKnownModelFileExtension("model.safetensors")).To(BeTrue())
|
||||||
|
Expect(model.HasKnownModelFileExtension("foo/bar.GGUF")).To(BeTrue())
|
||||||
|
Expect(model.HasKnownModelFileExtension("config.json")).To(BeTrue())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns false for HuggingFace-style repository IDs", func() {
|
||||||
|
// org/repo carries no recognized file extension...
|
||||||
|
Expect(model.HasKnownModelFileExtension("bartowski/DeepSeek-R1-Distill-Qwen-1.5B-GGUF")).To(BeFalse())
|
||||||
|
// ...and a version suffix like ".0" is not a known model extension.
|
||||||
|
Expect(model.HasKnownModelFileExtension("stabilityai/stable-diffusion-xl-base-1.0")).To(BeFalse())
|
||||||
|
Expect(model.HasKnownModelFileExtension("plain-model-name")).To(BeFalse())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
Context("ListFilesInModelPath", func() {
|
Context("ListFilesInModelPath", func() {
|
||||||
It("should list all valid model files in the model path", func() {
|
It("should list all valid model files in the model path", func() {
|
||||||
os.Create(filepath.Join(modelPath, "test.model"))
|
os.Create(filepath.Join(modelPath, "test.model"))
|
||||||
|
|||||||
Reference in New Issue
Block a user