One long page made every visit wait on Hugging Face for a list it might not show. Each tab now has its own address and does only its own work; only Held reloads while a download runs, and a refused fetch returns to the form it came from. 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{"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)
|
|
}
|
|
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)
|
|
}
|
|
}
|
|
}
|