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>
This commit is contained in:
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
|
// 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 {
|
func ImportModelEndpoint(cl *config.ModelConfigLoader, gs *galleryop.GalleryService, appConfig *config.ApplicationConfig, opcache *galleryop.OpCache) echo.HandlerFunc {
|
||||||
return func(c echo.Context) error {
|
return func(c echo.Context) error {
|
||||||
// Get the raw body
|
// Get the raw body
|
||||||
|
|||||||
@@ -7,9 +7,11 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"github.com/mudler/LocalAI/core/config"
|
"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/http/endpoints/localai"
|
||||||
"github.com/mudler/LocalAI/core/services/galleryop"
|
"github.com/mudler/LocalAI/core/services/galleryop"
|
||||||
"github.com/mudler/LocalAI/pkg/model"
|
"github.com/mudler/LocalAI/pkg/model"
|
||||||
@@ -120,3 +122,80 @@ var _ = Describe("ImportModelURIEndpoint ambiguity handling", func() {
|
|||||||
Expect(candidates).ToNot(ContainElement("vllm"))
|
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())
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -121,6 +121,20 @@ This is especially useful for:
|
|||||||
- Setting up complex model setups
|
- Setting up complex model setups
|
||||||
- Editing existing model configurations
|
- Editing existing model configurations
|
||||||
|
|
||||||
|
When the configuration references remote assets — a URL in
|
||||||
|
`parameters.model` or `mmproj`, or `download_files` entries — **Create**
|
||||||
|
does not download them inside the request: the import is queued like a
|
||||||
|
gallery installation, and the page reports the job's progress until the
|
||||||
|
model is ready. The configuration file is written once every download
|
||||||
|
has finished, so a failed download never leaves a config pointing at
|
||||||
|
files that do not exist. A configuration that only references files
|
||||||
|
already on disk is written immediately, as before.
|
||||||
|
|
||||||
|
The same split applies to the API behind the page: `POST /models/import`
|
||||||
|
answers a queued import with a job id and a status URL to poll
|
||||||
|
(`/models/jobs/{id}` — the same shape gallery installs use), and a
|
||||||
|
download-free import with a plain success response.
|
||||||
|
|
||||||
## Method 2: Installing from Hugging Face
|
## Method 2: Installing from Hugging Face
|
||||||
|
|
||||||
LocalAI can directly install models from Hugging Face:
|
LocalAI can directly install models from Hugging Face:
|
||||||
|
|||||||
@@ -2754,6 +2754,46 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/models/import": {
|
||||||
|
"post": {
|
||||||
|
"description": "Writes the posted model configuration. A config referencing\nremote assets (download_files entries, URI-valued model/mmproj)\nis enqueued on the gallery job queue and answered with a job id\nplus status URL to poll, like a gallery install; a config with\nno downloads is written synchronously and answered with a\nplain success response.",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"models"
|
||||||
|
],
|
||||||
|
"summary": "Import a model from a raw YAML or JSON configuration",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "model configuration (YAML or JSON body)",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "remote assets present: job id and status URL",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/schema.GalleryResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "invalid or incomplete configuration",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/localai.ModelResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/models/jobs": {
|
"/models/jobs": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
|
|||||||
@@ -2751,6 +2751,46 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/models/import": {
|
||||||
|
"post": {
|
||||||
|
"description": "Writes the posted model configuration. A config referencing\nremote assets (download_files entries, URI-valued model/mmproj)\nis enqueued on the gallery job queue and answered with a job id\nplus status URL to poll, like a gallery install; a config with\nno downloads is written synchronously and answered with a\nplain success response.",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"models"
|
||||||
|
],
|
||||||
|
"summary": "Import a model from a raw YAML or JSON configuration",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "model configuration (YAML or JSON body)",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "remote assets present: job id and status URL",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/schema.GalleryResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "invalid or incomplete configuration",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/localai.ModelResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/models/jobs": {
|
"/models/jobs": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
|
|||||||
@@ -5301,6 +5301,38 @@ paths:
|
|||||||
summary: List all Galleries
|
summary: List all Galleries
|
||||||
tags:
|
tags:
|
||||||
- models
|
- models
|
||||||
|
/models/import:
|
||||||
|
post:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: |-
|
||||||
|
Writes the posted model configuration. A config referencing
|
||||||
|
remote assets (download_files entries, URI-valued model/mmproj)
|
||||||
|
is enqueued on the gallery job queue and answered with a job id
|
||||||
|
plus status URL to poll, like a gallery install; a config with
|
||||||
|
no downloads is written synchronously and answered with a
|
||||||
|
plain success response.
|
||||||
|
parameters:
|
||||||
|
- description: model configuration (YAML or JSON body)
|
||||||
|
in: body
|
||||||
|
name: request
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: 'remote assets present: job id and status URL'
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/schema.GalleryResponse'
|
||||||
|
"400":
|
||||||
|
description: invalid or incomplete configuration
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/localai.ModelResponse'
|
||||||
|
summary: Import a model from a raw YAML or JSON configuration
|
||||||
|
tags:
|
||||||
|
- models
|
||||||
/models/jobs:
|
/models/jobs:
|
||||||
get:
|
get:
|
||||||
responses:
|
responses:
|
||||||
|
|||||||
Reference in new issue
Block a user