-
Notifications
You must be signed in to change notification settings - Fork 1
feat(ui): add front-page stats #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -128,6 +128,7 @@ func API(application *application.Application) (*fiber.App, error) { | |
| router.Use(recover.New()) | ||
| } | ||
|
|
||
| // OpenTelemetry metrics for Prometheus export | ||
| if !application.ApplicationConfig().DisableMetrics { | ||
| metricsService, err := services.NewLocalAIMetricsService() | ||
| if err != nil { | ||
|
|
@@ -141,6 +142,7 @@ func API(application *application.Application) (*fiber.App, error) { | |
| }) | ||
| } | ||
| } | ||
|
|
||
| // Health Checks should always be exempt from auth, so register these first | ||
| routes.HealthRoutes(router) | ||
|
|
||
|
|
@@ -202,12 +204,28 @@ func API(application *application.Application) (*fiber.App, error) { | |
| routes.RegisterElevenLabsRoutes(router, requestExtractor, application.ModelConfigLoader(), application.ModelLoader(), application.ApplicationConfig()) | ||
| routes.RegisterLocalAIRoutes(router, requestExtractor, application.ModelConfigLoader(), application.ModelLoader(), application.ApplicationConfig(), application.GalleryService()) | ||
| routes.RegisterOpenAIRoutes(router, requestExtractor, application) | ||
|
|
||
| if !application.ApplicationConfig().DisableWebUI { | ||
|
|
||
| // Create metrics store for tracking usage (before API routes registration) | ||
| metricsStore := services.NewInMemoryMetricsStore() | ||
|
|
||
| // Add metrics middleware BEFORE API routes so it can intercept them | ||
| router.Use(middleware.MetricsMiddleware(metricsStore)) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The MetricsMiddleware is registered via router.Use() AFTER RegisterElevenLabsRoutes, RegisterLocalAIRoutes, and RegisterOpenAIRoutes have already been called, so it will not intercept those routes in Fiber; move the router.Use(middleware.MetricsMiddleware(metricsStore)) call to before the route registrations. Suggested fix // Create metrics store for tracking usage
metricsStore := services.NewInMemoryMetricsStore()
// Register cleanup on shutdown
router.Hooks().OnShutdown(func() error {
metricsStore.Stop()
log.Info().Msg("Metrics store stopped")
return nil
})
// Add metrics middleware BEFORE API routes so it can intercept them
router.Use(middleware.MetricsMiddleware(metricsStore))
// ... then register API routes ...
routes.RegisterElevenLabsRoutes(...)
routes.RegisterLocalAIRoutes(...)
routes.RegisterOpenAIRoutes(...)Prompt for AI assistanceCopy the prompt below and paste it into ChatGPT, Claude, or any LLM: |
||
|
|
||
| // Register cleanup on shutdown | ||
| router.Hooks().OnShutdown(func() error { | ||
| metricsStore.Stop() | ||
| log.Info().Msg("Metrics store stopped") | ||
| return nil | ||
| }) | ||
|
Comment on lines
208
to
+221
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The metricsStore and MetricsMiddleware are created inside the DisableWebUI guard block, so metrics collection is silently skipped when the Web UI is disabled, even though the metrics store is presumably useful for API-level tracking regardless of UI state. Suggested fix // Create metrics store for tracking usage (before API routes registration)
metricsStore := services.NewInMemoryMetricsStore()
// Add metrics middleware BEFORE API routes so it can intercept them
router.Use(middleware.MetricsMiddleware(metricsStore))
// Register cleanup on shutdown
router.Hooks().OnShutdown(func() error {
metricsStore.Stop()
log.Info().Msg("Metrics store stopped")
return nil
})
// ... register API routes ...
if !application.ApplicationConfig().DisableWebUI {
opcache := services.NewOpCache(application.GalleryService())
routes.RegisterUIAPIRoutes(router, application.ModelConfigLoader(), application.ApplicationConfig(), application.GalleryService(), opcache, metricsStore)
routes.RegisterUIRoutes(router, application.ModelConfigLoader(), application.ModelLoader(), application.ApplicationConfig(), application.GalleryService())
}Prompt for AI assistanceCopy the prompt below and paste it into ChatGPT, Claude, or any LLM: |
||
|
|
||
| // Create opcache for tracking UI operations | ||
| opcache := services.NewOpCache(application.GalleryService()) | ||
| routes.RegisterUIAPIRoutes(router, application.ModelConfigLoader(), application.ApplicationConfig(), application.GalleryService(), opcache) | ||
| routes.RegisterUIAPIRoutes(router, application.ModelConfigLoader(), application.ApplicationConfig(), application.GalleryService(), opcache, metricsStore) | ||
|
Comment on lines
+207
to
+225
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The metrics middleware is registered AFTER the API routes (ElevenLabs, LocalAI, OpenAI, JINA), so it will never intercept requests to those routes; move Suggested fix // Create metrics store for tracking usage (before API routes registration)
var metricsStore services.MetricsStore
if !application.ApplicationConfig().DisableWebUI {
metricsStore = services.NewInMemoryMetricsStore()
// Add metrics middleware BEFORE API routes so it can intercept them
router.Use(middleware.MetricsMiddleware(metricsStore))
// Register cleanup on shutdown
router.Hooks().OnShutdown(func() error {
metricsStore.Stop()
log.Info().Msg("Metrics store stopped")
return nil
})
}
routes.RegisterElevenLabsRoutes(router, ...)
routes.RegisterLocalAIRoutes(router, ...)
routes.RegisterOpenAIRoutes(router, ...)
if !application.ApplicationConfig().DisableWebUI {
opcache := services.NewOpCache(application.GalleryService())
routes.RegisterUIAPIRoutes(router, application.ModelConfigLoader(), application.ApplicationConfig(), application.GalleryService(), opcache, metricsStore)
routes.RegisterUIRoutes(router, application.ModelConfigLoader(), application.ModelLoader(), application.ApplicationConfig(), application.GalleryService())
}Prompt for AI assistanceCopy the prompt below and paste it into ChatGPT, Claude, or any LLM:
Comment on lines
+207
to
+225
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The MetricsMiddleware is registered after RegisterOpenAIRoutes, RegisterLocalAIRoutes, and RegisterElevenLabsRoutes, so it will never intercept those API requests; the comment on line 213 states it should be added BEFORE API routes. Also reported at: Suggested fix if !application.ApplicationConfig().DisableWebUI {
// Create metrics store for tracking usage (before API routes registration)
metricsStore := services.NewInMemoryMetricsStore()
// Add metrics middleware BEFORE API routes so it can intercept them
router.Use(middleware.MetricsMiddleware(metricsStore))
// Register cleanup on shutdown
router.Hooks().OnShutdown(func() error {
metricsStore.Stop()
log.Info().Msg("Metrics store stopped")
return nil
})
}
routes.RegisterElevenLabsRoutes(...)
routes.RegisterLocalAIRoutes(...)
routes.RegisterOpenAIRoutes(...)
if !application.ApplicationConfig().DisableWebUI {
opcache := services.NewOpCache(application.GalleryService())
routes.RegisterUIAPIRoutes(router, application.ModelConfigLoader(), application.ApplicationConfig(), application.GalleryService(), opcache, metricsStore)
routes.RegisterUIRoutes(...)
}Prompt for AI assistanceCopy the prompt below and paste it into ChatGPT, Claude, or any LLM: |
||
| routes.RegisterUIRoutes(router, application.ModelConfigLoader(), application.ModelLoader(), application.ApplicationConfig(), application.GalleryService()) | ||
|
Comment on lines
208
to
226
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
In Fiber v2, routes are matched in registration order. The |
||
| } | ||
|
|
||
| routes.RegisterJINARoutes(router, requestExtractor, application.ModelConfigLoader(), application.ModelLoader(), application.ApplicationConfig()) | ||
|
|
||
| // Define a custom 404 handler | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,61 @@ | ||||||
| package localai | ||||||
|
|
||||||
| import ( | ||||||
| "github.com/gofiber/fiber/v2" | ||||||
| "github.com/mudler/LocalAI/core/config" | ||||||
| "github.com/mudler/LocalAI/core/gallery" | ||||||
| "github.com/mudler/LocalAI/core/http/utils" | ||||||
| "github.com/mudler/LocalAI/core/services" | ||||||
| "github.com/mudler/LocalAI/internal" | ||||||
| "github.com/mudler/LocalAI/pkg/model" | ||||||
| ) | ||||||
|
|
||||||
| // SettingsEndpoint handles the settings page which shows detailed model/backend management | ||||||
| func SettingsEndpoint(appConfig *config.ApplicationConfig, | ||||||
| cl *config.ModelConfigLoader, ml *model.ModelLoader, opcache *services.OpCache) func(*fiber.Ctx) error { | ||||||
| return func(c *fiber.Ctx) error { | ||||||
| modelConfigs := cl.GetAllModelsConfigs() | ||||||
| galleryConfigs := map[string]*gallery.ModelConfig{} | ||||||
|
|
||||||
| installedBackends, err := gallery.ListSystemBackends(appConfig.SystemState) | ||||||
| if err != nil { | ||||||
| return err | ||||||
| } | ||||||
|
|
||||||
| for _, m := range modelConfigs { | ||||||
| cfg, err := gallery.GetLocalModelConfiguration(ml.ModelPath, m.Name) | ||||||
| if err != nil { | ||||||
| continue | ||||||
| } | ||||||
| galleryConfigs[m.Name] = cfg | ||||||
| } | ||||||
|
|
||||||
| loadedModels := ml.ListLoadedModels() | ||||||
| loadedModelsMap := map[string]bool{} | ||||||
| for _, m := range loadedModels { | ||||||
| loadedModelsMap[m.ID] = true | ||||||
| } | ||||||
|
|
||||||
| modelsWithoutConfig, _ := services.ListModels(cl, ml, config.NoFilterFn, services.LOOSE_ONLY) | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The error from Suggested fix modelsWithoutConfig, err := services.ListModels(cl, ml, config.NoFilterFn, services.LOOSE_ONLY)
if err != nil {
return err
}Prompt for AI assistanceCopy the prompt below and paste it into ChatGPT, Claude, or any LLM: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The error from Suggested fix modelsWithoutConfig, err := services.ListModels(cl, ml, config.NoFilterFn, services.LOOSE_ONLY)
if err != nil {
return err
}Prompt for AI assistanceCopy the prompt below and paste it into ChatGPT, Claude, or any LLM: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The error from Suggested fix modelsWithoutConfig, err := services.ListModels(cl, ml, config.NoFilterFn, services.LOOSE_ONLY)
if err != nil {
log.Error().Err(err).Msg("error listing models")
}Prompt for AI assistanceCopy the prompt below and paste it into ChatGPT, Claude, or any LLM: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The error from ListModels is ignored and should be handled to avoid rendering a partial or misleading settings page. Suggested fix modelsWithoutConfig, err := services.ListModels(cl, ml, config.NoFilterFn, services.LOOSE_ONLY)
if err != nil {
return err
}Prompt for AI assistanceCopy the prompt below and paste it into ChatGPT, Claude, or any LLM: |
||||||
|
|
||||||
| // Get model statuses to display in the UI the operation in progress | ||||||
| processingModels, taskTypes := opcache.GetStatus() | ||||||
|
|
||||||
| summary := fiber.Map{ | ||||||
| "Title": "LocalAI - Settings & Management", | ||||||
| "Version": internal.PrintableVersion(), | ||||||
| "BaseURL": utils.BaseURL(c), | ||||||
| "Models": modelsWithoutConfig, | ||||||
| "ModelsConfig": modelConfigs, | ||||||
| "GalleryConfig": galleryConfigs, | ||||||
| "ApplicationConfig": appConfig, | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The full Suggested fix // Only expose the specific fields the template actually needs, not the entire config struct.
// e.g. "DebugMode": appConfig.Debug,Prompt for AI assistanceCopy the prompt below and paste it into ChatGPT, Claude, or any LLM: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested fix // Only expose the specific fields needed by the template rather than the full appConfig struct
"ApplicationConfig": appConfig, // TODO: replace with a safe DTO containing only UI-relevant fieldsPrompt for AI assistanceCopy the prompt below and paste it into ChatGPT, Claude, or any LLM: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The full
Suggested change
Prompt for AI assistanceCopy the prompt below and paste it into ChatGPT, Claude, or any LLM: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Passing the entire application config into the template exposes all server settings to the UI, so only include the specific non-sensitive fields the view needs. Suggested fix "ApplicationConfig": fiber.Map{
"SomeSafeField": appConfig.SomeSafeField,
},Prompt for AI assistanceCopy the prompt below and paste it into ChatGPT, Claude, or any LLM: |
||||||
| "ProcessingModels": processingModels, | ||||||
| "TaskTypes": taskTypes, | ||||||
| "LoadedModels": loadedModelsMap, | ||||||
| "InstalledBackends": installedBackends, | ||||||
| } | ||||||
|
Comment on lines
+44
to
+56
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Passing the entire application config into the view exposes server configuration to the UI, so send only the specific fields the template needs. Suggested fix summary := fiber.Map{
"Title": "LocalAI - Settings & Management",
"Version": internal.PrintableVersion(),
"BaseURL": utils.BaseURL(c),
"Models": modelsWithoutConfig,
"ModelsConfig": modelConfigs,
"GalleryConfig": galleryConfigs,
"ProcessingModels": processingModels,
"TaskTypes": taskTypes,
"LoadedModels": loadedModelsMap,
"InstalledBackends": installedBackends,
}Prompt for AI assistanceCopy the prompt below and paste it into ChatGPT, Claude, or any LLM: |
||||||
|
|
||||||
| // Render settings page | ||||||
| return c.Render("views/settings", summary) | ||||||
| } | ||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,174 @@ | ||
| package middleware | ||
|
|
||
| import ( | ||
| "encoding/json" | ||
| "strings" | ||
| "time" | ||
|
|
||
| "github.com/gofiber/fiber/v2" | ||
| "github.com/mudler/LocalAI/core/services" | ||
| "github.com/rs/zerolog/log" | ||
| ) | ||
|
|
||
| // MetricsMiddleware creates a middleware that tracks API usage metrics | ||
| // Note: Uses CONTEXT_LOCALS_KEY_MODEL_NAME constant defined in request.go | ||
| func MetricsMiddleware(metricsStore services.MetricsStore) fiber.Handler { | ||
| return func(c *fiber.Ctx) error { | ||
| path := c.Path() | ||
|
|
||
| // Skip tracking for UI routes, static files, and non-API endpoints | ||
| if shouldSkipMetrics(path) { | ||
| return c.Next() | ||
| } | ||
|
|
||
| // Record start time | ||
| start := time.Now() | ||
|
|
||
| // Get endpoint category | ||
| endpoint := categorizeEndpoint(path) | ||
|
|
||
| // Continue with the request | ||
| err := c.Next() | ||
|
|
||
| // Record metrics after request completes | ||
| duration := time.Since(start) | ||
| success := err == nil && c.Response().StatusCode() < 400 | ||
|
|
||
| // Extract model name from context (set by RequestExtractor middleware) | ||
| // Use the same constant as RequestExtractor | ||
| model := "unknown" | ||
| if modelVal, ok := c.Locals(CONTEXT_LOCALS_KEY_MODEL_NAME).(string); ok && modelVal != "" { | ||
| model = modelVal | ||
| log.Debug().Str("model", model).Str("endpoint", endpoint).Msg("Recording metrics for request") | ||
| } else { | ||
| // Fallback: try to extract from path params or query | ||
| model = extractModelFromRequest(c) | ||
| log.Debug().Str("model", model).Str("endpoint", endpoint).Msg("Recording metrics for request (fallback)") | ||
| } | ||
|
|
||
| // Extract backend from response headers if available | ||
| backend := string(c.Response().Header.Peek("X-LocalAI-Backend")) | ||
|
|
||
| // Record the request | ||
| metricsStore.RecordRequest(endpoint, model, backend, success, duration) | ||
|
|
||
| return err | ||
| } | ||
| } | ||
|
|
||
| // shouldSkipMetrics determines if a request should be excluded from metrics | ||
| func shouldSkipMetrics(path string) bool { | ||
| // Skip UI routes | ||
| skipPrefixes := []string{ | ||
| "/views/", | ||
| "/static/", | ||
| "/browse/", | ||
| "/chat/", | ||
| "/text2image/", | ||
| "/tts/", | ||
| "/talk/", | ||
| "/models/edit/", | ||
| "/import-model", | ||
| "/settings", | ||
| "/api/models", // UI API endpoints | ||
| "/api/backends", // UI API endpoints | ||
| "/api/operations", // UI API endpoints | ||
| "/api/p2p", // UI API endpoints | ||
| "/api/metrics", // Metrics API itself | ||
| } | ||
|
Comment on lines
+62
to
+78
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested fix skipPrefixes := []string{
"/views/",
"/static/",
"/browse/",
"/chat/",
"/text2image/",
"/tts/",
"/talk/",
"/models/edit/",
"/import-model",
"/settings",
"/metrics", // Prometheus metrics endpoint
"/api/models", // UI API endpoints
"/api/backends", // UI API endpoints
"/api/operations", // UI API endpoints
"/api/p2p", // UI API endpoints
"/api/metrics", // Metrics API itself
}Prompt for AI assistanceCopy the prompt below and paste it into ChatGPT, Claude, or any LLM: |
||
|
|
||
| for _, prefix := range skipPrefixes { | ||
| if strings.HasPrefix(path, prefix) { | ||
| return true | ||
| } | ||
| } | ||
|
|
||
| // Also skip root path and other UI pages | ||
| if path == "/" || path == "/index" { | ||
| return true | ||
| } | ||
|
|
||
| return false | ||
| } | ||
|
|
||
| // categorizeEndpoint maps request paths to friendly endpoint categories | ||
| func categorizeEndpoint(path string) string { | ||
| // OpenAI-compatible endpoints | ||
| if strings.HasPrefix(path, "/v1/chat/completions") || strings.HasPrefix(path, "/chat/completions") { | ||
| return "chat" | ||
| } | ||
| if strings.HasPrefix(path, "/v1/completions") || strings.HasPrefix(path, "/completions") { | ||
| return "completions" | ||
| } | ||
| if strings.HasPrefix(path, "/v1/embeddings") || strings.HasPrefix(path, "/embeddings") { | ||
| return "embeddings" | ||
| } | ||
| if strings.HasPrefix(path, "/v1/images/generations") || strings.HasPrefix(path, "/images/generations") { | ||
| return "image-generation" | ||
| } | ||
| if strings.HasPrefix(path, "/v1/audio/transcriptions") || strings.HasPrefix(path, "/audio/transcriptions") { | ||
| return "transcriptions" | ||
| } | ||
| if strings.HasPrefix(path, "/v1/audio/speech") || strings.HasPrefix(path, "/audio/speech") { | ||
| return "text-to-speech" | ||
| } | ||
| if strings.HasPrefix(path, "/v1/models") || strings.HasPrefix(path, "/models") { | ||
| return "models" | ||
| } | ||
|
|
||
| // LocalAI-specific endpoints | ||
| if strings.HasPrefix(path, "/v1/internal") { | ||
| return "internal" | ||
| } | ||
| if strings.Contains(path, "/tts") { | ||
| return "text-to-speech" | ||
| } | ||
| if strings.Contains(path, "/stt") || strings.Contains(path, "/whisper") { | ||
| return "speech-to-text" | ||
| } | ||
| if strings.Contains(path, "/sound-generation") { | ||
| return "sound-generation" | ||
| } | ||
|
|
||
| // Default to the first path segment | ||
| parts := strings.Split(strings.Trim(path, "/"), "/") | ||
| if len(parts) > 0 { | ||
| return parts[0] | ||
| } | ||
|
Comment on lines
+134
to
+137
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested fix parts := strings.Split(strings.Trim(path, "/"), "/")
if len(parts) > 0 && parts[0] != "" {
return parts[0]
}Prompt for AI assistanceCopy the prompt below and paste it into ChatGPT, Claude, or any LLM: |
||
|
|
||
| return "unknown" | ||
|
Comment on lines
+134
to
+139
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested fix parts := strings.Split(strings.Trim(path, "/"), "/")
if len(parts) > 0 && parts[0] != "" {
return parts[0]
}
return "unknown"Prompt for AI assistanceCopy the prompt below and paste it into ChatGPT, Claude, or any LLM:
Comment on lines
+134
to
+139
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested fix parts := strings.Split(strings.Trim(path, "/"), "/")
if len(parts) > 0 && parts[0] != "" {
return parts[0]
}
return "unknown"Prompt for AI assistanceCopy the prompt below and paste it into ChatGPT, Claude, or any LLM:
Comment on lines
+134
to
+139
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested fix parts := strings.Split(strings.Trim(path, "/"), "/")
if len(parts) > 0 && parts[0] != "" {
return parts[0]
}
return "unknown"Prompt for AI assistanceCopy the prompt below and paste it into ChatGPT, Claude, or any LLM: |
||
| } | ||
|
|
||
| // extractModelFromRequest attempts to extract the model name from the request | ||
| func extractModelFromRequest(c *fiber.Ctx) string { | ||
| // Try query parameter first | ||
| model := c.Query("model") | ||
| if model != "" { | ||
| return model | ||
| } | ||
|
|
||
| // Try to extract from JSON body for POST requests | ||
| if c.Method() == fiber.MethodPost { | ||
| // Read body | ||
| bodyBytes := c.Body() | ||
| if len(bodyBytes) > 0 { | ||
| // Parse JSON | ||
| var reqBody map[string]interface{} | ||
| if err := json.Unmarshal(bodyBytes, &reqBody); err == nil { | ||
| if modelVal, ok := reqBody["model"]; ok { | ||
| if modelStr, ok := modelVal.(string); ok { | ||
| return modelStr | ||
| } | ||
| } | ||
| } | ||
|
Comment on lines
+145
to
+163
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Model name extracted from user-controlled query parameters and JSON body is stored in metrics without sanitization, allowing arbitrary strings to pollute metric labels and potentially cause cardinality explosion in the metrics store. Suggested fix const maxModelNameLen = 256
sanitize := func(s string) string {
if len(s) > maxModelNameLen {
return "unknown"
}
return s
}
model := c.Query("model")
if model != "" {
return sanitize(model)
}
if c.Method() == fiber.MethodPost {
bodyBytes := c.Body()
if len(bodyBytes) > 0 {
var reqBody map[string]interface{}
if err := json.Unmarshal(bodyBytes, &reqBody); err == nil {
if modelVal, ok := reqBody["model"]; ok {
if modelStr, ok := modelVal.(string); ok {
return sanitize(modelStr)
}
}
}
}
}Prompt for AI assistanceCopy the prompt below and paste it into ChatGPT, Claude, or any LLM:
Comment on lines
+150
to
+163
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Parsing the full request body again in middleware adds avoidable CPU and memory cost on every POST request, so prefer reusing the model already extracted by earlier middleware or gate this fallback behind endpoint checks and content type validation. Suggested fix // Try to extract from JSON body only for JSON POST requests on known model-carrying endpoints
if c.Method() == fiber.MethodPost && strings.HasPrefix(c.Get("Content-Type"), fiber.MIMEApplicationJSON) {
if shouldInspectBodyForModel(c.Path()) {
bodyBytes := c.Body()
if len(bodyBytes) > 0 {
var reqBody struct {
Model string `json:"model"`
}
if err := json.Unmarshal(bodyBytes, &reqBody); err == nil && reqBody.Model != "" {
return reqBody.Model
}
}
}
}Prompt for AI assistanceCopy the prompt below and paste it into ChatGPT, Claude, or any LLM: |
||
| } | ||
| } | ||
|
Comment on lines
+151
to
+165
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In Suggested fix // NOTE: c.Body() is only reliable before c.Next() is called.
// This fallback path is only reached when the context local was not set by
// RequestExtractor, so body-based extraction here may return empty.
if c.Method() == fiber.MethodPost {
bodyBytes := c.Body()
if len(bodyBytes) > 0 {
var reqBody map[string]interface{}
if err := json.Unmarshal(bodyBytes, &reqBody); err == nil {
if modelVal, ok := reqBody["model"]; ok {
if modelStr, ok := modelVal.(string); ok {
return modelStr
}
}
}
}
}Prompt for AI assistanceCopy the prompt below and paste it into ChatGPT, Claude, or any LLM:
Comment on lines
+151
to
+165
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In Suggested fix // Try to extract from JSON body for POST requests
// Note: c.Body() is safe to call multiple times in Fiber (body is cached),
// but model extraction should ideally happen before c.Next() to avoid
// relying on post-handler body state. Consider moving extraction pre-Next.
if c.Method() == fiber.MethodPost {
bodyBytes := c.Body()
if len(bodyBytes) > 0 {
var reqBody map[string]interface{}
if err := json.Unmarshal(bodyBytes, &reqBody); err == nil {
if modelVal, ok := reqBody["model"]; ok {
if modelStr, ok := modelVal.(string); ok {
return modelStr
}
}
}
}
}Prompt for AI assistanceCopy the prompt below and paste it into ChatGPT, Claude, or any LLM:
Comment on lines
+151
to
+165
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In Suggested fix // Move model extraction to before c.Next() is called, storing the result in a local variable,
// so the body is still available. For example:
//
// modelName := extractModelFromRequest(c) // called before c.Next()
// err := c.Next()
// // use modelName after c.Next()
//
// Alternatively, rely solely on the CONTEXT_LOCALS_KEY_MODEL_NAME set by RequestExtractor.Prompt for AI assistanceCopy the prompt below and paste it into ChatGPT, Claude, or any LLM: |
||
|
|
||
| // Try path parameter for endpoints like /models/:model | ||
| model = c.Params("model") | ||
| if model != "" { | ||
| return model | ||
| } | ||
|
|
||
| return "unknown" | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -127,6 +127,10 @@ func (re *RequestExtractor) SetModelAndConfig(initializer func() schema.LocalAIR | |
| log.Debug().Str("context localModelName", localModelName).Msg("overriding empty model name in request body with value found earlier in middleware chain") | ||
| input.ModelName(&localModelName) | ||
| } | ||
| } else { | ||
| // Update context locals with the model name from the request body | ||
| // This ensures downstream middleware (like metrics) can access it | ||
| ctx.Locals(CONTEXT_LOCALS_KEY_MODEL_NAME, input.ModelName(nil)) | ||
|
Comment on lines
+130
to
+133
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The request body can overwrite the trusted model selected earlier in middleware, so only set the local when it is empty or validate it against the authorized model. Suggested fix } else {
// Preserve the model selected earlier in the middleware chain.
if localModelName, ok := ctx.Locals(CONTEXT_LOCALS_KEY_MODEL_NAME).(string); !ok || localModelName == "" {
ctx.Locals(CONTEXT_LOCALS_KEY_MODEL_NAME, input.ModelName(nil))
}
}Prompt for AI assistanceCopy the prompt below and paste it into ChatGPT, Claude, or any LLM: |
||
| } | ||
|
|
||
| cfg, err := re.modelConfigLoader.LoadModelConfigFileByNameDefaultOptions(input.ModelName(nil), re.applicationConfig) | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -23,6 +23,9 @@ func RegisterUIRoutes(app *fiber.App, | |||||
|
|
||||||
| app.Get("/", localai.WelcomeEndpoint(appConfig, cl, ml, processingOps)) | ||||||
|
|
||||||
| // Settings page - detailed model/backend management | ||||||
| app.Get("/settings", localai.SettingsEndpoint(appConfig, cl, ml, processingOps)) | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The new settings UI exposes
Suggested change
Prompt for AI assistanceCopy the prompt below and paste it into ChatGPT, Claude, or any LLM: |
||||||
|
|
||||||
| // P2P | ||||||
| app.Get("/p2p", func(c *fiber.Ctx) error { | ||||||
| summary := fiber.Map{ | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The metrics middleware is registered after API routes (RegisterElevenLabsRoutes, RegisterLocalAIRoutes, RegisterOpenAIRoutes) are already added to the router, so it will not intercept those routes despite the comment claiming it runs 'BEFORE API routes'.
Also reported at:
core/http/app.goL208–L221Suggested fix
Prompt for AI assistance
Copy the prompt below and paste it into ChatGPT, Claude, or any LLM: