Skip to content

Commit 847d303

Browse files
committed
Convert to a YAML editor
Signed-off-by: Ettore Di Giacinto <[email protected]>
1 parent e4df0db commit 847d303

File tree

6 files changed

+906
-695
lines changed

6 files changed

+906
-695
lines changed

core/http/endpoints/localai/edit_model.go

Lines changed: 35 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,10 @@ import (
44
"fmt"
55
"os"
66
"path/filepath"
7-
"regexp"
8-
"strconv"
9-
"strings"
107

118
"github.com/gofiber/fiber/v2"
129
"github.com/mudler/LocalAI/core/config"
13-
"github.com/mudler/LocalAI/pkg/downloader"
10+
"github.com/mudler/LocalAI/pkg/utils"
1411
"gopkg.in/yaml.v3"
1512
)
1613

@@ -28,6 +25,14 @@ func GetEditModelPage(cl *config.BackendConfigLoader, appConfig *config.Applicat
2825

2926
// Load the existing configuration
3027
configPath := filepath.Join(appConfig.ModelPath, modelName+".yaml")
28+
if err := utils.InTrustedRoot(configPath, appConfig.ModelPath); err != nil {
29+
response := ModelResponse{
30+
Success: false,
31+
Error: "Model configuration not trusted: " + err.Error(),
32+
}
33+
return c.Status(404).JSON(response)
34+
}
35+
3136
if _, err := os.Stat(configPath); os.IsNotExist(err) {
3237
response := ModelResponse{
3338
Success: false,
@@ -56,7 +61,7 @@ func GetEditModelPage(cl *config.BackendConfigLoader, appConfig *config.Applicat
5661
}
5762

5863
// Render the edit page with the current configuration
59-
return c.Render("views/edit-model", fiber.Map{
64+
return c.Render("views/model-editor", fiber.Map{
6065
"ModelName": modelName,
6166
"Config": backendConfig,
6267
})
@@ -73,11 +78,22 @@ func EditModelEndpoint(cl *config.BackendConfigLoader, appConfig *config.Applica
7378
})
7479
}
7580

76-
var req ModelRequest
77-
if err := c.BodyParser(&req); err != nil {
81+
// Get the raw body as YAML
82+
body := c.Body()
83+
if len(body) == 0 {
7884
response := ModelResponse{
7985
Success: false,
80-
Error: "Failed to parse request: " + err.Error(),
86+
Error: "Request body is empty",
87+
}
88+
return c.Status(400).JSON(response)
89+
}
90+
91+
// Unmarshal YAML directly into BackendConfig
92+
var req config.BackendConfig
93+
if err := yaml.Unmarshal(body, &req); err != nil {
94+
response := ModelResponse{
95+
Success: false,
96+
Error: "Failed to parse YAML: " + err.Error(),
8197
}
8298
return c.Status(400).JSON(response)
8399
}
@@ -93,6 +109,14 @@ func EditModelEndpoint(cl *config.BackendConfigLoader, appConfig *config.Applica
93109

94110
// Load the existing configuration
95111
configPath := filepath.Join(appConfig.ModelPath, modelName+".yaml")
112+
if err := utils.InTrustedRoot(configPath, appConfig.ModelPath); err != nil {
113+
response := ModelResponse{
114+
Success: false,
115+
Error: "Model configuration not trusted: " + err.Error(),
116+
}
117+
return c.Status(404).JSON(response)
118+
}
119+
96120
if _, err := os.Stat(configPath); os.IsNotExist(err) {
97121
response := ModelResponse{
98122
Success: false,
@@ -111,146 +135,25 @@ func EditModelEndpoint(cl *config.BackendConfigLoader, appConfig *config.Applica
111135
return c.Status(500).JSON(response)
112136
}
113137

114-
var existingConfig config.BackendConfig
115-
if err := yaml.Unmarshal(configData, &existingConfig); err != nil {
138+
var backendConfig config.BackendConfig
139+
if err := yaml.Unmarshal(configData, &backendConfig); err != nil {
116140
response := ModelResponse{
117141
Success: false,
118142
Error: "Failed to parse existing configuration: " + err.Error(),
119143
}
120144
return c.Status(500).JSON(response)
121145
}
122146

123-
// Update the configuration with new values
124-
backendConfig := &existingConfig
125-
backendConfig.Backend = req.Backend
126-
backendConfig.Description = req.Description
127-
backendConfig.Usage = req.Usage
128-
backendConfig.Model = req.Model
129-
backendConfig.MMProj = req.MMProj
130-
131-
// Set template configuration
132-
if req.ChatTemplate != "" || req.CompletionTemplate != "" {
133-
backendConfig.TemplateConfig = config.TemplateConfig{
134-
Chat: req.ChatTemplate,
135-
Completion: req.CompletionTemplate,
136-
}
137-
}
138-
139-
// Set system prompt
140-
if req.SystemPrompt != "" {
141-
backendConfig.SystemPrompt = req.SystemPrompt
142-
}
143-
144-
// Set prediction options
145-
if temp, err := strconv.ParseFloat(req.Temperature, 32); err == nil {
146-
backendConfig.Temperature = &temp
147-
}
148-
if topP, err := strconv.ParseFloat(req.TopP, 32); err == nil {
149-
backendConfig.TopP = &topP
150-
}
151-
if topK, err := strconv.Atoi(req.TopK); err == nil {
152-
backendConfig.TopK = &topK
153-
}
154-
if ctxSize, err := strconv.Atoi(req.ContextSize); err == nil {
155-
backendConfig.ContextSize = &ctxSize
156-
}
157-
if threads, err := strconv.Atoi(req.Threads); err == nil {
158-
backendConfig.Threads = &threads
159-
}
160-
if seed, err := strconv.Atoi(req.Seed); err == nil {
161-
backendConfig.Seed = &seed
162-
}
163-
164-
// Set feature flags
165-
backendConfig.F16 = &req.F16
166-
backendConfig.CUDA = req.CUDA
167-
backendConfig.Embeddings = &req.Embeddings
168-
backendConfig.Debug = &req.Debug
169-
backendConfig.MMap = &req.MMap
170-
backendConfig.MMlock = &req.MMlock
171-
172-
// Set backend-specific configurations
173-
switch req.Backend {
174-
case "llama.cpp":
175-
if req.LoraAdapter != "" {
176-
backendConfig.LoraAdapter = req.LoraAdapter
177-
}
178-
if req.Grammar != "" {
179-
backendConfig.Grammar = req.Grammar
180-
}
181-
case "diffusers":
182-
if req.PipelineType != "" {
183-
backendConfig.Diffusers.PipelineType = req.PipelineType
184-
}
185-
if req.SchedulerType != "" {
186-
backendConfig.Diffusers.SchedulerType = req.SchedulerType
187-
}
188-
case "whisper":
189-
if req.AudioPath != "" {
190-
backendConfig.AudioPath = req.AudioPath
191-
}
192-
case "bark-cpp":
193-
if req.Voice != "" {
194-
backendConfig.Voice = req.Voice
195-
}
196-
}
197-
198147
// Set defaults
199148
backendConfig.SetDefaults()
200149

201-
// Check if model file is a URL and needs downloading
202-
if strings.HasPrefix(req.Model, "http") {
203-
// Add to download files
204-
backendConfig.DownloadFiles = append(backendConfig.DownloadFiles, config.File{
205-
URI: downloader.URI(req.Model),
206-
})
207-
}
208-
209-
if req.MMProj != "" && strings.HasPrefix(req.MMProj, "http") {
210-
// Add MMProj to download files
211-
backendConfig.DownloadFiles = append(backendConfig.DownloadFiles, config.File{
212-
URI: downloader.URI(req.MMProj),
213-
})
214-
}
215-
216150
// Validate the configuration
217151
if !backendConfig.Validate() {
218-
// Provide more specific validation error messages
219-
var validationErrors []string
220-
221-
if backendConfig.Backend == "" {
222-
validationErrors = append(validationErrors, "Backend name is required")
223-
} else {
224-
// Check if backend name contains invalid characters
225-
re := regexp.MustCompile(`^[a-zA-Z0-9-_\.]+$`)
226-
if !re.MatchString(backendConfig.Backend) {
227-
validationErrors = append(validationErrors, "Backend name contains invalid characters (only letters, numbers, hyphens, underscores, and dots are allowed)")
228-
}
229-
}
230-
231-
if backendConfig.Model == "" {
232-
validationErrors = append(validationErrors, "Model file/URL is required")
233-
}
234-
235-
// Check for path traversal attempts
236-
if strings.Contains(backendConfig.Model, "..") || strings.Contains(backendConfig.Model, string(os.PathSeparator)) {
237-
validationErrors = append(validationErrors, "Model path contains invalid characters")
238-
}
239-
240-
if backendConfig.MMProj != "" {
241-
if strings.Contains(backendConfig.MMProj, "..") || strings.Contains(backendConfig.MMProj, string(os.PathSeparator)) {
242-
validationErrors = append(validationErrors, "MMProj path contains invalid characters")
243-
}
244-
}
245-
246-
if len(validationErrors) == 0 {
247-
validationErrors = append(validationErrors, "Configuration validation failed")
248-
}
249152

250153
response := ModelResponse{
251154
Success: false,
252155
Error: "Validation failed",
253-
Details: validationErrors,
156+
Details: []string{"Calling backend.Config.Validate()"},
254157
}
255158
return c.Status(400).JSON(response)
256159
}

0 commit comments

Comments
 (0)