Skip to content
Open
6 changes: 3 additions & 3 deletions .github/workflows/secscan.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ jobs:
steps:
- name: Checkout Source
uses: actions/checkout@v6
if: ${{ github.actor != 'dependabot[bot]' }}
if: ${{ !github.repository.fork && github.actor != 'dependabot[bot]' }}
- name: Run Gosec Security Scanner
if: ${{ github.actor != 'dependabot[bot]' }}
if: ${{ !github.repository.fork && github.actor != 'dependabot[bot]' }}
uses: securego/gosec@v2.27.1
with:
# we let the report trigger content trigger a failure using the GitHub Security features.
args: '-no-fail -fmt sarif -out results.sarif ./...'
- name: Upload SARIF file
if: ${{ github.actor != 'dependabot[bot]' }}
if: ${{ !github.repository.fork && github.actor != 'dependabot[bot]' }}
uses: github/codeql-action/upload-sarif@v4
with:
# Path to SARIF file relative to the root of the repository
Expand Down
3 changes: 1 addition & 2 deletions core/http/endpoints/explorer/dashboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,7 @@ func AddNetwork(db *explorer.Database) echo.HandlerFunc {
return c.JSON(http.StatusBadRequest, map[string]any{"error": "Description is required"})
}

// TODO: check if token is valid, otherwise reject
// try to decode the token from base64
// Validate token by decoding from base64
_, err := base64.StdEncoding.DecodeString(request.Token)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]any{"error": "Invalid token"})
Expand Down
2 changes: 1 addition & 1 deletion core/http/endpoints/localai/backend_monitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func BackendMonitorEndpoint(bm *monitoring.BackendMonitorService) echo.HandlerFu
return echo.NewHTTPError(400, "model query parameter is required")
}

resp, err := bm.CheckAndSample(model)
resp, err := bm.CheckAndSample(c.Request().Context(), model)
if err != nil {
return err
}
Expand Down
30 changes: 30 additions & 0 deletions core/http/endpoints/localai/config_meta.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,36 @@ func AutocompleteEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, a
}
}

// GetConfigEndpoint returns the YAML + JSON view for an installed model.
// Used by the MCP httpapi.Client for get_model_config, and by the React
// model editor when it wants a clean disk-read view (not the in-memory
// loader copy which has SetDefaults applied).
// @Summary Read a model configuration from disk
// @Description Returns the raw YAML and parsed JSON view of an installed model's config file
// @Tags config
// @Produce json
// @Param name path string true "Model name"
// @Success 200 {object} map[string]any "{name, yaml, json}"
// @Router /api/models/config-yaml/{name} [get]
func GetConfigEndpoint(cl *config.ModelConfigLoader, appConfig *config.ApplicationConfig) echo.HandlerFunc {
svc := modeladmin.NewConfigService(cl, appConfig)
return func(c echo.Context) error {
modelName := c.Param("name")
if decoded, err := url.PathUnescape(modelName); err == nil {
modelName = decoded
}
view, err := svc.GetConfig(c.Request().Context(), modelName)
if err != nil {
return c.JSON(httpStatusForModelAdminError(err), map[string]any{"error": err.Error()})
}
return c.JSON(http.StatusOK, map[string]any{
"name": view.Name,
"yaml": view.YAML,
"json": view.JSON,
})
}
}

// PatchConfigEndpoint handles PATCH requests to partially update a model config
// using nested JSON merge.
// @Summary Partially update a model configuration
Expand Down
2 changes: 0 additions & 2 deletions core/http/endpoints/localai/get_token_metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ import (
"github.com/mudler/LocalAI/pkg/model"
)

// TODO: This is not yet in use. Needs middleware rework, since it is not referenced.

// TokenMetricsEndpoint is an endpoint to get TokensProcessed Per Second for Active SlotID
//
// @Summary Get TokenMetrics for Active Slot.
Expand Down
2 changes: 1 addition & 1 deletion core/http/endpoints/localai/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func LocalAIMetricsAPIMiddleware(metrics *monitoring.LocalAIMetricsService) echo
start := time.Now()
err := next(c)
elapsed := float64(time.Since(start)) / float64(time.Second)
cfg.metricsService.ObserveAPICall(method, path, elapsed)
cfg.metricsService.ObserveAPICall(c.Request().Context(), method, path, elapsed)
return err
}
}
Expand Down
51 changes: 42 additions & 9 deletions core/http/endpoints/localai/video.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
Expand Down Expand Up @@ -182,14 +183,8 @@ func VideoEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, appConfi
}
outputFile.Close()

// TODO: use mime type to determine the extension
output := outputFile.Name() + ".mp4"

// Rename the temporary file
err = os.Rename(outputFile.Name(), output)
if err != nil {
return err
}
// Backend writes to a path without extension; we detect format afterward
rawOutput := outputFile.Name()

baseURL := middleware.BaseURL(c)

Expand All @@ -210,7 +205,7 @@ func VideoEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, appConfi
input.NegativePrompt,
src,
endSrc,
output,
rawOutput,
input.NumFrames,
input.FPS,
input.Seed,
Expand All @@ -227,6 +222,15 @@ func VideoEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, appConfi
return err
}

// Determine extension from content type and rename
output := rawOutput
ext := videoExtFromContentType(rawOutput)
if filepath.Ext(rawOutput) == "" && ext != "" {
if err := os.Rename(rawOutput, rawOutput+ext); err == nil {
output = rawOutput + ext
}
}

item := &schema.Item{}

if b64JSON {
Expand Down Expand Up @@ -259,3 +263,32 @@ func VideoEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, appConfi
return c.JSON(200, resp)
}
}

func videoExtFromContentType(path string) string {
f, err := os.Open(path)
if err != nil {
return ".mp4"
}
defer f.Close()

head := make([]byte, 512)
n, err := f.Read(head)
if err != nil && err != io.EOF {
return ".mp4"
}

ct := http.DetectContentType(head[:n])
switch ct {
case "video/mp4":
return ".mp4"
case "video/webm":
return ".webm"
case "video/quicktime":
return ".mov"
case "video/x-matroska":
return ".mkv"
case "image/gif":
return ".gif"
}
return ".mp4"
}
2 changes: 1 addition & 1 deletion core/http/endpoints/openai/chat.go
Original file line number Diff line number Diff line change
Expand Up @@ -743,7 +743,7 @@ func ChatEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, evaluator
// Detect if thinking token is already in prompt or template
var template string
if config.TemplateConfig.UseTokenizerTemplate {
template = config.GetModelTemplate() // TODO: this should be the parsed jinja template. But for now this is the best we can do.
template = config.GetModelTemplate() // Uses raw template text; parsed jinja would be a future improvement
} else {
template = predInput
}
Expand Down
Loading