Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
255 changes: 129 additions & 126 deletions core/config/backend_config.go

Large diffs are not rendered by default.

18 changes: 7 additions & 11 deletions core/gallery/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -316,24 +316,21 @@ func DeleteModelFromSystem(systemState *system.SystemState, name string) error {
return fmt.Errorf("failed to verify path %s: %w", galleryFile, err)
}

var filesToRemove []string

// Delete all the files associated to the model
// read the model config
galleryconfig, err := ReadConfigFile[ModelConfig](galleryFile)
if err != nil {
log.Error().Err(err).Msgf("failed to read gallery file %s", configFile)
}

var filesToRemove []string

// Remove additional files
if galleryconfig != nil {
if err == nil && galleryconfig != nil {
for _, f := range galleryconfig.Files {
fullPath := filepath.Join(systemState.Model.ModelsPath, f.Filename)
if err := utils.VerifyPath(fullPath, systemState.Model.ModelsPath); err != nil {
return fmt.Errorf("failed to verify path %s: %w", fullPath, err)
}
filesToRemove = append(filesToRemove, fullPath)
}
} else {
log.Error().Err(err).Msgf("failed to read gallery file %s", configFile)
}

for _, f := range additionalFiles {
Expand All @@ -344,7 +341,6 @@ func DeleteModelFromSystem(systemState *system.SystemState, name string) error {
filesToRemove = append(filesToRemove, fullPath)
}

filesToRemove = append(filesToRemove, configFile)
filesToRemove = append(filesToRemove, galleryFile)

// skip duplicates
Expand All @@ -353,11 +349,11 @@ func DeleteModelFromSystem(systemState *system.SystemState, name string) error {
// Removing files
for _, f := range filesToRemove {
if e := os.Remove(f); e != nil {
err = errors.Join(err, fmt.Errorf("failed to remove file %s: %w", f, e))
log.Error().Err(e).Msgf("failed to remove file %s", f)
}
}

return err
return os.Remove(configFile)
}

// This is ***NEVER*** going to be perfect or finished.
Expand Down
228 changes: 228 additions & 0 deletions core/http/endpoints/localai/edit_model.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
package localai

import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"

"github.com/gofiber/fiber/v2"
"github.com/mudler/LocalAI/core/config"
httpUtils "github.com/mudler/LocalAI/core/http/utils"
"github.com/mudler/LocalAI/internal"
"github.com/mudler/LocalAI/pkg/utils"

"gopkg.in/yaml.v3"
)

// GetEditModelPage renders the edit model page with current configuration
func GetEditModelPage(cl *config.ModelConfigLoader, appConfig *config.ApplicationConfig) fiber.Handler {
return func(c *fiber.Ctx) error {
modelName := c.Params("name")
if modelName == "" {
response := ModelResponse{
Success: false,
Error: "Model name is required",
}
return c.Status(400).JSON(response)
}

modelConfig, exists := cl.GetModelConfig(modelName)
if !exists {
response := ModelResponse{
Success: false,
Error: "Model configuration not found",
}
return c.Status(404).JSON(response)
}

configData, err := yaml.Marshal(modelConfig)
if err != nil {
response := ModelResponse{
Success: false,
Error: "Failed to marshal configuration: " + err.Error(),
}
return c.Status(500).JSON(response)
}

// Marshal the config to JSON for the template
configJSON, err := json.Marshal(modelConfig)
if err != nil {
response := ModelResponse{
Success: false,
Error: "Failed to marshal configuration: " + err.Error(),
}
return c.Status(500).JSON(response)
}

// Render the edit page with the current configuration
templateData := struct {
Title string
ModelName string
Config *config.ModelConfig
ConfigJSON string
ConfigYAML string
BaseURL string
Version string
}{
Title: "LocalAI - Edit Model " + modelName,
ModelName: modelName,
Config: &modelConfig,
ConfigJSON: string(configJSON),
ConfigYAML: string(configData),
BaseURL: httpUtils.BaseURL(c),
Version: internal.PrintableVersion(),
}

return c.Render("views/model-editor", templateData)
}
}

// EditModelEndpoint handles updating existing model configurations
func EditModelEndpoint(cl *config.ModelConfigLoader, appConfig *config.ApplicationConfig) fiber.Handler {
return func(c *fiber.Ctx) error {
modelName := c.Params("name")
if modelName == "" {
response := ModelResponse{
Success: false,
Error: "Model name is required",
}
return c.Status(400).JSON(response)
}

// Get the raw body
body := c.Body()
if len(body) == 0 {
response := ModelResponse{
Success: false,
Error: "Request body is empty",
}
return c.Status(400).JSON(response)
}

// Check content type to determine how to parse
contentType := string(c.Context().Request.Header.ContentType())
var req config.ModelConfig
var err error

if strings.Contains(contentType, "application/json") {
// Parse JSON
if err := json.Unmarshal(body, &req); err != nil {
response := ModelResponse{
Success: false,
Error: "Failed to parse JSON: " + err.Error(),
}
return c.Status(400).JSON(response)
}
} else if strings.Contains(contentType, "application/x-yaml") || strings.Contains(contentType, "text/yaml") {
// Parse YAML
if err := yaml.Unmarshal(body, &req); err != nil {
response := ModelResponse{
Success: false,
Error: "Failed to parse YAML: " + err.Error(),
}
return c.Status(400).JSON(response)
}
} else {
// Try to auto-detect format
if strings.TrimSpace(string(body))[0] == '{' {
// Looks like JSON
if err := json.Unmarshal(body, &req); err != nil {
response := ModelResponse{
Success: false,
Error: "Failed to parse JSON: " + err.Error(),
}
return c.Status(400).JSON(response)
}
} else {
// Assume YAML
if err := yaml.Unmarshal(body, &req); err != nil {
response := ModelResponse{
Success: false,
Error: "Failed to parse YAML: " + err.Error(),
}
return c.Status(400).JSON(response)
}
}
}

// Validate required fields
if req.Name == "" {
response := ModelResponse{
Success: false,
Error: "Name is required",
}
return c.Status(400).JSON(response)
}

// Load the existing configuration
configPath := filepath.Join(appConfig.SystemState.Model.ModelsPath, modelName+".yaml")
if err := utils.InTrustedRoot(configPath, appConfig.SystemState.Model.ModelsPath); err != nil {
response := ModelResponse{
Success: false,
Error: "Model configuration not trusted: " + err.Error(),
}
return c.Status(404).JSON(response)
}

// Set defaults
req.SetDefaults()

// Validate the configuration
if !req.Validate() {
response := ModelResponse{
Success: false,
Error: "Validation failed",
Details: []string{"Configuration validation failed. Please check your YAML syntax and required fields."},
}
return c.Status(400).JSON(response)
}

// Create the YAML file
yamlData, err := yaml.Marshal(req)
if err != nil {
response := ModelResponse{
Success: false,
Error: "Failed to marshal configuration: " + err.Error(),
}
return c.Status(500).JSON(response)
}

// Write to file
if err := os.WriteFile(configPath, yamlData, 0644); err != nil {
response := ModelResponse{
Success: false,
Error: "Failed to write configuration file: " + err.Error(),
}
return c.Status(500).JSON(response)
}

// Reload configurations
if err := cl.LoadModelConfigsFromPath(appConfig.SystemState.Model.ModelsPath); err != nil {
response := ModelResponse{
Success: false,
Error: "Failed to reload configurations: " + err.Error(),
}
return c.Status(500).JSON(response)
}

// Preload the model
if err := cl.Preload(appConfig.SystemState.Model.ModelsPath); err != nil {
response := ModelResponse{
Success: false,
Error: "Failed to preload model: " + err.Error(),
}
return c.Status(500).JSON(response)
}

// Return success response
response := ModelResponse{
Success: true,
Message: fmt.Sprintf("Model '%s' updated successfully", modelName),
Filename: configPath,
Config: req,
}
return c.JSON(response)
}
}
72 changes: 72 additions & 0 deletions core/http/endpoints/localai/edit_model_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package localai_test

import (
"bytes"
"io"
"net/http/httptest"
"os"
"path/filepath"

"github.com/gofiber/fiber/v2"
"github.com/mudler/LocalAI/core/config"
. "github.com/mudler/LocalAI/core/http/endpoints/localai"
"github.com/mudler/LocalAI/pkg/system"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

var _ = Describe("Edit Model test", func() {

var tempDir string
BeforeEach(func() {
var err error
tempDir, err = os.MkdirTemp("", "localai-test")
Expect(err).ToNot(HaveOccurred())
})
AfterEach(func() {
os.RemoveAll(tempDir)
})

Context("Edit Model endpoint", func() {
It("should edit a model", func() {
systemState, err := system.GetSystemState(
system.WithModelPath(filepath.Join(tempDir)),
)
Expect(err).ToNot(HaveOccurred())

applicationConfig := config.NewApplicationConfig(
config.WithSystemState(systemState),
)
//modelLoader := model.NewModelLoader(systemState, true)
modelConfigLoader := config.NewModelConfigLoader(systemState.Model.ModelsPath)

// Define Fiber app.
app := fiber.New()
app.Put("/import-model", ImportModelEndpoint(modelConfigLoader, applicationConfig))

requestBody := bytes.NewBufferString(`{"name": "foo", "backend": "foo", "model": "foo"}`)

req := httptest.NewRequest("PUT", "/import-model", requestBody)
resp, err := app.Test(req, 5000)
Expect(err).ToNot(HaveOccurred())

body, err := io.ReadAll(resp.Body)
defer resp.Body.Close()
Expect(err).ToNot(HaveOccurred())
Expect(string(body)).To(ContainSubstring("Model configuration created successfully"))
Expect(resp.StatusCode).To(Equal(fiber.StatusOK))

app.Get("/edit-model/:name", EditModelEndpoint(modelConfigLoader, applicationConfig))
requestBody = bytes.NewBufferString(`{"name": "foo", "parameters": { "model": "foo"}}`)

req = httptest.NewRequest("GET", "/edit-model/foo", requestBody)
resp, _ = app.Test(req, 1)

body, err = io.ReadAll(resp.Body)
defer resp.Body.Close()
Expect(err).ToNot(HaveOccurred())
Expect(string(body)).To(ContainSubstring(`"model":"foo"`))
Expect(resp.StatusCode).To(Equal(fiber.StatusOK))
})
})
})
Loading
Loading