Skip to content
Open
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
17 changes: 17 additions & 0 deletions core/config/model_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,19 @@ func (cfg *ModelConfig) SetDefaults(opts ...ConfigLoaderOption) {
}

func (c *ModelConfig) Validate() (bool, error) {
// Check if this is a pipeline model
isPipeline := c.IsPipeline()

// Pipeline models don't require a backend
if !isPipeline && c.Backend == "" {
return false, fmt.Errorf("backend is required for non-pipeline models")
}

// Pipeline models must have at least one pipeline component
if isPipeline && c.Pipeline.VAD == "" && c.Pipeline.Transcription == "" && c.Pipeline.TTS == "" && c.Pipeline.LLM == "" {
return false, fmt.Errorf("pipeline models must have at least one component (vad, transcription, tts, or llm)")
}

downloadedFileNames := []string{}
for _, f := range c.DownloadFiles {
downloadedFileNames = append(downloadedFileNames, f.Filename)
Expand Down Expand Up @@ -516,6 +529,10 @@ func (c *ModelConfig) Validate() (bool, error) {
return true, nil
}

func (c *ModelConfig) IsPipeline() bool {
return c.Pipeline.VAD != "" || c.Pipeline.Transcription != "" || c.Pipeline.TTS != "" || c.Pipeline.LLM != ""
}

func (c *ModelConfig) HasTemplate() bool {
return c.TemplateConfig.Completion != "" || c.TemplateConfig.Edit != "" || c.TemplateConfig.Chat != "" || c.TemplateConfig.ChatMessage != "" || c.TemplateConfig.UseTokenizerTemplate
}
Expand Down
90 changes: 90 additions & 0 deletions core/http/endpoints/localai/validate_model.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package localai

import (
"io"
"net/http"
"strings"

"github.com/labstack/echo/v4"
"github.com/mudler/LocalAI/core/config"
"gopkg.in/yaml.v3"
)

// ValidateModelEndpoint handles validating model configurations without saving them
func ValidateModelEndpoint(cl *config.ModelConfigLoader, appConfig *config.ApplicationConfig) echo.HandlerFunc {
return func(c echo.Context) error {
// Get the raw body
body, err := io.ReadAll(c.Request().Body)
if err != nil {
response := ModelResponse{
Success: false,
Error: "Failed to read request body: " + err.Error(),
}
return c.JSON(http.StatusBadRequest, response)
}
if len(body) == 0 {
response := ModelResponse{
Success: false,
Error: "Request body is empty",
}
return c.JSON(http.StatusBadRequest, response)
}

// Check content type to determine how to parse
contentType := c.Request().Header.Get("Content-Type")
var modelConfig config.ModelConfig

// Parse YAML (default) or auto-detect
if strings.Contains(contentType, "application/x-yaml") || strings.Contains(contentType, "text/yaml") {
if err := yaml.Unmarshal(body, &modelConfig); err != nil {
response := ModelResponse{
Success: false,
Error: "Failed to parse YAML: " + err.Error(),
}
return c.JSON(http.StatusBadRequest, response)
}
} else {
// Auto-detect - assume YAML for model editor
if err := yaml.Unmarshal(body, &modelConfig); err != nil {
response := ModelResponse{
Success: false,
Error: "Failed to parse YAML: " + err.Error(),
}
return c.JSON(http.StatusBadRequest, response)
}
}

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

// Set defaults before validation
modelConfig.SetDefaults(appConfig.ToConfigLoaderOptions()...)

// Validate the configuration
valid, err := modelConfig.Validate()
if !valid {
errorMsg := "Validation failed"
if err != nil {
errorMsg = err.Error()
}
response := ModelResponse{
Success: false,
Error: errorMsg,
}
return c.JSON(http.StatusBadRequest, response)
}

// Validation successful
response := ModelResponse{
Success: true,
Message: "Configuration is valid",
}
return c.JSON(http.StatusOK, response)
}
}
7 changes: 3 additions & 4 deletions core/http/endpoints/openai/realtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,14 +183,13 @@ func registerRealtime(application *application.Application, model string) func(c
}

sttModel := cfg.Pipeline.Transcription
ttsModel := cfg.Pipeline.TTS

sessionID := generateSessionID()
session := &Session{
ID: sessionID,
TranscriptionOnly: false,
Model: model,
Voice: ttsModel,
Voice: cfg.TTSConfig.Voice,
ModelConfig: cfg,
TurnDetection: &types.TurnDetectionUnion{
ServerVad: &types.ServerVad{
Expand Down Expand Up @@ -557,13 +556,13 @@ func updateSession(session *Session, update *types.SessionUnion, cl *config.Mode
session.InputAudioTranscription = &types.AudioTranscription{}
}
session.InputAudioTranscription.Model = cfg.Pipeline.Transcription
session.Voice = cfg.Pipeline.TTS
session.Voice = cfg.TTSConfig.Voice
session.Model = rt.Model
session.ModelConfig = cfg
}

if rt.Audio != nil && rt.Audio.Output != nil && rt.Audio.Output.Voice != "" {
xlog.Warn("Ignoring voice setting; not implemented", "voice", rt.Audio.Output.Voice)
session.Voice = string(rt.Audio.Output.Voice)
}

if rt.Audio != nil && rt.Audio.Input != nil && rt.Audio.Input.Transcription != nil {
Expand Down
3 changes: 3 additions & 0 deletions core/http/routes/localai.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ func RegisterLocalAIRoutes(router *echo.Echo,
// Custom model edit endpoint
router.POST("/models/edit/:name", localai.EditModelEndpoint(cl, appConfig))

// Custom model validate endpoint
router.POST("/models/validate", localai.ValidateModelEndpoint(cl, appConfig))

// Reload models endpoint
router.POST("/models/reload", localai.ReloadModelsEndpoint(cl, appConfig))
}
Expand Down
50 changes: 26 additions & 24 deletions core/http/views/model-editor.html
Original file line number Diff line number Diff line change
Expand Up @@ -1014,51 +1014,53 @@ <h2 class="text-xl font-semibold text-[#E5E7EB] flex items-center gap-3">
`;
},

validateConfig() {
async validateConfig() {
try {
const yamlContent = this.yamlEditor.getValue();
const config = jsyaml.load(yamlContent);

// Basic YAML parsing check
const config = jsyaml.load(yamlContent);
if (!config || typeof config !== 'object') {
throw new Error('Invalid YAML structure');
}

if (!config.name) {
throw new Error('Model name is required');
}
if (!config.backend) {
throw new Error('Backend is required');
}
if (!config.parameters || !config.parameters.model) {
throw new Error('Model file/path is required in parameters.model');
// Call backend validation endpoint
const response = await fetch('/models/validate', {
method: 'POST',
headers: {
'Content-Type': 'application/x-yaml',
},
body: yamlContent
});

if (!response.ok) {
const errorData = await response.json().catch(() => ({ message: 'Validation failed' }));
let errorMessage = 'Validation failed';
if (errorData.error) {
errorMessage = errorData.error;
} else if (errorData.message) {
errorMessage = errorData.message;
}
throw new Error(errorMessage);
}

this.showAlert('success', 'Configuration is valid!');

const result = await response.json();
this.showAlert('success', result.message || 'Configuration is valid!');
} catch (error) {
this.showAlert('error', 'Validation failed: ' + error.message);
}
},

async saveConfig() {
try {
// Validate before saving
const yamlContent = this.yamlEditor.getValue();
const config = jsyaml.load(yamlContent);

// Basic YAML parsing check
const config = jsyaml.load(yamlContent);
if (!config || typeof config !== 'object') {
throw new Error('Invalid YAML structure');
}

if (!config.name) {
throw new Error('Model name is required');
}
if (!config.backend) {
throw new Error('Backend is required');
}
if (!config.parameters || !config.parameters.model) {
throw new Error('Model file/path is required in parameters.model');
}

const endpoint = this.isEditMode ? `/models/edit/{{.ModelName}}` : '/models/import';

const response = await fetch(endpoint, {
Expand Down
Loading