mirror of
https://github.com/mudler/LocalAI.git
synced 2026-10-28 09:08:54 -04:00
docs(models): document the queued manual imports — swagger, specs, docs
- swagger: annotate POST /models/import (untyped body: documenting it
as config.ModelConfig would inline the whole type graph, ~5.5k
generated lines; the description spells out the dual response) and
regenerate the spec
- tests: pin the admission contract of the queued branch — job id +
status URL, job queryable before the worker runs, config not written
at admission, preload-matching filenames, checksum passthrough, and
the untouched synchronous path
- docs: the Writing YAML section explains when Create queues and what
the API answers in each case
Assisted-by: Claude:claude-fable-5
Signed-off-by: Plamen K. Kosseff <p.kosseff@gmail.com>
(cherry picked from commit aec658a998)
This commit is contained in:
1 parent
28c4bb9277
commit
a50eb4e68a
6 files changed
+219
No files matched your search
@@ -125,6 +125,20 @@ func ImportModelURIEndpoint(cl *config.ModelConfigLoader, appConfig *config.Appl
|
||||
}
|
||||
|
||||
// ImportModelEndpoint handles creating new model configurations
|
||||
// @Summary Import a model from a raw YAML or JSON configuration
|
||||
// @Description Writes the posted model configuration. A config referencing
|
||||
// @Description remote assets (download_files entries, URI-valued model/mmproj)
|
||||
// @Description is enqueued on the gallery job queue and answered with a job id
|
||||
// @Description plus status URL to poll, like a gallery install; a config with
|
||||
// @Description no downloads is written synchronously and answered with a
|
||||
// @Description plain success response.
|
||||
// @Tags models
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body object true "model configuration (YAML or JSON body)"
|
||||
// @Success 200 {object} schema.GalleryResponse "remote assets present: job id and status URL"
|
||||
// @Failure 400 {object} localai.ModelResponse "invalid or incomplete configuration"
|
||||
// @Router /models/import [post]
|
||||
func ImportModelEndpoint(cl *config.ModelConfigLoader, gs *galleryop.GalleryService, appConfig *config.ApplicationConfig, opcache *galleryop.OpCache) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
// Get the raw body
|
||||
|
||||
@@ -7,9 +7,11 @@ import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/mudler/LocalAI/core/config"
|
||||
"github.com/mudler/LocalAI/core/gallery"
|
||||
. "github.com/mudler/LocalAI/core/http/endpoints/localai"
|
||||
"github.com/mudler/LocalAI/core/services/galleryop"
|
||||
"github.com/mudler/LocalAI/pkg/model"
|
||||
@@ -120,3 +122,80 @@ var _ = Describe("ImportModelURIEndpoint ambiguity handling", func() {
|
||||
Expect(candidates).ToNot(ContainElement("vllm"))
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("ImportModelEndpoint queued imports", func() {
|
||||
var (
|
||||
tempDir string
|
||||
app *echo.Echo
|
||||
galleryService *galleryop.GalleryService
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
tempDir = GinkgoT().TempDir()
|
||||
systemState, err := system.GetSystemState(system.WithModelPath(tempDir))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
applicationConfig := config.NewApplicationConfig(config.WithSystemState(systemState))
|
||||
modelConfigLoader := config.NewModelConfigLoader(systemState.Model.ModelsPath)
|
||||
ml := model.NewModelLoader(systemState)
|
||||
galleryService = galleryop.NewGalleryService(applicationConfig, ml)
|
||||
// The worker is deliberately NOT started: these specs assert the
|
||||
// admission contract (response shape, job visibility, op payload),
|
||||
// not the download itself.
|
||||
app = echo.New()
|
||||
app.POST("/models/import", ImportModelEndpoint(modelConfigLoader, galleryService, applicationConfig, nil))
|
||||
})
|
||||
|
||||
post := func(body string) *httptest.ResponseRecorder {
|
||||
req := httptest.NewRequest("POST", "/models/import", bytes.NewBufferString(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
app.ServeHTTP(rec, req)
|
||||
return rec
|
||||
}
|
||||
|
||||
It("enqueues a config whose model is a URL and answers with a pollable job", func() {
|
||||
rec := post(`{"name": "remote", "backend": "llama-cpp", "parameters": {"model": "https://example.com/weights/foo.gguf"}}`)
|
||||
Expect(rec.Code).To(Equal(http.StatusOK))
|
||||
|
||||
var resp map[string]any
|
||||
Expect(json.Unmarshal(rec.Body.Bytes(), &resp)).To(Succeed())
|
||||
Expect(resp).To(HaveKey("uuid"))
|
||||
Expect(resp["status"]).To(ContainSubstring("/models/jobs/" + resp["uuid"].(string)))
|
||||
|
||||
// Queryable from the instant the ID is handed out, before any
|
||||
// worker picks the op up.
|
||||
Expect(galleryService.GetStatus(resp["uuid"].(string))).ToNot(BeNil())
|
||||
|
||||
// The op writes the config once downloads finish; admission must
|
||||
// not have written it already.
|
||||
Expect(filepath.Join(tempDir, "remote.yaml")).ToNot(BeAnExistingFile())
|
||||
|
||||
var op galleryop.ManagementOp[gallery.GalleryModel, gallery.ModelConfig]
|
||||
Eventually(galleryService.ModelGalleryChannel).Should(Receive(&op))
|
||||
Expect(op.GalleryElementName).To(Equal("remote"))
|
||||
// Preload-matching name: the loader later finds the file under the
|
||||
// name the config resolves, so the config needs no rewriting.
|
||||
Expect(op.GalleryElement.Files).To(HaveLen(1))
|
||||
Expect(op.GalleryElement.Files[0].Filename).To(Equal("foo.gguf"))
|
||||
Expect(op.GalleryElement.Files[0].URI).To(Equal("https://example.com/weights/foo.gguf"))
|
||||
Expect(op.GalleryElement.ConfigFile).To(ContainSubstring("name: remote"))
|
||||
})
|
||||
|
||||
It("carries download_files entries with their checksums into the op", func() {
|
||||
rec := post(`{"name": "assets", "backend": "llama-cpp", "parameters": {"model": "weights.gguf"}, "download_files": [{"filename": "weights.gguf", "sha256": "abc123", "uri": "https://example.com/weights.gguf"}]}`)
|
||||
Expect(rec.Code).To(Equal(http.StatusOK))
|
||||
var op galleryop.ManagementOp[gallery.GalleryModel, gallery.ModelConfig]
|
||||
Eventually(galleryService.ModelGalleryChannel).Should(Receive(&op))
|
||||
Expect(op.GalleryElement.Files).To(HaveLen(1))
|
||||
Expect(op.GalleryElement.Files[0].SHA256).To(Equal("abc123"))
|
||||
})
|
||||
|
||||
It("keeps the synchronous path for configs without remote assets", func() {
|
||||
rec := post(`{"name": "local", "backend": "llama-cpp", "parameters": {"model": "already-here.gguf"}}`)
|
||||
Expect(rec.Code).To(Equal(http.StatusOK))
|
||||
Expect(rec.Body.String()).To(ContainSubstring("created successfully"))
|
||||
Expect(filepath.Join(tempDir, "local.yaml")).To(BeAnExistingFile())
|
||||
// Nothing was enqueued for a config with no downloads.
|
||||
Consistently(galleryService.ModelGalleryChannel, "100ms").ShouldNot(Receive())
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user