package web import ( "fmt" "html/template" "net/http" "net/url" "strings" "time" "gitea.mynodes.xyz/ilker/llmgw/internal/gguf" "gitea.mynodes.xyz/ilker/llmgw/internal/hf" ) // The explorer's views: what the registry is excited about this week, what // arrived this month, and what everybody settled on. const ( viewTrending = "trending" viewNew = "new" viewDownloads = "downloads" // viewAdapters lists LoRA adapters, all of them or those made for one // model. viewAdapters = "adapters" ) // newWithin is how recent a repository has to be to count as new. const newWithin = 30 * 24 * time.Hour // minParams is the smallest thing worth calling a model. const minParams = 50e6 // exploreShown is how many rows the explorer lists. const exploreShown = 25 var exploreViews = []struct{ key, label string }{ {viewTrending, "Trending"}, {viewNew, "New this month"}, {viewDownloads, "Most downloaded"}, {viewAdapters, "LoRA adapters"}, } var exploreTasks = []struct{ key, label, tag string }{ {"chat", "Chat", "text-generation"}, {"embed", "Embeddings", "feature-extraction"}, {"any", "Any task", ""}, } var exploreSizes = []struct { key, label string max int64 }{ {"3", "≤ 3B", 3e9}, {"9", "≤ 9B", 9e9}, {"35", "≤ 35B", 35e9}, {"any", "Any size", 0}, } // explorer is the registry half of the mirror page when no repository is // open: a listing, its filters, and links that keep them. type explorer struct { View, Task, Size string // Of is the model whose adapters are listed, in the adapters view; empty // lists adapters for any model. Of string // Here keeps to what the nodes' llama.cpp can load. On by default: a // trending list is mostly things this cluster cannot run, and finding // that out one repository at a time is what the explorer is for avoiding. Here bool Rows []exploreRow // Hidden is how many rows Here took out, so an empty page says why. Hidden int Views, Tasks, Sizes []exploreLink } type exploreLink struct { Label string Href template.URL Active bool } type exploreRow struct { hf.Model Verdict gguf.Verdict Href template.URL New bool } // ParamsLabel is the parameter count as people say it: 7.6B, 494M. func (r exploreRow) ParamsLabel() string { n := r.Params() switch { case n <= 0: return "—" case n >= 1e9: return trimZero(fmt.Sprintf("%.1f", float64(n)/1e9)) + "B" default: return fmt.Sprintf("%dM", n/1e6) } } // ContextLabel is the context length in thousands of tokens. func (r exploreRow) ContextLabel() string { if r.GGUF == nil || r.GGUF.ContextLength <= 0 { return "—" } return fmt.Sprintf("%dk", r.GGUF.ContextLength/1024) } // Age is how long ago the repository last changed, coarsely. func (r exploreRow) Age() string { t := r.LastModified if t.IsZero() { t = r.CreatedAt } if t.IsZero() { return "—" } d := time.Since(t) switch { case d < time.Hour: return "just now" case d < 48*time.Hour: return fmt.Sprintf("%dh", int(d.Hours())) case d < 14*24*time.Hour: return fmt.Sprintf("%dd", int(d.Hours()/24)) case d < 60*24*time.Hour: return fmt.Sprintf("%dw", int(d.Hours()/24/7)) default: return fmt.Sprintf("%dmo", int(d.Hours()/24/30)) } } func trimZero(s string) string { return strings.TrimSuffix(s, ".0") } // exploreState reads the explorer's settings from the address. With none set // — the page opened from the menu — it is trending chat models this cluster // can run. func exploreState(q url.Values) explorer { e := explorer{View: q.Get("view"), Task: q.Get("task"), Size: q.Get("size"), Of: strings.Trim(strings.TrimSpace(q.Get("of")), "/")} // "x" marks a submitted form, so an unticked box reads as off rather than // as not yet chosen. e.Here = q.Get("x") != "1" || q.Get("here") == "1" if !oneOf(e.View, viewTrending, viewNew, viewDownloads, viewAdapters) { e.View = viewTrending // A search with no view asks for what a name usually means, which is // the one everybody downloads. if strings.TrimSpace(q.Get("q")) != "" { e.View = viewDownloads } } if !oneOf(e.Task, "chat", "embed", "any") { e.Task = "chat" } if !oneOf(e.Size, "3", "9", "35", "any") { e.Size = "any" } return e } func oneOf(s string, options ...string) bool { for _, o := range options { if s == o { return true } } return false } // href is the page with these settings and one of them changed. func (e explorer) href(query, key, value string) template.URL { v := url.Values{"tab": {tabExplore}, "view": {e.View}, "task": {e.Task}, "size": {e.Size}, "x": {"1"}} if e.Here { v.Set("here", "1") } if query != "" { v.Set("q", query) } if key != "" { v.Set(key, value) } // Whose adapters, kept while it is adapters being looked at. if e.Of != "" && v.Get("view") == viewAdapters { v.Set("of", e.Of) } return template.URL("/admin/mirror?" + v.Encode()) } // hfQuery is what to ask the registry for. func (e explorer) hfQuery(search string) hf.Query { q := hf.Query{Search: search, Sort: hf.SortTrending, Limit: 60, // What runs here is GGUF; the architecture check needs the registry's // reading of one. GGUF: e.Here} switch e.View { case viewDownloads: q.Sort = hf.SortDownloads case viewNew: // Trending, then kept to the recent: sorted by creation date alone, // the newest repositories are mostly empty uploads nobody has used. q.Limit = 100 case viewAdapters: // Adapters rarely carry a task or a parameter count, so neither is // asked for; what makes one useful is the model it was made for. q.Sort = hf.SortDownloads if e.Of != "" { q.Filters = []string{"base_model:adapter:" + e.Of} } else { q.Filters = []string{"lora"} } return q } for _, t := range exploreTasks { if t.key == e.Task { q.Task = t.tag } } for _, s := range exploreSizes { if s.key == e.Size { q.MaxParams = s.max } } return q } // exploreList fills in the listing. func (p *AccountPages) exploreList(r *http.Request, d *adminMirrorData) { e := exploreState(r.URL.Query()) // From a held model's card: its repository, whose adapters are those of // the model it was made from — a GGUF repository is a quantisation, and // adapters are made for the original. if from := strings.TrimSpace(r.URL.Query().Get("from")); e.View == viewAdapters && e.Of == "" && from != "" { e.Of = from if repo, err := p.registry.Repo(r.Context(), from); err == nil && len(repo.BaseModel) > 0 { e.Of = repo.BaseModel[0] } } for _, v := range exploreViews { e.Views = append(e.Views, exploreLink{v.label, e.href(d.Query, "view", v.key), v.key == e.View}) } for _, t := range exploreTasks { e.Tasks = append(e.Tasks, exploreLink{t.label, e.href(d.Query, "task", t.key), t.key == e.Task}) } for _, s := range exploreSizes { e.Sizes = append(e.Sizes, exploreLink{s.label, e.href(d.Query, "size", s.key), s.key == e.Size}) } d.Explore = &e got, err := p.registry.Explore(r.Context(), e.hfQuery(d.Query)) if err != nil { d.RegistryErr = err.Error() return } back := e.href(d.Query, "", "") for _, m := range got { if e.View == viewNew && time.Since(m.CreatedAt) > newWithin { continue } row := exploreRow{Model: m, Verdict: gguf.Verdict{Servable: true}, Href: back + template.URL("&repo="+url.QueryEscape(m.ID)), New: !m.CreatedAt.IsZero() && time.Since(m.CreatedAt) < newWithin} if a := m.Architecture(); a != "" && p.llamaVersion != "" { row.Verdict = gguf.LlamaCpp(p.llamaVersion, a) } // Adapters are small and say little about their architecture in a // listing; whether one fits is read from its file when it is opened. if e.View == viewAdapters { if len(e.Rows) < exploreShown { e.Rows = append(e.Rows, row) } continue } // An adapter or a fragment, not a model: tens of megabytes of LoRA // weights listed among the models they need a model to run on. if e.Here && m.Params() > 0 && m.Params() < minParams { continue } if e.Here && row.Verdict.Checked && !row.Verdict.Servable { e.Hidden++ continue } if len(e.Rows) < exploreShown { e.Rows = append(e.Rows, row) } } }