CI / test (push) Successful in 9m0s
A repository with neither GGUF nor safetensors (by the registry's summary or its tags) is hidden under "runs here", which is ticked by default, and counted with the architectures llama.cpp cannot load; unticked it is listed. An open repository's pickle files are not offered, only counted, and a repository of pickles only says to look for another version. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
314 lines
8.9 KiB
Go
314 lines
8.9 KiB
Go
package web
|
|
|
|
import (
|
|
"fmt"
|
|
"html/template"
|
|
"net/http"
|
|
"net/url"
|
|
"slices"
|
|
"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
|
|
|
|
// The labels are catalogue keys: explore.view.<key>, explore.task.<key>.
|
|
var exploreViews = []string{viewTrending, viewNew, viewDownloads, viewAdapters}
|
|
|
|
var exploreTasks = []struct{ key, tag string }{
|
|
{"chat", "text-generation"}, {"embed", "feature-extraction"}, {"any", ""},
|
|
}
|
|
|
|
func init() {
|
|
for _, v := range exploreViews {
|
|
registerKeys("explore.view." + v)
|
|
}
|
|
for _, t := range exploreTasks {
|
|
registerKeys("explore.task." + t.key)
|
|
}
|
|
registerKeys("explore.size.any")
|
|
}
|
|
|
|
var exploreSizes = []struct {
|
|
key, label string
|
|
max int64
|
|
}{
|
|
// "any" is named in words, explore.size.any; the others are figures.
|
|
{"3", "≤ 3B", 3e9}, {"9", "≤ 9B", 9e9}, {"35", "≤ 35B", 35e9}, {"any", "", 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
|
|
// Updated is how long ago the repository changed, in the page's language.
|
|
Updated string
|
|
}
|
|
|
|
// 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(l *Lang) string {
|
|
t := r.LastModified
|
|
if t.IsZero() {
|
|
t = r.CreatedAt
|
|
}
|
|
if t.IsZero() {
|
|
return "—"
|
|
}
|
|
d := time.Since(t)
|
|
switch {
|
|
case d < time.Hour:
|
|
return l.T("explore.age.now")
|
|
case d < 48*time.Hour:
|
|
return l.T("explore.age.h", int(d.Hours()))
|
|
case d < 14*24*time.Hour:
|
|
return l.T("explore.age.d", int(d.Hours()/24))
|
|
case d < 60*24*time.Hour:
|
|
return l.T("explore.age.w", int(d.Hours()/24/7))
|
|
default:
|
|
return l.T("explore.age.mo", 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]
|
|
}
|
|
}
|
|
l := langOf(r)
|
|
for _, v := range exploreViews {
|
|
e.Views = append(e.Views, exploreLink{l.T("explore.view." + v), e.href(d.Query, "view", v), v == e.View})
|
|
}
|
|
for _, t := range exploreTasks {
|
|
e.Tasks = append(e.Tasks, exploreLink{l.T("explore.task." + t.key), e.href(d.Query, "task", t.key), t.key == e.Task})
|
|
}
|
|
for _, s := range exploreSizes {
|
|
label := s.label
|
|
if label == "" {
|
|
label = l.T("explore.size." + s.key)
|
|
}
|
|
e.Sizes = append(e.Sizes, exploreLink{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 = l.Err(err)
|
|
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}
|
|
row.Updated = row.age(l)
|
|
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
|
|
}
|
|
// Nothing in it a node loads: no GGUF, no safetensors — pickles,
|
|
// which nodes refuse, or a format no engine here reads.
|
|
if e.Here && !loadable(m) {
|
|
e.Hidden++
|
|
continue
|
|
}
|
|
if e.Here && row.Verdict.Checked && !row.Verdict.Servable {
|
|
e.Hidden++
|
|
continue
|
|
}
|
|
if len(e.Rows) < exploreShown {
|
|
e.Rows = append(e.Rows, row)
|
|
}
|
|
}
|
|
}
|
|
|
|
// loadable reports whether a listed repository has weights a node loads:
|
|
// GGUF or safetensors, by the registry's summary of them or its tags.
|
|
func loadable(m hf.Model) bool {
|
|
if m.GGUF != nil || m.Safetensors != nil {
|
|
return true
|
|
}
|
|
return slices.Contains(m.Tags, "gguf") || slices.Contains(m.Tags, "safetensors")
|
|
}
|