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., explore.task..
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
// Fit says what hardware Here judged by; empty when there was none.
Fit string
// gpus is whether the cluster has cards, and so runs safetensors too.
gpus bool
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
// CPUSpeed is about how many tokens a second one reply would get on the
// CPU of CPUNode, the fastest node that holds it; empty when unknown.
CPUSpeed, CPUNode string
// GPUSpeed is the same on the cards, laid out as GPULayout says.
GPUSpeed, GPULayout 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,
// Without cards what runs here is GGUF only. With them safetensors
// too, and a page asks for more, since many are then left out for
// their size.
GGUF: e.Here && !e.gpus}
if e.Here && e.gpus {
q.Limit = 100
}
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})
}
fit := p.fitOf()
if e.Here && fit != nil {
e.gpus = fit.gpus()
e.Fit = fit.note(l)
}
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
}
// Too big for every node here, or in a form these cards lack.
if e.Here && fit != nil && !fit.runs(m) {
e.Hidden++
continue
}
if fit != nil {
row.CPUSpeed, row.CPUNode = fit.rowSpeed(m)
row.GPUSpeed, row.GPULayout = fit.rowGPU(l, m)
}
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")
}