provider: the Agents page's data reads the settings and each provider's model list once per request (Junjie Zhou on Discord: the Agents page takes long to show) — a look at the agents asked settings.Load for every model of every agent (hundreds of reads of settings.json, about a fifth of /api/state's time in a profile) and catalog.Live for each model's APIs. The settings are now read through filememo (again only once the file changes) and held for the request like the catalog (heldSettings, used by VisibleTo, HiddenModels, ModelAPI, Labels), ListedAPIs holds each provider's live list, and settings.Save drops what is held. With a read-only copy of the owner's configs, a warm /api/state went from about 165 ms to about 100 ms. TestHoldReadsSettingsOnce. Check chain under a temp HOME passes.
This commit is contained in:
1 parent
63f83bf1e1
commit
60b910b758
6 files changed
+53
-8
No files matched your search
@@ -124,7 +124,8 @@ func (p Provider) ListedAPIs(model string) []Protocol {
|
||||
if p.IsPlugin() {
|
||||
return p.pluginAPIs(model)
|
||||
}
|
||||
ms, _, _ := catalog.Live(p.ID)
|
||||
// asked for every model of every agent: read once while a request holds
|
||||
ms := heldOf("live:"+p.ID, func() []catalog.Model { ms, _, _ := catalog.Live(p.ID); return ms })
|
||||
for _, m := range ms {
|
||||
if m.ID == model && len(m.APIs) > 0 {
|
||||
out := make([]Protocol, len(m.APIs))
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/yetone/magpie/internal/catalog"
|
||||
"github.com/yetone/magpie/internal/plugin"
|
||||
"github.com/yetone/magpie/internal/settings"
|
||||
)
|
||||
|
||||
// The catalog is rebuilt from every provider's files on each Resolve (tens
|
||||
@@ -96,3 +97,8 @@ func heldEntries(build func() []Entry) []Entry {
|
||||
func heldPlugins() []plugin.Provider {
|
||||
return heldOf("plugins", func() []plugin.Provider { return slices.Clip(plugin.Cached()) })
|
||||
}
|
||||
|
||||
// heldSettings is the settings, read once while a request holds the
|
||||
// catalog: a look at the agents asks them for every model of every agent.
|
||||
// Only for reading — what it gives is shared by the request's look-ups.
|
||||
func heldSettings() settings.Settings { return heldOf("settings", settings.Load) }
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/yetone/magpie/internal/settings"
|
||||
)
|
||||
|
||||
// A request that only reads builds the catalog once for all its look-ups
|
||||
@@ -40,6 +42,31 @@ func TestHoldBuildsOnce(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// The settings are read once while a request holds the catalog (a look at
|
||||
// the agents read settings.json for every model of every agent), and a
|
||||
// setting saved meanwhile is seen at once.
|
||||
func TestHoldReadsSettingsOnce(t *testing.T) {
|
||||
defer Hold()()
|
||||
if _, ok := VisibleTo("held-agent"); ok {
|
||||
t.Fatal("a visibility before any was set")
|
||||
}
|
||||
// written by something else: the held read stands
|
||||
path := settings.Path()
|
||||
os.MkdirAll(filepath.Dir(path), 0o755)
|
||||
os.WriteFile(path, []byte(`{"visible":{"held-agent":["a"]}}`), 0o600)
|
||||
if _, ok := VisibleTo("held-agent"); ok {
|
||||
t.Fatal("settings.json read again while held")
|
||||
}
|
||||
s := settings.Load()
|
||||
s.Visible = map[string][]string{"held-agent": {"b"}}
|
||||
if err := settings.Save(s); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if names, ok := VisibleTo("held-agent"); !ok || len(names) != 1 || names[0] != "b" {
|
||||
t.Fatalf("a setting saved while held: %v %v", names, ok)
|
||||
}
|
||||
}
|
||||
|
||||
// A provider saved while a request holds the catalog is in it at once.
|
||||
func TestHoldSeesWrites(t *testing.T) {
|
||||
if err := Save(Provider{ID: "held", Name: "Held", Chat: "https://held.example/v1", Key: "k", Models: []string{"a"}}); err != nil {
|
||||
|
||||
@@ -415,7 +415,7 @@ func (p Provider) ModelAPI(model string) (Protocol, bool) {
|
||||
if len(p.Speaks()) < 2 {
|
||||
return "", false
|
||||
}
|
||||
proto := Protocol(settings.Load().ModelAPIs[p.ID+"/"+model])
|
||||
proto := Protocol(heldSettings().ModelAPIs[p.ID+"/"+model])
|
||||
if proto == "" || !slices.Contains(Protocols, proto) || p.Base(proto) == "" {
|
||||
return "", false
|
||||
}
|
||||
@@ -612,7 +612,7 @@ func (e Entry) Label() string {
|
||||
// is that name just as they wrote it, and the vendor's keep Label's.
|
||||
func Labels(es []Entry) []string {
|
||||
out := make([]string, len(es))
|
||||
s := settings.Load()
|
||||
s := heldSettings()
|
||||
plain := s.PlainNames
|
||||
own := !plain && s.PlainOwnNames
|
||||
same := map[string]int{}
|
||||
|
||||
@@ -31,8 +31,8 @@ func (e Entry) Names() []string {
|
||||
|
||||
// VisibleTo is what agent's lists are narrowed to, and whether they are.
|
||||
func VisibleTo(agent string) ([]string, bool) {
|
||||
names, ok := settings.Load().Visible[strings.ToLower(agent)]
|
||||
return names, ok
|
||||
names, ok := heldSettings().Visible[strings.ToLower(agent)]
|
||||
return slices.Clone(names), ok
|
||||
}
|
||||
|
||||
// Shows reports whether a visibility shows e.
|
||||
@@ -91,7 +91,7 @@ func ListedFor(agent string) (listed, kept []Entry) {
|
||||
|
||||
// HiddenModels are the ids of the entries taken out of agent's lists.
|
||||
func HiddenModels(agent string) map[string]bool {
|
||||
ids := settings.Load().HiddenModels[strings.ToLower(agent)]
|
||||
ids := heldSettings().HiddenModels[strings.ToLower(agent)]
|
||||
out := make(map[string]bool, len(ids))
|
||||
for _, id := range ids {
|
||||
out[id] = true
|
||||
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
"github.com/yetone/magpie/internal/appdir"
|
||||
"github.com/yetone/magpie/internal/catalog"
|
||||
"github.com/yetone/magpie/internal/edit"
|
||||
"github.com/yetone/magpie/internal/filememo"
|
||||
"github.com/yetone/magpie/internal/redact"
|
||||
"github.com/yetone/magpie/internal/steady"
|
||||
)
|
||||
@@ -591,7 +592,10 @@ func Load() Settings {
|
||||
fileMu.RLock()
|
||||
defer fileMu.RUnlock()
|
||||
var s Settings
|
||||
if b, err := steady.ReadFile(Path()); err == nil {
|
||||
// read again only once the file changed: a look at the agents asks for
|
||||
// the settings for every model of every agent (hundreds of reads, a
|
||||
// fifth of the Agents page's wait)
|
||||
if b, err := filememo.Read("settings", Path(), func(b []byte) ([]byte, error) { return b, nil }); err == nil {
|
||||
_ = json.Unmarshal(bytes.TrimPrefix(b, []byte("\xef\xbb\xbf")), &s)
|
||||
}
|
||||
return s.normal()
|
||||
@@ -740,7 +744,14 @@ func Save(s Settings) error {
|
||||
if err := f.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return edit.WriteAtomic(Path(), append(b, '\n'))
|
||||
if err := edit.WriteAtomic(Path(), append(b, '\n')); err != nil {
|
||||
return err
|
||||
}
|
||||
// a request holding the catalog holds the settings too: it sees these
|
||||
if catalog.Forget != nil {
|
||||
catalog.Forget()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s Settings) normal() Settings {
|
||||
|
||||
Reference in new issue
Block a user