Skip to content
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

refactor: ListModels Filtering Upgrade #2773

Merged
merged 67 commits into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
d170bb7
seperate the filtering from the middleware changes
dave-gray101 Jul 11, 2024
9012344
Merge branch 'master' into gw-list-model-filter-upgrade
dave-gray101 Jul 11, 2024
d5d6837
merge with 2772, since ListModels now has an easy way to include loos…
dave-gray101 Jul 11, 2024
688cddc
fix backwards bool, add documentation to explain why
dave-gray101 Jul 11, 2024
d9443cf
fix another backwards bool
dave-gray101 Jul 11, 2024
41c1ecc
fix another backwards bool
dave-gray101 Jul 11, 2024
0ced357
Merge branch 'master' into gw-list-model-filter-upgrade
dave-gray101 Jul 12, 2024
d2699d2
clean
dave-gray101 Jul 12, 2024
380282d
pay attention while coding
dave-gray101 Jul 12, 2024
865aad4
pay attention while coding
dave-gray101 Jul 12, 2024
fd92e81
fix
dave-gray101 Jul 12, 2024
7115941
Merge branch 'master' into gw-list-model-filter-upgrade
dave-gray101 Jul 12, 2024
bbd56bd
fix
dave-gray101 Jul 12, 2024
aace492
Merge branch 'gw-list-model-filter-upgrade' of ghgray101:/dave-gray10…
dave-gray101 Jul 12, 2024
ffe1006
bad merge
dave-gray101 Jul 12, 2024
cffb148
fix
dave-gray101 Jul 12, 2024
845bb83
Merge branch 'master' into gw-list-model-filter-upgrade
dave-gray101 Jul 12, 2024
46b651b
Merge branch 'master' into gw-list-model-filter-upgrade
dave-gray101 Jul 13, 2024
ca462c8
Merge branch 'master' into gw-list-model-filter-upgrade
dave-gray101 Jul 15, 2024
39ab1fd
Merge branch 'master' into gw-list-model-filter-upgrade
dave-gray101 Jul 28, 2024
474a417
fix pointer change
dave-gray101 Jul 28, 2024
7d731f5
Merge branch 'master' into gw-list-model-filter-upgrade
dave-gray101 Jul 29, 2024
794c359
fix merge error in a test
dave-gray101 Jul 29, 2024
b23f0c0
Merge branch 'master' into gw-list-model-filter-upgrade
dave-gray101 Jul 30, 2024
1d279ff
Merge branch 'master' into gw-list-model-filter-upgrade
dave-gray101 Jul 30, 2024
c6ca55f
Merge branch 'master' into gw-list-model-filter-upgrade
dave-gray101 Aug 1, 2024
5c1b603
ALWAYS_INCLUDE ==> SKIP_IF_CONFIGURED see #3107 and #3011
dave-gray101 Aug 1, 2024
f3e9759
automerge
dave-gray101 Aug 2, 2024
579ee54
Merge branch 'master' into gw-list-model-filter-upgrade
dave-gray101 Aug 6, 2024
a56dda7
Merge branch 'master' into gw-list-model-filter-upgrade
dave-gray101 Aug 9, 2024
6182737
Merge branch 'master' into gw-list-model-filter-upgrade
dave-gray101 Aug 9, 2024
a9e97ae
Merge branch 'master' into gw-list-model-filter-upgrade
dave-gray101 Aug 13, 2024
569f03b
Merge branch 'master' into gw-list-model-filter-upgrade
dave-gray101 Aug 14, 2024
6357b41
Merge branch 'master' into gw-list-model-filter-upgrade
dave-gray101 Aug 15, 2024
a0f7888
Merge branch 'master' into gw-list-model-filter-upgrade
dave-gray101 Aug 19, 2024
f48f62f
Merge branch 'master' into gw-list-model-filter-upgrade
dave-gray101 Aug 20, 2024
0ad9b72
Merge branch 'master' into gw-list-model-filter-upgrade
dave-gray101 Aug 21, 2024
bec39f2
Merge branch 'master' into gw-list-model-filter-upgrade
dave-gray101 Aug 24, 2024
a16790a
update for SoundGeneration
dave-gray101 Aug 24, 2024
7cb71c7
missed file
dave-gray101 Aug 24, 2024
f2c4fb4
Merge branch 'master' into gw-list-model-filter-upgrade
dave-gray101 Aug 25, 2024
66f7826
re-add fix lost in DCO force push
dave-gray101 Aug 25, 2024
6c8c00f
Merge branch 'master' into gw-list-model-filter-upgrade
dave-gray101 Aug 31, 2024
5ad8943
Merge branch 'master' into gw-list-model-filter-upgrade
dave-gray101 Sep 6, 2024
9cb2d0c
add cli command
dave-gray101 Sep 6, 2024
2e8c010
manual merge
dave-gray101 Sep 17, 2024
5c22b95
Merge branch 'master' into gw-list-model-filter-upgrade
dave-gray101 Sep 23, 2024
8aa637f
Merge branch 'master' into gw-list-model-filter-upgrade
dave-gray101 Sep 24, 2024
595bc8f
Merge branch 'master' into gw-list-model-filter-upgrade
dave-gray101 Sep 26, 2024
67d6bf2
experimental workflow fix to unbreak
dave-gray101 Sep 26, 2024
d68dcca
revert
dave-gray101 Sep 26, 2024
320ab30
Merge branch 'master' into gw-list-model-filter-upgrade
dave-gray101 Sep 26, 2024
f9dd9d2
Merge branch 'master' into gw-list-model-filter-upgrade
dave-gray101 Sep 27, 2024
1b50daa
Merge branch 'master' into gw-list-model-filter-upgrade
dave-gray101 Sep 27, 2024
0e77ac2
allow manually adding usecases
dave-gray101 Sep 27, 2024
898b6be
Merge branch 'master' into gw-list-model-filter-upgrade
dave-gray101 Sep 27, 2024
258c9bd
fix
dave-gray101 Sep 27, 2024
204d6a1
fix
dave-gray101 Sep 28, 2024
0b43fad
fix
dave-gray101 Sep 28, 2024
64d2f63
Merge branch 'master' into gw-list-model-filter-upgrade
dave-gray101 Sep 28, 2024
f68d54e
test change
dave-gray101 Sep 29, 2024
e67fcce
Merge branch 'gw-list-model-filter-upgrade' of ghgray101:/dave-gray10…
dave-gray101 Sep 29, 2024
7589128
test fix
dave-gray101 Sep 29, 2024
45c809c
oops
dave-gray101 Sep 29, 2024
d4224e3
oops
dave-gray101 Sep 29, 2024
d750dc6
Merge branch 'master' into gw-list-model-filter-upgrade
mudler Oct 1, 2024
68a1006
Merge branch 'master' into gw-list-model-filter-upgrade
dave-gray101 Oct 1, 2024
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
70 changes: 70 additions & 0 deletions core/config/backend_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package config
import (
"os"
"regexp"
"slices"
"strings"

"github.com/mudler/LocalAI/core/schema"
Expand Down Expand Up @@ -406,3 +407,72 @@ func (c *BackendConfig) Validate() bool {
func (c *BackendConfig) HasTemplate() bool {
return c.TemplateConfig.Completion != "" || c.TemplateConfig.Edit != "" || c.TemplateConfig.Chat != "" || c.TemplateConfig.ChatMessage != ""
}

type BackendConfigUsecases int

const (
FLAG_ANY BackendConfigUsecases = 0b00000000
FLAG_CHAT BackendConfigUsecases = 0b00000001
FLAG_COMPLETION BackendConfigUsecases = 0b00000010
FLAG_EDIT BackendConfigUsecases = 0b00000100
FLAG_EMBEDDINGS BackendConfigUsecases = 0b00001000
FLAG_RERANK BackendConfigUsecases = 0b00010000
FLAG_IMAGE BackendConfigUsecases = 0b00100000
FLAG_TRANSCRIPT BackendConfigUsecases = 0b01000000
FLAG_TTS BackendConfigUsecases = 0b10000000

// Common Subsets
FLAG_LLM BackendConfigUsecases = FLAG_CHAT & FLAG_COMPLETION & FLAG_EDIT
)

func (c *BackendConfig) HasUsecases(u BackendConfigUsecases) bool {
Copy link
Owner

@mudler mudler Sep 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My two cents on this - I would expect a "category" field on the backend config at this point. Hooking into the backend name can be brittle, mainly because a user might expand backends with --external-backends. Maybe we can mix (detection) and explicit configuration via backend config?

Copy link
Collaborator Author

@dave-gray101 dave-gray101 Sep 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I mentioned in that comment, I'm significantly concerned with the maintenance burden of our yaml config backlog [to either initially create it, or update if new backends are created].

I see a few paths forward:

  • We could ignore heuristics if category is set - this is the simplest and sanest option, but it's got possible future tech debt in the form of "we add a new usecase" or any other major breaking change. Existing yaml configs would run through the heuristic, but new ones could be created to bypass it.
  • We could just use both. If a model has a category field, we also run it through heuristics and use the union. At worst, we show a model in a place it's not super useful, which is what we currently do everywhere. This is my current plan
    • PR updated to demonstrate this
  • A more dramatic solution would be to adapt this heuristic function to a "one off" script that we could run against existing gallery files. If we added the category field to everything that exists today, we could mandate that it's a requirement going forward, and eliminate the need for runtime scanning. This does run the risk of "new usecase tech debt", but it seems saner to require an "update the gallery" script?

if (u & FLAG_CHAT) == FLAG_CHAT {
if c.TemplateConfig.Chat == "" && c.TemplateConfig.ChatMessage == "" {
return false
}
}
if (u & FLAG_COMPLETION) == FLAG_COMPLETION {
if c.TemplateConfig.Completion == "" {
return false
}
}
if (u & FLAG_EDIT) == FLAG_EDIT {
if c.TemplateConfig.Edit == "" {
return false
}
}
if (u & FLAG_EMBEDDINGS) == FLAG_EMBEDDINGS {
if c.Embeddings == nil || !*c.Embeddings {
return false
}
}
if (u & FLAG_IMAGE) == FLAG_IMAGE {
imageBackends := []string{"diffusers", "tinydream", "stablediffusion"}
if !slices.Contains(imageBackends, c.Backend) {
return false
}

if c.Backend == "diffusers" && c.Diffusers.PipelineType == "" {
return false
}

}
if (u & FLAG_RERANK) == FLAG_RERANK {
if c.Backend != "rerankers" {
return false
}
}
if (u & FLAG_TRANSCRIPT) == FLAG_TRANSCRIPT {
if c.Backend != "whisper" {
return false
}
}
if (u & FLAG_TTS) == FLAG_TTS {
ttsBackends := []string{"piper", "transformer-musicgen", "parler-tts"}
if !slices.Contains(ttsBackends, c.Backend) {
return false
}
}

return true
}
35 changes: 35 additions & 0 deletions core/config/backend_config_filter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package config

import "regexp"

type BackendConfigFilterFn func(string, *BackendConfig) bool

func NoFilterFn(_ string, _ *BackendConfig) bool { return true }

func BuildNameFilterFn(filter string) (BackendConfigFilterFn, error) {
if filter == "" {
return NoFilterFn, nil
}
rxp, err := regexp.Compile(filter)
if err != nil {
return nil, err
}
return func(name string, config *BackendConfig) bool {
if config != nil {
return rxp.MatchString(config.Name)
}
return rxp.MatchString(name)
}, nil
}

func BuildUsecaseFilterFn(usecases BackendConfigUsecases) BackendConfigFilterFn {
if usecases == FLAG_ANY {
return NoFilterFn
}
return func(name string, config *BackendConfig) bool {
if config == nil {
return false // TODO: Potentially make this a param, for now, no known usecase to include
}
return config.HasUsecases(usecases)
}
}
20 changes: 20 additions & 0 deletions core/config/backend_config_loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,26 @@ func (bcl *BackendConfigLoader) GetAllBackendConfigs() []BackendConfig {
return res
}

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
}

func (bcl *BackendConfigLoader) RemoveBackendConfig(m string) {
bcl.Lock()
defer bcl.Unlock()
Expand Down
73 changes: 73 additions & 0 deletions core/config/backend_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,77 @@ parameters:
Expect(config.Validate()).To(BeTrue())
})
})
It("Properly handles backend usecase matching", func() {

a := BackendConfig{
Name: "a",
}
Expect(a.HasUsecases(FLAG_ANY)).To(BeTrue()) // FLAG_ANY just means the config _exists_ essentially.

b := BackendConfig{
Name: "b",
Backend: "stablediffusion",
}
Expect(b.HasUsecases(FLAG_ANY)).To(BeTrue())
Expect(b.HasUsecases(FLAG_IMAGE)).To(BeTrue())
Expect(b.HasUsecases(FLAG_CHAT)).To(BeFalse())

c := BackendConfig{
Name: "c",
Backend: "llama-cpp",
TemplateConfig: TemplateConfig{
Chat: "chat",
},
}
Expect(c.HasUsecases(FLAG_ANY)).To(BeTrue())
Expect(c.HasUsecases(FLAG_IMAGE)).To(BeFalse())
Expect(c.HasUsecases(FLAG_COMPLETION)).To(BeFalse())
Expect(c.HasUsecases(FLAG_CHAT)).To(BeTrue())

d := BackendConfig{
Name: "d",
Backend: "llama-cpp",
TemplateConfig: TemplateConfig{
Chat: "chat",
Completion: "completion",
},
}
Expect(d.HasUsecases(FLAG_ANY)).To(BeTrue())
Expect(d.HasUsecases(FLAG_IMAGE)).To(BeFalse())
Expect(d.HasUsecases(FLAG_COMPLETION)).To(BeTrue())
Expect(d.HasUsecases(FLAG_CHAT)).To(BeTrue())

trueValue := true
e := BackendConfig{
Name: "e",
Backend: "llama-cpp",
TemplateConfig: TemplateConfig{
Completion: "completion",
},
Embeddings: &trueValue,
}

Expect(e.HasUsecases(FLAG_ANY)).To(BeTrue())
Expect(e.HasUsecases(FLAG_IMAGE)).To(BeFalse())
Expect(e.HasUsecases(FLAG_COMPLETION)).To(BeTrue())
Expect(e.HasUsecases(FLAG_CHAT)).To(BeFalse())
Expect(e.HasUsecases(FLAG_EMBEDDINGS)).To(BeTrue())

f := BackendConfig{
Name: "f",
Backend: "piper",
}
Expect(f.HasUsecases(FLAG_ANY)).To(BeTrue())
Expect(f.HasUsecases(FLAG_TTS)).To(BeTrue())
Expect(f.HasUsecases(FLAG_CHAT)).To(BeFalse())

g := BackendConfig{
Name: "g",
Backend: "whisper",
}
Expect(g.HasUsecases(FLAG_ANY)).To(BeTrue())
Expect(g.HasUsecases(FLAG_TRANSCRIPT)).To(BeTrue())
Expect(g.HasUsecases(FLAG_TTS)).To(BeFalse())

})
})
4 changes: 2 additions & 2 deletions core/http/ctx/fiber.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ func ModelFromContext(ctx *fiber.Ctx, cl *config.BackendConfigLoader, loader *mo
}

