The mirror page lists Hugging Face's trending models without anything typed, with views for what is new this month and what is most downloaded, filters for task and size, and each repository's parameter count, architecture, context length and age from one registry call. "Only what these nodes can run" is on by default and leaves out architectures the nodes' llama.cpp cannot load, and adapters, saying how many it hid. Listings are remembered for ten minutes. The stylesheet is now linked by a hash of its content. It is cached for a day, so under a fixed name a deploy's new styles reached people a day after its markup. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
242 lines
6.5 KiB
Go
242 lines
6.5 KiB
Go
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"
|
|
)
|
|
|
|
// 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"},
|
|
}
|
|
|
|
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
|
|
// 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")}
|
|
// "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) {
|
|
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{"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)
|
|
}
|
|
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
|
|
}
|
|
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())
|
|
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)
|
|
}
|
|
// 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)
|
|
}
|
|
}
|
|
}
|