mirror of
https://github.com/mudler/LocalAI.git
synced 2026-11-03 19:14:56 -05:00
* ci(gallery): have the checksum bot write the model library The checksum bot edited gallery/index.yaml, which is a generated copy of gallery/models/index.yaml. The model library check fails any PR where the copy differs from the generated one, and an edit made to the copy never reaches the source. Point the checker at the library, read the files under .model.files as the library keeps them, and run modelcatalog after the checker so the PR carries both files. Assisted-by: Claude:claude-sonnet-5-5 [Claude Code] Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * ci(gallery): have the other index bots write the model library The gallery agent and the variant proposer still read and wrote the generated gallery copy, so their pull requests failed the model library check and their changes never reached the source. The gallery agent now appends library entries to gallery/models/index.yaml, converting its entries with the gallery package and skipping ids the library already has. Its duplicate check reads the same file. The variant proposer decodes the library shape (the body under model:) and splices variants into the model body through a library layout in galleryedit. The anchor refusal goes: the library has no anchors. Its tests use library entries. Both workflows run modelcatalog after the bot and before the pull request step, and use the Go version from go.mod. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> --------- Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
186 lines
6.0 KiB
Go
186 lines
6.0 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/mudler/LocalAI/core/gallery/importers"
|
|
"sigs.k8s.io/yaml"
|
|
)
|
|
|
|
func formatTextContent(text string) string {
|
|
return formatTextContentWithIndent(text, 4, 6)
|
|
}
|
|
|
|
// formatTextContentWithIndent formats text content with specified base and list item indentation
|
|
func formatTextContentWithIndent(text string, baseIndent int, listItemIndent int) string {
|
|
var formattedLines []string
|
|
lines := strings.Split(text, "\n")
|
|
for _, line := range lines {
|
|
trimmed := strings.TrimRight(line, " \t\r")
|
|
if trimmed == "" {
|
|
// Keep empty lines as empty (no indentation)
|
|
formattedLines = append(formattedLines, "")
|
|
} else {
|
|
// Preserve relative indentation from yaml.Marshal output
|
|
// Count existing leading spaces to preserve relative structure
|
|
leadingSpaces := len(trimmed) - len(strings.TrimLeft(trimmed, " \t"))
|
|
trimmedStripped := strings.TrimLeft(trimmed, " \t")
|
|
|
|
var totalIndent int
|
|
if strings.HasPrefix(trimmedStripped, "-") {
|
|
// List items: use listItemIndent (ignore existing leading spaces)
|
|
totalIndent = listItemIndent
|
|
} else {
|
|
// Regular lines: use baseIndent + preserve relative indentation
|
|
// This handles both top-level keys (leadingSpaces=0) and nested properties (leadingSpaces>0)
|
|
totalIndent = baseIndent + leadingSpaces
|
|
}
|
|
|
|
indentStr := strings.Repeat(" ", totalIndent)
|
|
formattedLines = append(formattedLines, indentStr+trimmedStripped)
|
|
}
|
|
}
|
|
formattedText := strings.Join(formattedLines, "\n")
|
|
// Remove any trailing spaces from the formatted description
|
|
formattedText = strings.TrimRight(formattedText, " \t")
|
|
return formattedText
|
|
}
|
|
|
|
// generateYAMLEntry generates a YAML entry for a model using the specified anchor
|
|
func generateYAMLEntry(model ProcessedModel, quantization string) string {
|
|
modelConfig, err := importers.DiscoverModelConfig("https://huggingface.co/"+model.ModelID, json.RawMessage(`{ "quantization": "`+quantization+`"}`))
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
// Extract model name from ModelID
|
|
parts := strings.Split(model.ModelID, "/")
|
|
modelName := model.ModelID
|
|
if len(parts) > 0 {
|
|
modelName = strings.ToLower(parts[len(parts)-1])
|
|
}
|
|
// Remove common suffixes
|
|
modelName = strings.ReplaceAll(modelName, "-gguf", "")
|
|
modelName = strings.ReplaceAll(modelName, "-q4_k_m", "")
|
|
modelName = strings.ReplaceAll(modelName, "-q4_k_s", "")
|
|
modelName = strings.ReplaceAll(modelName, "-q3_k_m", "")
|
|
modelName = strings.ReplaceAll(modelName, "-q2_k", "")
|
|
|
|
description := model.ReadmeContent
|
|
if description == "" {
|
|
description = fmt.Sprintf("AI model: %s", modelName)
|
|
}
|
|
|
|
// Clean up description to prevent YAML linting issues
|
|
description = cleanTextContent(description)
|
|
formattedDescription := formatTextContent(description)
|
|
|
|
// Strip name and description from config file since they are
|
|
// already present at the gallery entry level and should not
|
|
// appear under overrides.
|
|
configFileContent := modelConfig.ConfigFile
|
|
var cfgMap map[string]any
|
|
if err := yaml.Unmarshal([]byte(configFileContent), &cfgMap); err == nil {
|
|
delete(cfgMap, "name")
|
|
delete(cfgMap, "description")
|
|
if cleaned, err := yaml.Marshal(cfgMap); err == nil {
|
|
configFileContent = string(cleaned)
|
|
}
|
|
}
|
|
|
|
configFile := formatTextContent(configFileContent)
|
|
|
|
filesYAML, _ := yaml.Marshal(modelConfig.Files)
|
|
|
|
// Files section: list items need 4 spaces (not 6), since files: is at 2 spaces
|
|
files := formatTextContentWithIndent(string(filesYAML), 4, 4)
|
|
|
|
// Build metadata sections
|
|
var metadataSections []string
|
|
|
|
// Add license if present
|
|
if model.License != "" {
|
|
metadataSections = append(metadataSections, fmt.Sprintf(` license: "%s"`, model.License))
|
|
}
|
|
|
|
// Add tags if present
|
|
if len(model.Tags) > 0 {
|
|
tagsYAML, _ := yaml.Marshal(model.Tags)
|
|
tagsFormatted := formatTextContentWithIndent(string(tagsYAML), 4, 4)
|
|
tagsFormatted = strings.TrimRight(tagsFormatted, "\n")
|
|
metadataSections = append(metadataSections, fmt.Sprintf(" tags:\n%s", tagsFormatted))
|
|
}
|
|
|
|
// Add icon if present
|
|
if model.Icon != "" {
|
|
metadataSections = append(metadataSections, fmt.Sprintf(` icon: %s`, model.Icon))
|
|
}
|
|
|
|
// Build the metadata block
|
|
metadataBlock := ""
|
|
if len(metadataSections) > 0 {
|
|
metadataBlock = strings.Join(metadataSections, "\n") + "\n"
|
|
}
|
|
|
|
yamlTemplate := ""
|
|
yamlTemplate = `- name: "%s"
|
|
url: "github:mudler/LocalAI/gallery/virtual.yaml@master"
|
|
urls:
|
|
- https://huggingface.co/%s
|
|
description: |
|
|
%s%s
|
|
overrides:
|
|
%s
|
|
files:
|
|
%s`
|
|
// Trim trailing newlines from formatted sections to prevent extra blank lines
|
|
formattedDescription = strings.TrimRight(formattedDescription, "\n")
|
|
configFile = strings.TrimRight(configFile, "\n")
|
|
files = strings.TrimRight(files, "\n")
|
|
// Add newline before metadata block if present
|
|
if metadataBlock != "" {
|
|
metadataBlock = "\n" + strings.TrimRight(metadataBlock, "\n")
|
|
}
|
|
return fmt.Sprintf(yamlTemplate,
|
|
modelName,
|
|
model.ModelID,
|
|
formattedDescription,
|
|
metadataBlock,
|
|
configFile,
|
|
files,
|
|
)
|
|
}
|
|
|
|
// generateYAMLForModels generates library entries for the selected models and
|
|
// appends them to the model library, gallery/models/index.yaml.
|
|
//
|
|
// The entry text is still built in the gallery shape, because that is what the
|
|
// importer output maps onto. appendLibraryEntries converts each entry to the
|
|
// library shape before anything is written.
|
|
func generateYAMLForModels(ctx context.Context, models []ProcessedModel, quantization string) error {
|
|
var yamlEntries []string
|
|
for _, model := range models {
|
|
fmt.Printf("Generating YAML entry for model: %s\n", model.ModelID)
|
|
yamlEntries = append(yamlEntries, generateYAMLEntry(model, quantization))
|
|
}
|
|
|
|
if len(yamlEntries) == 0 {
|
|
return nil
|
|
}
|
|
|
|
indexPath := getGalleryIndexPath()
|
|
fmt.Printf("Appending library entries to %s...\n", indexPath)
|
|
added, skipped, err := appendLibraryEntries(indexPath, yamlEntries)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, id := range skipped {
|
|
fmt.Printf("Skipped %s: the library already has an entry with this id\n", id)
|
|
}
|
|
fmt.Printf("Successfully appended %d models to %s\n", len(added), indexPath)
|
|
return nil
|
|
}
|