mirror of
https://github.com/mudler/LocalAI.git
synced 2026-07-12 09:20:23 -04:00
We can't use the mutate.Extract written bytes as current status as that will be bigger than the compressed image size. Image manifest don't have any guarantee of the type of artifact (can be compressed or not) when showing the layer size. Split the extraction process in two parts: Downloading and extracting as a flattened system, in this way we can display the status of downloading and extracting accordingly. This change also fixes a small nuance in detecting installed backends, now it's more consistent and looks if a metadata.json and/or a path with a `run.sh` file is present. Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
283 lines
8.2 KiB
Go
283 lines
8.2 KiB
Go
package routes
|
|
|
|
import (
|
|
"fmt"
|
|
"html/template"
|
|
"math"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
"github.com/google/uuid"
|
|
"github.com/microcosm-cc/bluemonday"
|
|
"github.com/mudler/LocalAI/core/config"
|
|
"github.com/mudler/LocalAI/core/gallery"
|
|
"github.com/mudler/LocalAI/core/http/elements"
|
|
"github.com/mudler/LocalAI/core/http/utils"
|
|
"github.com/mudler/LocalAI/core/p2p"
|
|
"github.com/mudler/LocalAI/core/services"
|
|
"github.com/mudler/LocalAI/internal"
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
func registerGalleryRoutes(app *fiber.App, cl *config.BackendConfigLoader, appConfig *config.ApplicationConfig, galleryService *services.GalleryService, opcache *services.OpCache) {
|
|
|
|
// Show the Models page (all models)
|
|
app.Get("/browse", func(c *fiber.Ctx) error {
|
|
term := c.Query("term")
|
|
page := c.Query("page")
|
|
items := c.Query("items")
|
|
|
|
models, err := gallery.AvailableGalleryModels(appConfig.Galleries, appConfig.ModelPath)
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("could not list models from galleries")
|
|
return c.Status(fiber.StatusInternalServerError).Render("views/error", fiber.Map{
|
|
"Title": "LocalAI - Models",
|
|
"BaseURL": utils.BaseURL(c),
|
|
"Version": internal.PrintableVersion(),
|
|
"ErrorCode": "500",
|
|
"ErrorMessage": err.Error(),
|
|
})
|
|
}
|
|
|
|
// Get all available tags
|
|
allTags := map[string]struct{}{}
|
|
tags := []string{}
|
|
for _, m := range models {
|
|
for _, t := range m.Tags {
|
|
allTags[t] = struct{}{}
|
|
}
|
|
}
|
|
for t := range allTags {
|
|
tags = append(tags, t)
|
|
}
|
|
sort.Strings(tags)
|
|
|
|
if term != "" {
|
|
models = gallery.GalleryElements[*gallery.GalleryModel](models).Search(term)
|
|
}
|
|
|
|
// Get model statuses
|
|
processingModelsData, taskTypes := opcache.GetStatus()
|
|
|
|
summary := fiber.Map{
|
|
"Title": "LocalAI - Models",
|
|
"BaseURL": utils.BaseURL(c),
|
|
"Version": internal.PrintableVersion(),
|
|
"Models": template.HTML(elements.ListModels(models, opcache, galleryService)),
|
|
"Repositories": appConfig.Galleries,
|
|
"AllTags": tags,
|
|
"ProcessingModels": processingModelsData,
|
|
"AvailableModels": len(models),
|
|
"IsP2PEnabled": p2p.IsP2PEnabled(),
|
|
|
|
"TaskTypes": taskTypes,
|
|
// "ApplicationConfig": appConfig,
|
|
}
|
|
|
|
if page == "" {
|
|
page = "1"
|
|
}
|
|
|
|
if page != "" {
|
|
// return a subset of the models
|
|
pageNum, err := strconv.Atoi(page)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusBadRequest).SendString("Invalid page number")
|
|
}
|
|
|
|
if pageNum == 0 {
|
|
return c.Render("views/models", summary)
|
|
}
|
|
|
|
itemsNum, err := strconv.Atoi(items)
|
|
if err != nil {
|
|
itemsNum = 21
|
|
}
|
|
|
|
totalPages := int(math.Ceil(float64(len(models)) / float64(itemsNum)))
|
|
|
|
models = models.Paginate(pageNum, itemsNum)
|
|
|
|
prevPage := pageNum - 1
|
|
nextPage := pageNum + 1
|
|
if prevPage < 1 {
|
|
prevPage = 1
|
|
}
|
|
if nextPage > totalPages {
|
|
nextPage = totalPages
|
|
}
|
|
if prevPage != pageNum {
|
|
summary["PrevPage"] = prevPage
|
|
}
|
|
summary["NextPage"] = nextPage
|
|
summary["TotalPages"] = totalPages
|
|
summary["CurrentPage"] = pageNum
|
|
summary["Models"] = template.HTML(elements.ListModels(models, opcache, galleryService))
|
|
}
|
|
|
|
// Render index
|
|
return c.Render("views/models", summary)
|
|
})
|
|
|
|
// Show the models, filtered from the user input
|
|
// https://htmx.org/examples/active-search/
|
|
app.Post("/browse/search/models", func(c *fiber.Ctx) error {
|
|
page := c.Query("page")
|
|
items := c.Query("items")
|
|
|
|
form := struct {
|
|
Search string `form:"search"`
|
|
}{}
|
|
if err := c.BodyParser(&form); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).SendString(bluemonday.StrictPolicy().Sanitize(err.Error()))
|
|
}
|
|
|
|
models, _ := gallery.AvailableGalleryModels(appConfig.Galleries, appConfig.ModelPath)
|
|
|
|
if page != "" {
|
|
// return a subset of the models
|
|
pageNum, err := strconv.Atoi(page)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusBadRequest).SendString("Invalid page number")
|
|
}
|
|
|
|
itemsNum, err := strconv.Atoi(items)
|
|
if err != nil {
|
|
itemsNum = 21
|
|
}
|
|
|
|
models = models.Paginate(pageNum, itemsNum)
|
|
}
|
|
|
|
if form.Search != "" {
|
|
models = models.Search(form.Search)
|
|
}
|
|
|
|
return c.SendString(elements.ListModels(models, opcache, galleryService))
|
|
})
|
|
|
|
/*
|
|
|
|
Install routes
|
|
|
|
*/
|
|
|
|
// This route is used when the "Install" button is pressed, we submit here a new job to the gallery service
|
|
// https://htmx.org/examples/progress-bar/
|
|
app.Post("/browse/install/model/:id", func(c *fiber.Ctx) error {
|
|
galleryID := strings.Clone(c.Params("id")) // note: strings.Clone is required for multiple requests!
|
|
log.Debug().Msgf("UI job submitted to install : %+v\n", galleryID)
|
|
|
|
id, err := uuid.NewUUID()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
uid := id.String()
|
|
|
|
opcache.Set(galleryID, uid)
|
|
|
|
op := services.GalleryOp[gallery.GalleryModel]{
|
|
ID: uid,
|
|
GalleryElementName: galleryID,
|
|
Galleries: appConfig.Galleries,
|
|
}
|
|
go func() {
|
|
galleryService.ModelGalleryChannel <- op
|
|
}()
|
|
|
|
return c.SendString(elements.StartModelProgressBar(uid, "0", "Installation"))
|
|
})
|
|
|
|
// This route is used when the "Install" button is pressed, we submit here a new job to the gallery service
|
|
// https://htmx.org/examples/progress-bar/
|
|
app.Post("/browse/delete/model/:id", func(c *fiber.Ctx) error {
|
|
galleryID := strings.Clone(c.Params("id")) // note: strings.Clone is required for multiple requests!
|
|
log.Debug().Msgf("UI job submitted to delete : %+v\n", galleryID)
|
|
var galleryName = galleryID
|
|
if strings.Contains(galleryID, "@") {
|
|
// if the galleryID contains a @ it means that it's a model from a gallery
|
|
// but we want to delete it from the local models which does not need
|
|
// a repository ID
|
|
galleryName = strings.Split(galleryID, "@")[1]
|
|
}
|
|
|
|
id, err := uuid.NewUUID()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
uid := id.String()
|
|
|
|
// Track the deletion job by galleryID and galleryName
|
|
// The GalleryID contains information about the repository,
|
|
// while the GalleryName is ONLY the name of the model
|
|
opcache.Set(galleryName, uid)
|
|
opcache.Set(galleryID, uid)
|
|
|
|
op := services.GalleryOp[gallery.GalleryModel]{
|
|
ID: uid,
|
|
Delete: true,
|
|
GalleryElementName: galleryName,
|
|
Galleries: appConfig.Galleries,
|
|
}
|
|
go func() {
|
|
galleryService.ModelGalleryChannel <- op
|
|
cl.RemoveBackendConfig(galleryName)
|
|
}()
|
|
|
|
return c.SendString(elements.StartModelProgressBar(uid, "0", "Deletion"))
|
|
})
|
|
|
|
// Display the job current progress status
|
|
// If the job is done, we trigger the /browse/job/:uid route
|
|
// https://htmx.org/examples/progress-bar/
|
|
app.Get("/browse/job/progress/:uid", func(c *fiber.Ctx) error {
|
|
jobUID := strings.Clone(c.Params("uid")) // note: strings.Clone is required for multiple requests!
|
|
|
|
status := galleryService.GetStatus(jobUID)
|
|
if status == nil {
|
|
//fmt.Errorf("could not find any status for ID")
|
|
return c.SendString(elements.ProgressBar("0"))
|
|
}
|
|
|
|
if status.Progress == 100 && status.Processed && status.Message == "completed" {
|
|
c.Set("HX-Trigger", "done") // this triggers /browse/job/:uid (which is when the job is done)
|
|
return c.SendString(elements.ProgressBar("100"))
|
|
}
|
|
if status.Error != nil {
|
|
// TODO: instead of deleting the job, we should keep it in the cache and make it dismissable by the user
|
|
opcache.DeleteUUID(jobUID)
|
|
return c.SendString(elements.ErrorProgress(status.Error.Error(), status.GalleryElementName))
|
|
}
|
|
|
|
return c.SendString(elements.ProgressBar(fmt.Sprint(status.Progress)))
|
|
})
|
|
|
|
// this route is hit when the job is done, and we display the
|
|
// final state (for now just displays "Installation completed")
|
|
app.Get("/browse/job/:uid", func(c *fiber.Ctx) error {
|
|
jobUID := strings.Clone(c.Params("uid")) // note: strings.Clone is required for multiple requests!
|
|
|
|
status := galleryService.GetStatus(jobUID)
|
|
|
|
galleryID := status.GalleryElementName
|
|
opcache.DeleteUUID(jobUID)
|
|
if galleryID == "" {
|
|
log.Debug().Msgf("no processing model found for job : %+v\n", jobUID)
|
|
}
|
|
|
|
log.Debug().Msgf("JOB finished : %+v\n", status)
|
|
showDelete := true
|
|
displayText := "Installation completed"
|
|
if status.Deletion {
|
|
showDelete = false
|
|
displayText = "Deletion completed"
|
|
}
|
|
|
|
return c.SendString(elements.DoneModelProgress(galleryID, displayText, showDelete))
|
|
})
|
|
}
|