From 6bede7483d08e295b7f7455494ee22a0aa30251e Mon Sep 17 00:00:00 2001 From: Laura Date: Sun, 28 Sep 2025 00:12:50 +0200 Subject: [PATCH] auto-update model list and show creation date --- chat.go | 4 +-- main.go | 7 +++-- models.go | 71 +++++++++++++++++++++++++++++++++++++------ static/js/chat.js | 2 +- static/js/dropdown.js | 1 - static/js/lib.js | 4 +++ 6 files changed, 73 insertions(+), 16 deletions(-) diff --git a/chat.go b/chat.go index 4b2a8ff..431a550 100644 --- a/chat.go +++ b/chat.go @@ -101,8 +101,8 @@ func (r *Request) Parse() (*openrouter.ChatCompletionRequest, int, error) { toolIndex int ) - model, ok := ModelMap[r.Model] - if !ok { + model := GetModel(r.Model) + if model == nil { return nil, 0, fmt.Errorf("unknown model: %q", r.Model) } diff --git a/main.go b/main.go index 9e14ad9..86c148c 100644 --- a/main.go +++ b/main.go @@ -19,7 +19,7 @@ func main() { icons, err := LoadIcons() log.MustFail(err) - models, err := LoadModels() + err = StartModelUpdateLoop() log.MustFail(err) tokenizer, err := LoadTokenizer(TikTokenSource) @@ -35,6 +35,9 @@ func main() { r.Handle("/*", cache(http.StripPrefix("/", fs))) r.Get("/-/data", func(w http.ResponseWriter, r *http.Request) { + modelMx.RLock() + defer modelMx.RUnlock() + RespondJson(w, http.StatusOK, map[string]any{ "authenticated": IsAuthenticated(r), "config": map[string]any{ @@ -43,7 +46,7 @@ func main() { "motion": env.UI.ReducedMotion, }, "icons": icons, - "models": models, + "models": ModelList, "prompts": Prompts, "version": Version, }) diff --git a/models.go b/models.go index 1a6c016..7082ab3 100644 --- a/models.go +++ b/models.go @@ -4,12 +4,15 @@ import ( "context" "sort" "strings" + "sync" + "time" "github.com/revrost/go-openrouter" ) type Model struct { ID string `json:"id"` + Created int64 `json:"created"` Name string `json:"name"` Description string `json:"description"` Tags []string `json:"tags,omitempty"` @@ -21,23 +24,64 @@ type Model struct { Images bool `json:"-"` } -var ModelMap = make(map[string]*Model) +var ( + modelMx sync.RWMutex -func LoadModels() ([]*Model, error) { - log.Println("Loading models...") + ModelMap map[string]*Model + ModelList []*Model +) + +func GetModel(name string) *Model { + modelMx.RLock() + defer modelMx.RUnlock() + + return ModelMap[name] +} + +func StartModelUpdateLoop() error { + err := LoadModels(true) + if err != nil { + return err + } + + go func() { + ticker := time.NewTicker(2 * time.Hour) + + for range ticker.C { + err := LoadModels(false) + if err != nil { + log.Warnln(err) + } + } + }() + + return nil +} + +func LoadModels(initial bool) error { + log.Println("Refreshing model list...") client := OpenRouterClient() list, err := client.ListUserModels(context.Background()) if err != nil { - return nil, err + return err + } + + if !initial && len(list) == len(ModelList) { + log.Println("No new models, skipping update") + + return nil } sort.Slice(list, func(i, j int) bool { return list[i].Created > list[j].Created }) - models := make([]*Model, len(list)) + var ( + newList = make([]*Model, len(list)) + newMap = make(map[string]*Model, len(list)) + ) for index, model := range list { name := model.Name @@ -48,20 +92,27 @@ func LoadModels() ([]*Model, error) { m := &Model{ ID: model.ID, + Created: model.Created, Name: name, Description: model.Description, } GetModelTags(model, m) - models[index] = m - - ModelMap[model.ID] = m + newList[index] = m + newMap[model.ID] = m } - log.Printf("Loaded %d models\n", len(models)) + log.Printf("Loaded %d models\n", len(newList)) - return models, nil + modelMx.Lock() + + ModelList = newList + ModelMap = newMap + + modelMx.Unlock() + + return nil } func GetModelTags(model openrouter.Model, m *Model) { diff --git a/static/js/chat.js b/static/js/chat.js index 09d40e3..67da5ad 100644 --- a/static/js/chat.js +++ b/static/js/chat.js @@ -1451,7 +1451,7 @@ // render models fillSelect($model, data.models, (el, model) => { el.value = model.id; - el.title = model.description; + el.title = `${model.name} (${formatTimestamp(model.created)})\n---\n${model.description}`; el.textContent = model.name; el.dataset.tags = (model.tags || []).join(","); diff --git a/static/js/dropdown.js b/static/js/dropdown.js index c394e51..a30b60a 100644 --- a/static/js/dropdown.js +++ b/static/js/dropdown.js @@ -97,7 +97,6 @@ // option label const _label = make("div", "label"); - _label.title = option.label; _label.textContent = option.label; _opt.appendChild(_label); diff --git a/static/js/lib.js b/static/js/lib.js index 19344f0..e8eb726 100644 --- a/static/js/lib.js +++ b/static/js/lib.js @@ -56,6 +56,10 @@ function formatMilliseconds(ms) { return `${Math.round(ms / 1000)}s`; } +function formatTimestamp(ts) { + return new Date(ts * 1000).toLocaleDateString(); +} + function dataBlob(dataUrl) { const [header, data] = dataUrl.split(","), mime = header.match(/data:(.*?)(;|$)/)[1];