// Set model from bearer token, if available
bearer := strings.TrimLeft(ctx.Get("authorization"), "Bearer ")
bearer := strings.TrimLeft(ctx.Get("authorization"), "Bear ") // Reduced duplicate characters of Bearer
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this breaking?

Copy link
Collaborator Author

@dave-gray101 dave-gray101 Oct 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting enough it is not. The static analysis tooling we've been experimenting with pointed this out to me and I verified it in the golang source: https://github.com/golang/go/blob/master/src/strings/strings.go

TrimLeft (and its many friends) iterate through the string a single time, checking every character. If that character is contained in the "cutset" string even once, it will be removed. No need for duplicated characters - they will be scanned M*N every time its called, without benefit. This example is B e a r er

bearerExists := bearer != "" && loader.ExistsInModelPath(bearer)

// If no model was specified, take the first available
if modelInput == "" && !bearerExists && firstModel {
models, _ := services.ListModels(cl, loader, "", true)
models, _ := services.ListModels(cl, loader, config.NoFilterFn, services.SKIP_IF_CONFIGURED)
if len(models) > 0 {
modelInput = models[0]
log.Debug().Msgf("No model specified, using: %s", modelInput)
Expand Down
12 changes: 2 additions & 10 deletions core/http/endpoints/localai/welcome.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
func WelcomeEndpoint(appConfig *config.ApplicationConfig,
cl *config.BackendConfigLoader, ml *model.ModelLoader, modelStatus func() (map[string]string, map[string]string)) func(*fiber.Ctx) error {
return func(c *fiber.Ctx) error {
models, _ := services.ListModels(cl, ml, "", true)
models, _ := services.ListModels(cl, ml, config.NoFilterFn, services.ALWAYS_INCLUDE)
dave-gray101 marked this conversation as resolved.
Show resolved Hide resolved
backendConfigs := cl.GetAllBackendConfigs()

galleryConfigs := map[string]*gallery.Config{}
Expand All @@ -29,18 +29,10 @@ func WelcomeEndpoint(appConfig *config.ApplicationConfig,
// Get model statuses to display in the UI the operation in progress
processingModels, taskTypes := modelStatus()

modelsWithoutConfig := []string{}

for _, m := range models {
if _, ok := galleryConfigs[m]; !ok {
modelsWithoutConfig = append(modelsWithoutConfig, m)
}
}

summary := fiber.Map{
"Title": "LocalAI API - " + internal.PrintableVersion(),
"Version": internal.PrintableVersion(),
"Models": modelsWithoutConfig,
"Models": models,
"ModelsConfig": backendConfigs,
"GalleryConfig": galleryConfigs,
"IsP2PEnabled": p2p.IsP2PEnabled(),
Expand Down
2 changes: 1 addition & 1 deletion core/http/endpoints/openai/assistant.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ func filterAssistantsAfterID(assistants []Assistant, id string) []Assistant {

func modelExists(cl *config.BackendConfigLoader, ml *model.ModelLoader, modelName string) (found bool) {
found = false
models, err := services.ListModels(cl, ml, "", true)
models, err := services.ListModels(cl, ml, config.NoFilterFn, services.SKIP_IF_CONFIGURED)
if err != nil {
return
}
Expand Down
38 changes: 19 additions & 19 deletions core/http/endpoints/openai/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,32 +18,32 @@ func ListModelsEndpoint(bcl *config.BackendConfigLoader, ml *model.ModelLoader)
filter := c.Query("filter")

// By default, exclude any loose files that are already referenced by a configuration file.
excludeConfigured := c.QueryBool("excludeConfigured", true)
var policy services.LooseFilePolicy
if c.QueryBool("excludeConfigured", true) {
policy = services.SKIP_IF_CONFIGURED
} else {
policy = services.ALWAYS_INCLUDE // This replicates current behavior. TODO: give more options to the user?
}

filterFn, err := config.BuildNameFilterFn(filter)
if err != nil {
return err
}

dataModels, err := modelList(bcl, ml, filter, excludeConfigured)
modelNames, err := services.ListModels(bcl, ml, filterFn, policy)
if err != nil {
return err
}

// Map from a slice of names to a slice of OpenAIModel response objects
dataModels := []schema.OpenAIModel{}
for _, m := range modelNames {
dataModels = append(dataModels, schema.OpenAIModel{ID: m, Object: "model"})
}

return c.JSON(schema.ModelsDataResponse{
Object: "list",
Data: dataModels,
})
}
}

func modelList(bcl *config.BackendConfigLoader, ml *model.ModelLoader, filter string, excludeConfigured bool) ([]schema.OpenAIModel, error) {

models, err := services.ListModels(bcl, ml, filter, excludeConfigured)
if err != nil {
return nil, err
}

dataModels := []schema.OpenAIModel{}

// Then iterate through the loose files:
for _, m := range models {
dataModels = append(dataModels, schema.OpenAIModel{ID: m, Object: "model"})
}

return dataModels, nil
}
6 changes: 3 additions & 3 deletions core/http/routes/ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ func RegisterUIRoutes(app *fiber.App,

// Show the Chat page
app.Get("/chat/:model", auth, func(c *fiber.Ctx) error {
backendConfigs, _ := services.ListModels(cl, ml, "", true)
backendConfigs, _ := services.ListModels(cl, ml, config.NoFilterFn, services.SKIP_IF_CONFIGURED)

summary := fiber.Map{
"Title": "LocalAI - Chat with " + c.Params("model"),
Expand All @@ -315,7 +315,7 @@ func RegisterUIRoutes(app *fiber.App,
})

app.Get("/talk/", auth, func(c *fiber.Ctx) error {
backendConfigs, _ := services.ListModels(cl, ml, "", true)
backendConfigs, _ := services.ListModels(cl, ml, config.NoFilterFn, services.SKIP_IF_CONFIGURED)

if len(backendConfigs) == 0 {
// If no model is available redirect to the index which suggests how to install models
Expand All @@ -336,7 +336,7 @@ func RegisterUIRoutes(app *fiber.App,

app.Get("/chat/", auth, func(c *fiber.Ctx) error {

backendConfigs, _ := services.ListModels(cl, ml, "", true)
backendConfigs, _ := services.ListModels(cl, ml, config.NoFilterFn, services.SKIP_IF_CONFIGURED)

if len(backendConfigs) == 0 {
// If no model is available redirect to the index which suggests how to install models
Expand Down
Loading