| | package routes |
| |
|
| | import ( |
| | "context" |
| | "fmt" |
| | "math" |
| | "net/http" |
| | "net/url" |
| | "sort" |
| | "strconv" |
| | "strings" |
| |
|
| | "github.com/google/uuid" |
| | "github.com/labstack/echo/v4" |
| | "github.com/mudler/LocalAI/core/application" |
| | "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/middleware" |
| | "github.com/mudler/LocalAI/core/p2p" |
| | "github.com/mudler/LocalAI/core/services" |
| | "github.com/mudler/LocalAI/pkg/model" |
| | "github.com/mudler/LocalAI/pkg/xsysinfo" |
| | "github.com/mudler/xlog" |
| | ) |
| |
|
| | const ( |
| | nameSortFieldName = "name" |
| | repositorySortFieldName = "repository" |
| | licenseSortFieldName = "license" |
| | statusSortFieldName = "status" |
| | ascSortOrder = "asc" |
| | ) |
| |
|
| | |
| | func RegisterUIAPIRoutes(app *echo.Echo, cl *config.ModelConfigLoader, ml *model.ModelLoader, appConfig *config.ApplicationConfig, galleryService *services.GalleryService, opcache *services.OpCache, applicationInstance *application.Application) { |
| |
|
| | |
| | app.GET("/api/operations", func(c echo.Context) error { |
| | processingData, taskTypes := opcache.GetStatus() |
| |
|
| | operations := []map[string]interface{}{} |
| | for galleryID, jobID := range processingData { |
| | taskType := "installation" |
| | if tt, ok := taskTypes[galleryID]; ok { |
| | taskType = tt |
| | } |
| |
|
| | status := galleryService.GetStatus(jobID) |
| | progress := 0 |
| | isDeletion := false |
| | isQueued := false |
| | isCancelled := false |
| | isCancellable := false |
| | message := "" |
| |
|
| | if status != nil { |
| | |
| | if status.Processed && !status.Cancelled { |
| | continue |
| | } |
| | |
| | if status.Processed && status.Cancelled { |
| | continue |
| | } |
| |
|
| | progress = int(status.Progress) |
| | isDeletion = status.Deletion |
| | isCancelled = status.Cancelled |
| | isCancellable = status.Cancellable |
| | message = status.Message |
| | if isDeletion { |
| | taskType = "deletion" |
| | } |
| | if isCancelled { |
| | taskType = "cancelled" |
| | } |
| | } else { |
| | |
| | isQueued = true |
| | isCancellable = true |
| | message = "Operation queued" |
| | } |
| |
|
| | |
| | |
| | isBackend := opcache.IsBackendOp(galleryID) |
| | |
| | if !isBackend { |
| | backends, _ := gallery.AvailableBackends(appConfig.BackendGalleries, appConfig.SystemState) |
| | for _, b := range backends { |
| | backendID := fmt.Sprintf("%s@%s", b.Gallery.Name, b.Name) |
| | if backendID == galleryID || b.Name == galleryID { |
| | isBackend = true |
| | break |
| | } |
| | } |
| | } |
| |
|
| | |
| | displayName := galleryID |
| | if strings.Contains(galleryID, "@") { |
| | parts := strings.Split(galleryID, "@") |
| | if len(parts) > 1 { |
| | displayName = parts[1] |
| | } |
| | } |
| |
|
| | operations = append(operations, map[string]interface{}{ |
| | "id": galleryID, |
| | "name": displayName, |
| | "fullName": galleryID, |
| | "jobID": jobID, |
| | "progress": progress, |
| | "taskType": taskType, |
| | "isDeletion": isDeletion, |
| | "isBackend": isBackend, |
| | "isQueued": isQueued, |
| | "isCancelled": isCancelled, |
| | "cancellable": isCancellable, |
| | "message": message, |
| | }) |
| | } |
| |
|
| | |
| | sort.Slice(operations, func(i, j int) bool { |
| | progressI := operations[i]["progress"].(int) |
| | progressJ := operations[j]["progress"].(int) |
| |
|
| | |
| | if progressI != progressJ { |
| | return progressI < progressJ |
| | } |
| |
|
| | |
| | return operations[i]["id"].(string) < operations[j]["id"].(string) |
| | }) |
| |
|
| | return c.JSON(200, map[string]interface{}{ |
| | "operations": operations, |
| | }) |
| | }) |
| |
|
| | |
| | app.POST("/api/operations/:jobID/cancel", func(c echo.Context) error { |
| | jobID := c.Param("jobID") |
| | xlog.Debug("API request to cancel operation", "jobID", jobID) |
| |
|
| | err := galleryService.CancelOperation(jobID) |
| | if err != nil { |
| | xlog.Error("Failed to cancel operation", "error", err, "jobID", jobID) |
| | return c.JSON(http.StatusBadRequest, map[string]interface{}{ |
| | "error": err.Error(), |
| | }) |
| | } |
| |
|
| | |
| | opcache.DeleteUUID(jobID) |
| |
|
| | return c.JSON(200, map[string]interface{}{ |
| | "success": true, |
| | "message": "Operation cancelled", |
| | }) |
| | }) |
| |
|
| | |
| | app.GET("/api/models", func(c echo.Context) error { |
| | term := c.QueryParam("term") |
| | page := c.QueryParam("page") |
| | if page == "" { |
| | page = "1" |
| | } |
| | items := c.QueryParam("items") |
| | if items == "" { |
| | items = "21" |
| | } |
| |
|
| | models, err := gallery.AvailableGalleryModels(appConfig.Galleries, appConfig.SystemState) |
| | if err != nil { |
| | xlog.Error("could not list models from galleries", "error", err) |
| | return c.JSON(http.StatusInternalServerError, map[string]interface{}{ |
| | "error": err.Error(), |
| | }) |
| | } |
| |
|
| | |
| | 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) |
| | } |
| |
|
| | |
| | processingModelsData, taskTypes := opcache.GetStatus() |
| |
|
| | |
| | sortBy := c.QueryParam("sort") |
| | sortOrder := c.QueryParam("order") |
| | if sortOrder == "" { |
| | sortOrder = ascSortOrder |
| | } |
| |
|
| | switch sortBy { |
| | case nameSortFieldName: |
| | models = gallery.GalleryElements[*gallery.GalleryModel](models).SortByName(sortOrder) |
| | case repositorySortFieldName: |
| | models = gallery.GalleryElements[*gallery.GalleryModel](models).SortByRepository(sortOrder) |
| | case licenseSortFieldName: |
| | models = gallery.GalleryElements[*gallery.GalleryModel](models).SortByLicense(sortOrder) |
| | case statusSortFieldName: |
| | models = gallery.GalleryElements[*gallery.GalleryModel](models).SortByInstalled(sortOrder) |
| | } |
| |
|
| | pageNum, err := strconv.Atoi(page) |
| | if err != nil || pageNum < 1 { |
| | pageNum = 1 |
| | } |
| |
|
| | itemsNum, err := strconv.Atoi(items) |
| | if err != nil || itemsNum < 1 { |
| | itemsNum = 21 |
| | } |
| |
|
| | totalPages := int(math.Ceil(float64(len(models)) / float64(itemsNum))) |
| | totalModels := len(models) |
| |
|
| | if pageNum > 0 { |
| | models = models.Paginate(pageNum, itemsNum) |
| | } |
| |
|
| | |
| | modelsJSON := make([]map[string]interface{}, 0, len(models)) |
| | seenIDs := make(map[string]bool) |
| |
|
| | for _, m := range models { |
| | modelID := m.ID() |
| |
|
| | |
| | if seenIDs[modelID] { |
| | xlog.Debug("Skipping duplicate model ID", "modelID", modelID) |
| | continue |
| | } |
| | seenIDs[modelID] = true |
| |
|
| | currentlyProcessing := opcache.Exists(modelID) |
| | jobID := "" |
| | isDeletionOp := false |
| | if currentlyProcessing { |
| | jobID = opcache.Get(modelID) |
| | status := galleryService.GetStatus(jobID) |
| | if status != nil && status.Deletion { |
| | isDeletionOp = true |
| | } |
| | } |
| |
|
| | _, trustRemoteCodeExists := m.Overrides["trust_remote_code"] |
| |
|
| | modelsJSON = append(modelsJSON, map[string]interface{}{ |
| | "id": modelID, |
| | "name": m.Name, |
| | "description": m.Description, |
| | "icon": m.Icon, |
| | "license": m.License, |
| | "urls": m.URLs, |
| | "tags": m.Tags, |
| | "gallery": m.Gallery.Name, |
| | "installed": m.Installed, |
| | "processing": currentlyProcessing, |
| | "jobID": jobID, |
| | "isDeletion": isDeletionOp, |
| | "trustRemoteCode": trustRemoteCodeExists, |
| | "additionalFiles": m.AdditionalFiles, |
| | }) |
| | } |
| |
|
| | prevPage := pageNum - 1 |
| | nextPage := pageNum + 1 |
| | if prevPage < 1 { |
| | prevPage = 1 |
| | } |
| | if nextPage > totalPages { |
| | nextPage = totalPages |
| | } |
| |
|
| | |
| | modelConfigs := cl.GetAllModelsConfigs() |
| | modelsWithoutConfig, _ := services.ListModels(cl, ml, config.NoFilterFn, services.LOOSE_ONLY) |
| | installedModelsCount := len(modelConfigs) + len(modelsWithoutConfig) |
| |
|
| | return c.JSON(200, map[string]interface{}{ |
| | "models": modelsJSON, |
| | "repositories": appConfig.Galleries, |
| | "allTags": tags, |
| | "processingModels": processingModelsData, |
| | "taskTypes": taskTypes, |
| | "availableModels": totalModels, |
| | "installedModels": installedModelsCount, |
| | "currentPage": pageNum, |
| | "totalPages": totalPages, |
| | "prevPage": prevPage, |
| | "nextPage": nextPage, |
| | }) |
| | }) |
| |
|
| | app.POST("/api/models/install/:id", func(c echo.Context) error { |
| | galleryID := c.Param("id") |
| | |
| | galleryID, err := url.QueryUnescape(galleryID) |
| | if err != nil { |
| | return c.JSON(http.StatusBadRequest, map[string]interface{}{ |
| | "error": "invalid model ID", |
| | }) |
| | } |
| | xlog.Debug("API job submitted to install", "galleryID", galleryID) |
| |
|
| | id, err := uuid.NewUUID() |
| | if err != nil { |
| | return c.JSON(http.StatusInternalServerError, map[string]interface{}{ |
| | "error": err.Error(), |
| | }) |
| | } |
| |
|
| | uid := id.String() |
| | opcache.Set(galleryID, uid) |
| |
|
| | ctx, cancelFunc := context.WithCancel(context.Background()) |
| | op := services.GalleryOp[gallery.GalleryModel, gallery.ModelConfig]{ |
| | ID: uid, |
| | GalleryElementName: galleryID, |
| | Galleries: appConfig.Galleries, |
| | BackendGalleries: appConfig.BackendGalleries, |
| | Context: ctx, |
| | CancelFunc: cancelFunc, |
| | } |
| | |
| | galleryService.StoreCancellation(uid, cancelFunc) |
| | go func() { |
| | galleryService.ModelGalleryChannel <- op |
| | }() |
| |
|
| | return c.JSON(200, map[string]interface{}{ |
| | "jobID": uid, |
| | "message": "Installation started", |
| | }) |
| | }) |
| |
|
| | app.POST("/api/models/delete/:id", func(c echo.Context) error { |
| | galleryID := c.Param("id") |
| | |
| | galleryID, err := url.QueryUnescape(galleryID) |
| | if err != nil { |
| | return c.JSON(http.StatusBadRequest, map[string]interface{}{ |
| | "error": "invalid model ID", |
| | }) |
| | } |
| | xlog.Debug("API job submitted to delete", "galleryID", galleryID) |
| |
|
| | var galleryName = galleryID |
| | if strings.Contains(galleryID, "@") { |
| | galleryName = strings.Split(galleryID, "@")[1] |
| | } |
| |
|
| | id, err := uuid.NewUUID() |
| | if err != nil { |
| | return c.JSON(http.StatusInternalServerError, map[string]interface{}{ |
| | "error": err.Error(), |
| | }) |
| | } |
| |
|
| | uid := id.String() |
| |
|
| | opcache.Set(galleryID, uid) |
| |
|
| | ctx, cancelFunc := context.WithCancel(context.Background()) |
| | op := services.GalleryOp[gallery.GalleryModel, gallery.ModelConfig]{ |
| | ID: uid, |
| | Delete: true, |
| | GalleryElementName: galleryName, |
| | Galleries: appConfig.Galleries, |
| | BackendGalleries: appConfig.BackendGalleries, |
| | Context: ctx, |
| | CancelFunc: cancelFunc, |
| | } |
| | |
| | galleryService.StoreCancellation(uid, cancelFunc) |
| | go func() { |
| | galleryService.ModelGalleryChannel <- op |
| | cl.RemoveModelConfig(galleryName) |
| | }() |
| |
|
| | return c.JSON(200, map[string]interface{}{ |
| | "jobID": uid, |
| | "message": "Deletion started", |
| | }) |
| | }) |
| |
|
| | app.POST("/api/models/config/:id", func(c echo.Context) error { |
| | galleryID := c.Param("id") |
| | |
| | galleryID, err := url.QueryUnescape(galleryID) |
| | if err != nil { |
| | return c.JSON(http.StatusBadRequest, map[string]interface{}{ |
| | "error": "invalid model ID", |
| | }) |
| | } |
| | xlog.Debug("API job submitted to get config", "galleryID", galleryID) |
| |
|
| | models, err := gallery.AvailableGalleryModels(appConfig.Galleries, appConfig.SystemState) |
| | if err != nil { |
| | return c.JSON(http.StatusInternalServerError, map[string]interface{}{ |
| | "error": err.Error(), |
| | }) |
| | } |
| |
|
| | model := gallery.FindGalleryElement(models, galleryID) |
| | if model == nil { |
| | return c.JSON(http.StatusNotFound, map[string]interface{}{ |
| | "error": "model not found", |
| | }) |
| | } |
| |
|
| | config, err := gallery.GetGalleryConfigFromURL[gallery.ModelConfig](model.URL, appConfig.SystemState.Model.ModelsPath) |
| | if err != nil { |
| | return c.JSON(http.StatusInternalServerError, map[string]interface{}{ |
| | "error": err.Error(), |
| | }) |
| | } |
| |
|
| | _, err = gallery.InstallModel(context.Background(), appConfig.SystemState, model.Name, &config, model.Overrides, nil, false) |
| | if err != nil { |
| | return c.JSON(http.StatusInternalServerError, map[string]interface{}{ |
| | "error": err.Error(), |
| | }) |
| | } |
| |
|
| | return c.JSON(200, map[string]interface{}{ |
| | "message": "Configuration file saved", |
| | }) |
| | }) |
| |
|
| | app.GET("/api/models/job/:uid", func(c echo.Context) error { |
| | jobUID := c.Param("uid") |
| |
|
| | status := galleryService.GetStatus(jobUID) |
| | if status == nil { |
| | |
| | return c.JSON(200, map[string]interface{}{ |
| | "progress": 0, |
| | "message": "Operation queued", |
| | "galleryElementName": "", |
| | "processed": false, |
| | "deletion": false, |
| | "queued": true, |
| | }) |
| | } |
| |
|
| | response := map[string]interface{}{ |
| | "progress": status.Progress, |
| | "message": status.Message, |
| | "galleryElementName": status.GalleryElementName, |
| | "processed": status.Processed, |
| | "deletion": status.Deletion, |
| | "queued": false, |
| | } |
| |
|
| | if status.Error != nil { |
| | response["error"] = status.Error.Error() |
| | } |
| |
|
| | if status.Progress == 100 && status.Processed && status.Message == "completed" { |
| | opcache.DeleteUUID(jobUID) |
| | response["completed"] = true |
| | } |
| |
|
| | return c.JSON(200, response) |
| | }) |
| |
|
| | |
| | app.GET("/api/backends", func(c echo.Context) error { |
| | term := c.QueryParam("term") |
| | page := c.QueryParam("page") |
| | if page == "" { |
| | page = "1" |
| | } |
| | items := c.QueryParam("items") |
| | if items == "" { |
| | items = "21" |
| | } |
| |
|
| | backends, err := gallery.AvailableBackends(appConfig.BackendGalleries, appConfig.SystemState) |
| | if err != nil { |
| | xlog.Error("could not list backends from galleries", "error", err) |
| | return c.JSON(http.StatusInternalServerError, map[string]interface{}{ |
| | "error": err.Error(), |
| | }) |
| | } |
| |
|
| | |
| | allTags := map[string]struct{}{} |
| | tags := []string{} |
| | for _, b := range backends { |
| | for _, t := range b.Tags { |
| | allTags[t] = struct{}{} |
| | } |
| | } |
| | for t := range allTags { |
| | tags = append(tags, t) |
| | } |
| | sort.Strings(tags) |
| |
|
| | if term != "" { |
| | backends = gallery.GalleryElements[*gallery.GalleryBackend](backends).Search(term) |
| | } |
| |
|
| | |
| | processingBackendsData, taskTypes := opcache.GetStatus() |
| |
|
| | |
| | sortBy := c.QueryParam("sort") |
| | sortOrder := c.QueryParam("order") |
| | if sortOrder == "" { |
| | sortOrder = ascSortOrder |
| | } |
| |
|
| | switch sortBy { |
| | case nameSortFieldName: |
| | backends = gallery.GalleryElements[*gallery.GalleryBackend](backends).SortByName(sortOrder) |
| | case repositorySortFieldName: |
| | backends = gallery.GalleryElements[*gallery.GalleryBackend](backends).SortByRepository(sortOrder) |
| | case licenseSortFieldName: |
| | backends = gallery.GalleryElements[*gallery.GalleryBackend](backends).SortByLicense(sortOrder) |
| | case statusSortFieldName: |
| | backends = gallery.GalleryElements[*gallery.GalleryBackend](backends).SortByInstalled(sortOrder) |
| | } |
| |
|
| | pageNum, err := strconv.Atoi(page) |
| | if err != nil || pageNum < 1 { |
| | pageNum = 1 |
| | } |
| |
|
| | itemsNum, err := strconv.Atoi(items) |
| | if err != nil || itemsNum < 1 { |
| | itemsNum = 21 |
| | } |
| |
|
| | totalPages := int(math.Ceil(float64(len(backends)) / float64(itemsNum))) |
| | totalBackends := len(backends) |
| |
|
| | if pageNum > 0 { |
| | backends = backends.Paginate(pageNum, itemsNum) |
| | } |
| |
|
| | |
| | backendsJSON := make([]map[string]interface{}, 0, len(backends)) |
| | seenBackendIDs := make(map[string]bool) |
| |
|
| | for _, b := range backends { |
| | backendID := b.ID() |
| |
|
| | |
| | if seenBackendIDs[backendID] { |
| | xlog.Debug("Skipping duplicate backend ID", "backendID", backendID) |
| | continue |
| | } |
| | seenBackendIDs[backendID] = true |
| |
|
| | currentlyProcessing := opcache.Exists(backendID) |
| | jobID := "" |
| | isDeletionOp := false |
| | if currentlyProcessing { |
| | jobID = opcache.Get(backendID) |
| | status := galleryService.GetStatus(jobID) |
| | if status != nil && status.Deletion { |
| | isDeletionOp = true |
| | } |
| | } |
| |
|
| | backendsJSON = append(backendsJSON, map[string]interface{}{ |
| | "id": backendID, |
| | "name": b.Name, |
| | "description": b.Description, |
| | "icon": b.Icon, |
| | "license": b.License, |
| | "urls": b.URLs, |
| | "tags": b.Tags, |
| | "gallery": b.Gallery.Name, |
| | "installed": b.Installed, |
| | "processing": currentlyProcessing, |
| | "jobID": jobID, |
| | "isDeletion": isDeletionOp, |
| | }) |
| | } |
| |
|
| | prevPage := pageNum - 1 |
| | nextPage := pageNum + 1 |
| | if prevPage < 1 { |
| | prevPage = 1 |
| | } |
| | if nextPage > totalPages { |
| | nextPage = totalPages |
| | } |
| |
|
| | |
| | installedBackends, err := gallery.ListSystemBackends(appConfig.SystemState) |
| | installedBackendsCount := 0 |
| | if err == nil { |
| | installedBackendsCount = len(installedBackends) |
| | } |
| |
|
| | |
| | detectedCapability := "" |
| | if appConfig.SystemState != nil { |
| | detectedCapability = appConfig.SystemState.DetectedCapability() |
| | } |
| |
|
| | return c.JSON(200, map[string]interface{}{ |
| | "backends": backendsJSON, |
| | "repositories": appConfig.BackendGalleries, |
| | "allTags": tags, |
| | "processingBackends": processingBackendsData, |
| | "taskTypes": taskTypes, |
| | "availableBackends": totalBackends, |
| | "installedBackends": installedBackendsCount, |
| | "currentPage": pageNum, |
| | "totalPages": totalPages, |
| | "prevPage": prevPage, |
| | "nextPage": nextPage, |
| | "systemCapability": detectedCapability, |
| | }) |
| | }) |
| |
|
| | app.POST("/api/backends/install/:id", func(c echo.Context) error { |
| | backendID := c.Param("id") |
| | |
| | backendID, err := url.QueryUnescape(backendID) |
| | if err != nil { |
| | return c.JSON(http.StatusBadRequest, map[string]interface{}{ |
| | "error": "invalid backend ID", |
| | }) |
| | } |
| | xlog.Debug("API job submitted to install backend", "backendID", backendID) |
| |
|
| | id, err := uuid.NewUUID() |
| | if err != nil { |
| | return c.JSON(http.StatusInternalServerError, map[string]interface{}{ |
| | "error": err.Error(), |
| | }) |
| | } |
| |
|
| | uid := id.String() |
| | opcache.SetBackend(backendID, uid) |
| |
|
| | ctx, cancelFunc := context.WithCancel(context.Background()) |
| | op := services.GalleryOp[gallery.GalleryBackend, any]{ |
| | ID: uid, |
| | GalleryElementName: backendID, |
| | Galleries: appConfig.BackendGalleries, |
| | Context: ctx, |
| | CancelFunc: cancelFunc, |
| | } |
| | |
| | galleryService.StoreCancellation(uid, cancelFunc) |
| | go func() { |
| | galleryService.BackendGalleryChannel <- op |
| | }() |
| |
|
| | return c.JSON(200, map[string]interface{}{ |
| | "jobID": uid, |
| | "message": "Backend installation started", |
| | }) |
| | }) |
| |
|
| | |
| | app.POST("/api/backends/install-external", func(c echo.Context) error { |
| | |
| | type ExternalBackendRequest struct { |
| | URI string `json:"uri"` |
| | Name string `json:"name"` |
| | Alias string `json:"alias"` |
| | } |
| |
|
| | var req ExternalBackendRequest |
| | if err := c.Bind(&req); err != nil { |
| | return c.JSON(http.StatusBadRequest, map[string]interface{}{ |
| | "error": "invalid request body", |
| | }) |
| | } |
| |
|
| | |
| | if req.URI == "" { |
| | return c.JSON(http.StatusBadRequest, map[string]interface{}{ |
| | "error": "uri is required", |
| | }) |
| | } |
| |
|
| | xlog.Debug("API job submitted to install external backend", "uri", req.URI, "name", req.Name, "alias", req.Alias) |
| |
|
| | id, err := uuid.NewUUID() |
| | if err != nil { |
| | return c.JSON(http.StatusInternalServerError, map[string]interface{}{ |
| | "error": err.Error(), |
| | }) |
| | } |
| |
|
| | uid := id.String() |
| |
|
| | |
| | cacheKey := req.URI |
| | if req.Name != "" { |
| | cacheKey = req.Name |
| | } |
| | opcache.SetBackend(cacheKey, uid) |
| |
|
| | ctx, cancelFunc := context.WithCancel(context.Background()) |
| | op := services.GalleryOp[gallery.GalleryBackend, any]{ |
| | ID: uid, |
| | GalleryElementName: req.Name, |
| | Galleries: appConfig.BackendGalleries, |
| | Context: ctx, |
| | CancelFunc: cancelFunc, |
| | ExternalURI: req.URI, |
| | ExternalName: req.Name, |
| | ExternalAlias: req.Alias, |
| | } |
| | |
| | galleryService.StoreCancellation(uid, cancelFunc) |
| | go func() { |
| | galleryService.BackendGalleryChannel <- op |
| | }() |
| |
|
| | return c.JSON(200, map[string]interface{}{ |
| | "jobID": uid, |
| | "message": "External backend installation started", |
| | }) |
| | }) |
| |
|
| | app.POST("/api/backends/delete/:id", func(c echo.Context) error { |
| | backendID := c.Param("id") |
| | |
| | backendID, err := url.QueryUnescape(backendID) |
| | if err != nil { |
| | return c.JSON(http.StatusBadRequest, map[string]interface{}{ |
| | "error": "invalid backend ID", |
| | }) |
| | } |
| | xlog.Debug("API job submitted to delete backend", "backendID", backendID) |
| |
|
| | var backendName = backendID |
| | if strings.Contains(backendID, "@") { |
| | backendName = strings.Split(backendID, "@")[1] |
| | } |
| |
|
| | id, err := uuid.NewUUID() |
| | if err != nil { |
| | return c.JSON(http.StatusInternalServerError, map[string]interface{}{ |
| | "error": err.Error(), |
| | }) |
| | } |
| |
|
| | uid := id.String() |
| |
|
| | opcache.SetBackend(backendID, uid) |
| |
|
| | ctx, cancelFunc := context.WithCancel(context.Background()) |
| | op := services.GalleryOp[gallery.GalleryBackend, any]{ |
| | ID: uid, |
| | Delete: true, |
| | GalleryElementName: backendName, |
| | Galleries: appConfig.BackendGalleries, |
| | Context: ctx, |
| | CancelFunc: cancelFunc, |
| | } |
| | |
| | galleryService.StoreCancellation(uid, cancelFunc) |
| | go func() { |
| | galleryService.BackendGalleryChannel <- op |
| | }() |
| |
|
| | return c.JSON(200, map[string]interface{}{ |
| | "jobID": uid, |
| | "message": "Backend deletion started", |
| | }) |
| | }) |
| |
|
| | app.GET("/api/backends/job/:uid", func(c echo.Context) error { |
| | jobUID := c.Param("uid") |
| |
|
| | status := galleryService.GetStatus(jobUID) |
| | if status == nil { |
| | |
| | return c.JSON(200, map[string]interface{}{ |
| | "progress": 0, |
| | "message": "Operation queued", |
| | "galleryElementName": "", |
| | "processed": false, |
| | "deletion": false, |
| | "queued": true, |
| | }) |
| | } |
| |
|
| | response := map[string]interface{}{ |
| | "progress": status.Progress, |
| | "message": status.Message, |
| | "galleryElementName": status.GalleryElementName, |
| | "processed": status.Processed, |
| | "deletion": status.Deletion, |
| | "queued": false, |
| | } |
| |
|
| | if status.Error != nil { |
| | response["error"] = status.Error.Error() |
| | } |
| |
|
| | if status.Progress == 100 && status.Processed && status.Message == "completed" { |
| | opcache.DeleteUUID(jobUID) |
| | response["completed"] = true |
| | } |
| |
|
| | return c.JSON(200, response) |
| | }) |
| |
|
| | |
| | app.POST("/api/backends/system/delete/:name", func(c echo.Context) error { |
| | backendName := c.Param("name") |
| | |
| | backendName, err := url.QueryUnescape(backendName) |
| | if err != nil { |
| | return c.JSON(http.StatusBadRequest, map[string]interface{}{ |
| | "error": "invalid backend name", |
| | }) |
| | } |
| | xlog.Debug("API request to delete system backend", "backendName", backendName) |
| |
|
| | |
| | if err := gallery.DeleteBackendFromSystem(appConfig.SystemState, backendName); err != nil { |
| | xlog.Error("Failed to delete backend", "error", err, "backendName", backendName) |
| | return c.JSON(http.StatusInternalServerError, map[string]interface{}{ |
| | "error": err.Error(), |
| | }) |
| | } |
| |
|
| | return c.JSON(200, map[string]interface{}{ |
| | "success": true, |
| | "message": "Backend deleted successfully", |
| | }) |
| | }) |
| |
|
| | |
| | app.GET("/api/p2p/workers", func(c echo.Context) error { |
| | nodes := p2p.GetAvailableNodes(p2p.NetworkID(appConfig.P2PNetworkID, p2p.WorkerID)) |
| |
|
| | nodesJSON := make([]map[string]interface{}, 0, len(nodes)) |
| | for _, n := range nodes { |
| | nodesJSON = append(nodesJSON, map[string]interface{}{ |
| | "name": n.Name, |
| | "id": n.ID, |
| | "tunnelAddress": n.TunnelAddress, |
| | "serviceID": n.ServiceID, |
| | "lastSeen": n.LastSeen, |
| | "isOnline": n.IsOnline(), |
| | }) |
| | } |
| |
|
| | return c.JSON(200, map[string]interface{}{ |
| | "nodes": nodesJSON, |
| | }) |
| | }) |
| |
|
| | app.GET("/api/p2p/federation", func(c echo.Context) error { |
| | nodes := p2p.GetAvailableNodes(p2p.NetworkID(appConfig.P2PNetworkID, p2p.FederatedID)) |
| |
|
| | nodesJSON := make([]map[string]interface{}, 0, len(nodes)) |
| | for _, n := range nodes { |
| | nodesJSON = append(nodesJSON, map[string]interface{}{ |
| | "name": n.Name, |
| | "id": n.ID, |
| | "tunnelAddress": n.TunnelAddress, |
| | "serviceID": n.ServiceID, |
| | "lastSeen": n.LastSeen, |
| | "isOnline": n.IsOnline(), |
| | }) |
| | } |
| |
|
| | return c.JSON(200, map[string]interface{}{ |
| | "nodes": nodesJSON, |
| | }) |
| | }) |
| |
|
| | app.GET("/api/p2p/stats", func(c echo.Context) error { |
| | workerNodes := p2p.GetAvailableNodes(p2p.NetworkID(appConfig.P2PNetworkID, p2p.WorkerID)) |
| | federatedNodes := p2p.GetAvailableNodes(p2p.NetworkID(appConfig.P2PNetworkID, p2p.FederatedID)) |
| |
|
| | workersOnline := 0 |
| | for _, n := range workerNodes { |
| | if n.IsOnline() { |
| | workersOnline++ |
| | } |
| | } |
| |
|
| | federatedOnline := 0 |
| | for _, n := range federatedNodes { |
| | if n.IsOnline() { |
| | federatedOnline++ |
| | } |
| | } |
| |
|
| | return c.JSON(200, map[string]interface{}{ |
| | "workers": map[string]interface{}{ |
| | "online": workersOnline, |
| | "total": len(workerNodes), |
| | }, |
| | "federated": map[string]interface{}{ |
| | "online": federatedOnline, |
| | "total": len(federatedNodes), |
| | }, |
| | }) |
| | }) |
| |
|
| | |
| | app.GET("/api/resources", func(c echo.Context) error { |
| | resourceInfo := xsysinfo.GetResourceInfo() |
| |
|
| | |
| | watchdogInterval := "2s" |
| | if appConfig.WatchDogInterval > 0 { |
| | watchdogInterval = appConfig.WatchDogInterval.String() |
| | } |
| |
|
| | response := map[string]interface{}{ |
| | "type": resourceInfo.Type, |
| | "available": resourceInfo.Available, |
| | "gpus": resourceInfo.GPUs, |
| | "ram": resourceInfo.RAM, |
| | "aggregate": resourceInfo.Aggregate, |
| | "reclaimer_enabled": appConfig.MemoryReclaimerEnabled, |
| | "reclaimer_threshold": appConfig.MemoryReclaimerThreshold, |
| | "watchdog_interval": watchdogInterval, |
| | } |
| |
|
| | return c.JSON(200, response) |
| | }) |
| |
|
| | if !appConfig.DisableRuntimeSettings { |
| | |
| | app.GET("/api/settings", localai.GetSettingsEndpoint(applicationInstance)) |
| | app.POST("/api/settings", localai.UpdateSettingsEndpoint(applicationInstance)) |
| | } |
| |
|
| | |
| | app.GET("/api/traces", func(c echo.Context) error { |
| | if !appConfig.EnableTracing { |
| | return c.JSON(503, map[string]any{ |
| | "error": "Tracing disabled", |
| | }) |
| | } |
| | traces := middleware.GetTraces() |
| | return c.JSON(200, map[string]interface{}{ |
| | "traces": traces, |
| | }) |
| | }) |
| |
|
| | app.POST("/api/traces/clear", func(c echo.Context) error { |
| | middleware.ClearTraces() |
| | return c.JSON(200, map[string]interface{}{ |
| | "message": "Traces cleared", |
| | }) |
| | }) |
| | } |
| |
|