2024-04-29 17:42:37 +00:00
package config
import (
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"github.com/charmbracelet/glamour"
2024-06-23 08:24:36 +00:00
"github.com/mudler/LocalAI/core/schema"
"github.com/mudler/LocalAI/pkg/downloader"
"github.com/mudler/LocalAI/pkg/utils"
2024-04-29 17:42:37 +00:00
"github.com/rs/zerolog/log"
"gopkg.in/yaml.v3"
)
type BackendConfigLoader struct {
2024-06-08 20:13:02 +00:00
configs map [ string ] BackendConfig
modelPath string
2024-04-29 17:42:37 +00:00
sync . Mutex
}
2024-06-08 20:13:02 +00:00
func NewBackendConfigLoader ( modelPath string ) * BackendConfigLoader {
2024-05-23 20:48:12 +00:00
return & BackendConfigLoader {
2024-06-08 20:13:02 +00:00
configs : make ( map [ string ] BackendConfig ) ,
modelPath : modelPath ,
2024-05-23 20:48:12 +00:00
}
}
2024-04-29 17:42:37 +00:00
type LoadOptions struct {
2024-06-08 20:13:02 +00:00
modelPath string
2024-04-29 17:42:37 +00:00
debug bool
threads , ctxSize int
f16 bool
}
func LoadOptionDebug ( debug bool ) ConfigLoaderOption {
return func ( o * LoadOptions ) {
o . debug = debug
}
}
func LoadOptionThreads ( threads int ) ConfigLoaderOption {
return func ( o * LoadOptions ) {
o . threads = threads
}
}
func LoadOptionContextSize ( ctxSize int ) ConfigLoaderOption {
return func ( o * LoadOptions ) {
o . ctxSize = ctxSize
}
}
2024-06-08 20:13:02 +00:00
func ModelPath ( modelPath string ) ConfigLoaderOption {
return func ( o * LoadOptions ) {
o . modelPath = modelPath
}
}
2024-04-29 17:42:37 +00:00
func LoadOptionF16 ( f16 bool ) ConfigLoaderOption {
return func ( o * LoadOptions ) {
o . f16 = f16
}
}
type ConfigLoaderOption func ( * LoadOptions )
func ( lo * LoadOptions ) Apply ( options ... ConfigLoaderOption ) {
for _ , l := range options {
l ( lo )
}
}
2024-05-23 20:48:12 +00:00
// TODO: either in the next PR or the next commit, I want to merge these down into a single function that looks at the first few characters of the file to determine if we need to deserialize to []BackendConfig or BackendConfig
func readMultipleBackendConfigsFromFile ( file string , opts ... ConfigLoaderOption ) ( [ ] * BackendConfig , error ) {
2024-04-29 17:42:37 +00:00
c := & [ ] * BackendConfig { }
f , err := os . ReadFile ( file )
if err != nil {
return nil , fmt . Errorf ( "cannot read config file: %w" , err )
}
if err := yaml . Unmarshal ( f , c ) ; err != nil {
return nil , fmt . Errorf ( "cannot unmarshal config file: %w" , err )
}
for _ , cc := range * c {
cc . SetDefaults ( opts ... )
}
return * c , nil
}
2024-05-23 20:48:12 +00:00
func readBackendConfigFromFile ( file string , opts ... ConfigLoaderOption ) ( * BackendConfig , error ) {
2024-04-29 17:42:37 +00:00
lo := & LoadOptions { }
lo . Apply ( opts ... )
c := & BackendConfig { }
f , err := os . ReadFile ( file )
if err != nil {
return nil , fmt . Errorf ( "cannot read config file: %w" , err )
}
if err := yaml . Unmarshal ( f , c ) ; err != nil {
return nil , fmt . Errorf ( "cannot unmarshal config file: %w" , err )
}
c . SetDefaults ( opts ... )
return c , nil
}
2024-05-23 20:48:12 +00:00
// Load a config file for a model
func ( bcl * BackendConfigLoader ) LoadBackendConfigFileByName ( modelName , modelPath string , opts ... ConfigLoaderOption ) ( * BackendConfig , error ) {
// Load a config file if present after the model name
cfg := & BackendConfig {
PredictionOptions : schema . PredictionOptions {
Model : modelName ,
} ,
}
cfgExisting , exists := bcl . GetBackendConfig ( modelName )
if exists {
cfg = & cfgExisting
} else {
// Try loading a model config file
modelConfig := filepath . Join ( modelPath , modelName + ".yaml" )
if _ , err := os . Stat ( modelConfig ) ; err == nil {
if err := bcl . LoadBackendConfig (
modelConfig , opts ... ,
) ; err != nil {
return nil , fmt . Errorf ( "failed loading model config (%s) %s" , modelConfig , err . Error ( ) )
}
cfgExisting , exists = bcl . GetBackendConfig ( modelName )
if exists {
cfg = & cfgExisting
}
}
}
2024-11-20 17:06:35 +00:00
cfg . SetDefaults ( append ( opts , ModelPath ( modelPath ) ) ... )
2024-05-23 20:48:12 +00:00
return cfg , nil
}
// This format is currently only used when reading a single file at startup, passed in via ApplicationConfig.ConfigFile
func ( bcl * BackendConfigLoader ) LoadMultipleBackendConfigsSingleFile ( file string , opts ... ConfigLoaderOption ) error {
bcl . Lock ( )
defer bcl . Unlock ( )
c , err := readMultipleBackendConfigsFromFile ( file , opts ... )
2024-04-29 17:42:37 +00:00
if err != nil {
return fmt . Errorf ( "cannot load config file: %w" , err )
}
for _ , cc := range c {
2024-05-21 12:33:47 +00:00
if cc . Validate ( ) {
2024-05-23 20:48:12 +00:00
bcl . configs [ cc . Name ] = * cc
2024-05-21 12:33:47 +00:00
}
2024-04-29 17:42:37 +00:00
}
return nil
}
2024-05-23 20:48:12 +00:00
func ( bcl * BackendConfigLoader ) LoadBackendConfig ( file string , opts ... ConfigLoaderOption ) error {
bcl . Lock ( )
defer bcl . Unlock ( )
c , err := readBackendConfigFromFile ( file , opts ... )
2024-04-29 17:42:37 +00:00
if err != nil {
return fmt . Errorf ( "cannot read config file: %w" , err )
}
2024-05-21 12:33:47 +00:00
if c . Validate ( ) {
2024-05-23 20:48:12 +00:00
bcl . configs [ c . Name ] = * c
2024-05-21 12:33:47 +00:00
} else {
return fmt . Errorf ( "config is not valid" )
}
2024-04-29 17:42:37 +00:00
return nil
}
2024-05-23 20:48:12 +00:00
func ( bcl * BackendConfigLoader ) GetBackendConfig ( m string ) ( BackendConfig , bool ) {
bcl . Lock ( )
defer bcl . Unlock ( )
v , exists := bcl . configs [ m ]
2024-04-29 17:42:37 +00:00
return v , exists
}
2024-05-23 20:48:12 +00:00
func ( bcl * BackendConfigLoader ) GetAllBackendConfigs ( ) [ ] BackendConfig {
bcl . Lock ( )
defer bcl . Unlock ( )
2024-04-29 17:42:37 +00:00
var res [ ] BackendConfig
2024-05-23 20:48:12 +00:00
for _ , v := range bcl . configs {
2024-04-29 17:42:37 +00:00
res = append ( res , v )
}
sort . SliceStable ( res , func ( i , j int ) bool {
return res [ i ] . Name < res [ j ] . Name
} )
return res
}
2024-10-01 18:55:46 +00:00
func ( bcl * BackendConfigLoader ) GetBackendConfigsByFilter ( filter BackendConfigFilterFn ) [ ] BackendConfig {
bcl . Lock ( )
defer bcl . Unlock ( )
var res [ ] BackendConfig
if filter == nil {
filter = NoFilterFn
}
for n , v := range bcl . configs {
if filter ( n , & v ) {
res = append ( res , v )
}
}
// TODO: I don't think this one needs to Sort on name... but we'll see what breaks.
return res
}
2024-05-23 20:48:12 +00:00
func ( bcl * BackendConfigLoader ) RemoveBackendConfig ( m string ) {
bcl . Lock ( )
defer bcl . Unlock ( )
delete ( bcl . configs , m )
2024-04-29 17:42:37 +00:00
}
// Preload prepare models if they are not local but url or huggingface repositories
2024-05-23 20:48:12 +00:00
func ( bcl * BackendConfigLoader ) Preload ( modelPath string ) error {
bcl . Lock ( )
defer bcl . Unlock ( )
2024-04-29 17:42:37 +00:00
status := func ( fileName , current , total string , percent float64 ) {
utils . DisplayDownloadFunction ( fileName , current , total , percent )
}
log . Info ( ) . Msgf ( "Preloading models from %s" , modelPath )
renderMode := "dark"
if os . Getenv ( "COLOR" ) != "" {
renderMode = os . Getenv ( "COLOR" )
}
glamText := func ( t string ) {
out , err := glamour . Render ( t , renderMode )
if err == nil && os . Getenv ( "NO_COLOR" ) == "" {
fmt . Println ( out )
} else {
fmt . Println ( t )
}
}
2024-05-23 20:48:12 +00:00
for i , config := range bcl . configs {
2024-04-29 17:42:37 +00:00
// Download files and verify their SHA
for i , file := range config . DownloadFiles {
log . Debug ( ) . Msgf ( "Checking %q exists and matches SHA" , file . Filename )
if err := utils . VerifyPath ( file . Filename , modelPath ) ; err != nil {
return err
}
// Create file path
filePath := filepath . Join ( modelPath , file . Filename )
2024-08-02 18:06:25 +00:00
if err := file . URI . DownloadFile ( filePath , file . SHA256 , i , len ( config . DownloadFiles ) , status ) ; err != nil {
2024-04-29 17:42:37 +00:00
return err
}
}
// If the model is an URL, expand it, and download the file
if config . IsModelURL ( ) {
modelFileName := config . ModelFileName ( )
2024-08-02 18:06:25 +00:00
uri := downloader . URI ( config . Model )
2024-04-29 17:42:37 +00:00
// check if file exists
if _ , err := os . Stat ( filepath . Join ( modelPath , modelFileName ) ) ; errors . Is ( err , os . ErrNotExist ) {
2024-08-02 18:06:25 +00:00
err := uri . DownloadFile ( filepath . Join ( modelPath , modelFileName ) , "" , 0 , 0 , status )
2024-04-29 17:42:37 +00:00
if err != nil {
return err
}
}
2024-05-23 20:48:12 +00:00
cc := bcl . configs [ i ]
2024-04-29 17:42:37 +00:00
c := & cc
c . PredictionOptions . Model = modelFileName
2024-05-23 20:48:12 +00:00
bcl . configs [ i ] = * c
2024-04-29 17:42:37 +00:00
}
if config . IsMMProjURL ( ) {
modelFileName := config . MMProjFileName ( )
2024-08-02 18:06:25 +00:00
uri := downloader . URI ( config . MMProj )
2024-04-29 17:42:37 +00:00
// check if file exists
if _ , err := os . Stat ( filepath . Join ( modelPath , modelFileName ) ) ; errors . Is ( err , os . ErrNotExist ) {
2024-08-02 18:06:25 +00:00
err := uri . DownloadFile ( filepath . Join ( modelPath , modelFileName ) , "" , 0 , 0 , status )
2024-04-29 17:42:37 +00:00
if err != nil {
return err
}
}
2024-05-23 20:48:12 +00:00
cc := bcl . configs [ i ]
2024-04-29 17:42:37 +00:00
c := & cc
c . MMProj = modelFileName
2024-05-23 20:48:12 +00:00
bcl . configs [ i ] = * c
2024-04-29 17:42:37 +00:00
}
2024-05-23 20:48:12 +00:00
if bcl . configs [ i ] . Name != "" {
glamText ( fmt . Sprintf ( "**Model name**: _%s_" , bcl . configs [ i ] . Name ) )
2024-04-29 17:42:37 +00:00
}
2024-05-23 20:48:12 +00:00
if bcl . configs [ i ] . Description != "" {
2024-04-29 17:42:37 +00:00
//glamText("**Description**")
2024-05-23 20:48:12 +00:00
glamText ( bcl . configs [ i ] . Description )
2024-04-29 17:42:37 +00:00
}
2024-05-23 20:48:12 +00:00
if bcl . configs [ i ] . Usage != "" {
2024-04-29 17:42:37 +00:00
//glamText("**Usage**")
2024-05-23 20:48:12 +00:00
glamText ( bcl . configs [ i ] . Usage )
2024-04-29 17:42:37 +00:00
}
}
return nil
}
// LoadBackendConfigsFromPath reads all the configurations of the models from a path
// (non-recursive)
2024-05-23 20:48:12 +00:00
func ( bcl * BackendConfigLoader ) LoadBackendConfigsFromPath ( path string , opts ... ConfigLoaderOption ) error {
bcl . Lock ( )
defer bcl . Unlock ( )
2024-04-29 17:42:37 +00:00
entries , err := os . ReadDir ( path )
if err != nil {
2024-05-21 12:33:47 +00:00
return fmt . Errorf ( "cannot read directory '%s': %w" , path , err )
2024-04-29 17:42:37 +00:00
}
files := make ( [ ] fs . FileInfo , 0 , len ( entries ) )
for _ , entry := range entries {
info , err := entry . Info ( )
if err != nil {
return err
}
files = append ( files , info )
}
for _ , file := range files {
// Skip templates, YAML and .keep files
if ! strings . Contains ( file . Name ( ) , ".yaml" ) && ! strings . Contains ( file . Name ( ) , ".yml" ) ||
strings . HasPrefix ( file . Name ( ) , "." ) {
continue
}
2024-05-23 20:48:12 +00:00
c , err := readBackendConfigFromFile ( filepath . Join ( path , file . Name ( ) ) , opts ... )
2024-05-21 12:33:47 +00:00
if err != nil {
log . Error ( ) . Err ( err ) . Msgf ( "cannot read config file: %s" , file . Name ( ) )
continue
}
if c . Validate ( ) {
2024-05-23 20:48:12 +00:00
bcl . configs [ c . Name ] = * c
2024-05-21 12:33:47 +00:00
} else {
log . Error ( ) . Err ( err ) . Msgf ( "config is not valid" )
2024-04-29 17:42:37 +00:00
}
}
return nil
}