Ready of wanted, how many are starting, and the nodes, from the fleet data the card already reads, so the list answers it without a click. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
806 lines
27 KiB
Go
806 lines
27 KiB
Go
package web
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"html/template"
|
|
"net/http"
|
|
"slices"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/control/adminapi"
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/control/catalog"
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/gguf"
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/hf"
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/mirror"
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/sourcecheck"
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/store"
|
|
)
|
|
|
|
// MirrorAdmin is the mirror's management API as this page needs it.
|
|
//
|
|
// An interface rather than the client itself so the page can be tested without
|
|
// a mirror, and so a deployment without one leaves it nil and the page
|
|
// unmounted — the same shape the cluster dashboard uses.
|
|
type MirrorAdmin interface {
|
|
List(ctx context.Context) (mirror.ModelsResponse, error)
|
|
Fetch(ctx context.Context, req mirror.FetchRequest) (mirror.FetchResponse, error)
|
|
Remove(ctx context.Context, model, version string) ([]string, error)
|
|
// Addr is the mirror this points at, shown on the page because an operator
|
|
// looking at an empty table needs to know which machine is empty.
|
|
Addr() string
|
|
}
|
|
|
|
// Registry is the model registry the explorer browses. Nil leaves the explorer
|
|
// off, which is right for a deployment with no way out to the internet: a
|
|
// search box that can only time out is worse than no search box.
|
|
type Registry interface {
|
|
Explore(ctx context.Context, q hf.Query) ([]hf.Model, error)
|
|
Repo(ctx context.Context, id string) (hf.Repo, error)
|
|
}
|
|
|
|
// repoSet is one thing worth fetching: a file, or every shard of one.
|
|
type repoSet struct {
|
|
// Verdict is whether the nodes' llama.cpp can load it, from the
|
|
// architecture the registry reports; a set that cannot be served is
|
|
// shown with the reason and no fetch button.
|
|
Verdict gguf.Verdict
|
|
hf.FileSet
|
|
// Sources are every file in the set, in the order the registry listed them.
|
|
// A shard set goes as one action — a fifteen-shard model is not fifteen
|
|
// decisions, it is one.
|
|
Sources []repoSource
|
|
// Model and Version are what the fetch form starts with. Editable, because
|
|
// the name a catalog entry uses is the operator's to choose and the
|
|
// registry's idea of it is only a good guess.
|
|
Model string
|
|
Version string
|
|
}
|
|
|
|
type repoSource struct{ URL, Path string }
|
|
|
|
// modelUse is a catalog model that runs a mirrored version, and how.
|
|
type modelUse struct {
|
|
Name string
|
|
Mode string // always, ondemand or stopped
|
|
Replicas int
|
|
// Min and Max are its scaling as the catalog holds it.
|
|
Min, Max int
|
|
// Ready, Wanted and Nodes are how it runs now, for the row itself:
|
|
// replicas ready of those it should have, and the nodes they are on.
|
|
// Fleet is false where nothing could be asked, so the row says nothing
|
|
// rather than "0 of 0".
|
|
Fleet bool
|
|
Ready, Wanted int
|
|
Starting int
|
|
Nodes []string
|
|
Parked bool
|
|
Unplaceable bool
|
|
// Declared models are read from server.yaml on every start, so a mode
|
|
// changed here would revert; the page says where to change it instead.
|
|
Declared bool
|
|
}
|
|
|
|
// mirrorRow is one model version as the page shows it.
|
|
type mirrorRow struct {
|
|
mirror.Status
|
|
// URL is what a catalog entry should say. The one string on this page
|
|
// somebody will copy, so it is rendered rather than left to be assembled.
|
|
URL string
|
|
Percent int
|
|
// Serve is the offer to put this version into service, open on the row the
|
|
// operator clicked. Nil on a row that is still arriving: a catalog entry
|
|
// pointing at a partial file is the thing versioned paths exist to prevent.
|
|
Serve *serveForm
|
|
// Detail is the card opened beneath the row: what runs it, on which
|
|
// nodes, and how many replicas it runs.
|
|
Detail *heldDetail
|
|
// Open is the row's ?open= value.
|
|
Open string
|
|
|
|
// HasPercent is false when the upstream never sent a length. The bar is
|
|
// then not drawn at all: a progress bar that has to invent its own total is
|
|
// worse than a byte count that only goes up.
|
|
HasPercent bool
|
|
|
|
// Files is how many files a held version has. A version of several is one
|
|
// row — a fifteen-shard model is one thing to serve or remove, and fifteen
|
|
// rows of it pushed everything else off the page.
|
|
Files int
|
|
// UsedBy is the catalog models whose source is this row's address, and how
|
|
// each runs, so a file in use does not look like one waiting to be served
|
|
// and how it runs can be changed where it is seen.
|
|
UsedBy []modelUse
|
|
}
|
|
|
|
type adminMirrorData struct {
|
|
Nav Nav
|
|
// Tab is the part of the page shown: held, explore or fetch. Tabs are the
|
|
// links to each.
|
|
Tab string
|
|
Tabs []mirrorTab
|
|
Addr string
|
|
Base string
|
|
Rows []mirrorRow
|
|
// Groups are the same rows under their model.
|
|
Groups []mirrorGroup
|
|
|
|
Fetching int
|
|
Present int
|
|
Failed int
|
|
|
|
// Unreachable separates "the mirror holds nothing" from "the mirror did not
|
|
// answer". They render almost identically and mean opposite things: one is
|
|
// a mirror waiting to be filled, the other is a machine to go and look at.
|
|
Unreachable bool
|
|
|
|
// HasCatalog is false where the catalogue cannot be changed at runtime, and
|
|
// the serve action is then not offered rather than offered and refused.
|
|
HasCatalog bool
|
|
|
|
// The explorer.
|
|
HasRegistry bool
|
|
Query string
|
|
Explore *explorer
|
|
// Back is the listing an open repository was reached from.
|
|
Back template.URL
|
|
// Inline is a repository rendered beneath its row in the listing, without
|
|
// the page around it.
|
|
Inline bool
|
|
// Card is the open repository's model card, rendered.
|
|
Card template.HTML
|
|
// RepoURL is the open repository on the registry's own site.
|
|
RepoURL string
|
|
Repo *hf.Repo
|
|
RepoSets []repoSet
|
|
RegistryErr string
|
|
|
|
Message string
|
|
Error string
|
|
CSRF string
|
|
Version string
|
|
Now string
|
|
// RefreshSec is 0 when nothing is downloading. The page reloads only while
|
|
// there is something to watch, because a reload while an operator is typing
|
|
// a source URL into the form takes the URL with it.
|
|
RefreshSec int
|
|
}
|
|
|
|
// RegisterAdminMirror mounts the model mirror under /admin/mirror.
|
|
//
|
|
// Fetching and removing, and nothing else. What the mirror holds is operator
|
|
// state rather than account state, but the reasoning is the same as
|
|
// /admin/users: these two are the work somebody does repeatedly and should not
|
|
// need a shell for, while anything that would let a stolen session reach
|
|
// further — pointing the control plane at a different mirror, say — stays in
|
|
// configuration where changing it means access to the host.
|
|
// MirrorDeps is what the mirror page can reach.
|
|
//
|
|
// A struct rather than five parameters: three of them are optional interfaces
|
|
// and two are the same shape, which at a call site is a swap nobody notices
|
|
// until a page shows the wrong thing.
|
|
type MirrorDeps struct {
|
|
Mirror MirrorAdmin
|
|
// Registry browses a model registry. Nil turns the explorer off.
|
|
Registry Registry
|
|
// Catalog puts a mirrored model into service. Nil leaves the serve action
|
|
// off, which is right where the catalogue cannot be changed at runtime.
|
|
Catalog CatalogAdmin
|
|
// Fleet is the cluster, for saying whether anything could run a model
|
|
// before somebody spends an hour downloading it.
|
|
Fleet Source
|
|
}
|
|
|
|
func (p *AccountPages) RegisterAdminMirror(mux *http.ServeMux, d MirrorDeps) {
|
|
m := d.Mirror
|
|
p.hasMirror = true
|
|
p.registry = d.Registry
|
|
p.catalog = d.Catalog
|
|
p.fleet = d.Fleet
|
|
|
|
mux.HandleFunc("GET /admin/mirror", p.adminOnly(
|
|
func(w http.ResponseWriter, r *http.Request, _ store.User) {
|
|
p.renderMirror(w, r, m, "", "")
|
|
}))
|
|
|
|
// One repository as a fragment, for the row beneath it in the listing.
|
|
mux.HandleFunc("GET /admin/mirror/repo", p.adminOnly(
|
|
func(w http.ResponseWriter, r *http.Request, _ store.User) {
|
|
repo := strings.TrimSpace(r.URL.Query().Get("repo"))
|
|
if p.registry == nil || repo == "" {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
d := adminMirrorData{CSRF: csrfFor(r), Inline: true, HasRegistry: true}
|
|
p.openRepo(r, &d, repo)
|
|
if d.RegistryErr != "" {
|
|
http.Error(w, "The registry: "+d.RegistryErr, http.StatusBadGateway)
|
|
return
|
|
}
|
|
render(w, "mirror_repo", d)
|
|
}))
|
|
|
|
mux.HandleFunc("POST /admin/mirror/fetch", p.mirrorAction(m,
|
|
func(ctx context.Context, admin store.User, r *http.Request) (string, error) {
|
|
// One form posts one source or fifteen. A sharded model is one
|
|
// decision, not one per shard, and the mirror fetches each file
|
|
// independently anyway.
|
|
sources, files := r.Form["source"], r.Form["file"]
|
|
if len(sources) == 0 || strings.TrimSpace(sources[0]) == "" {
|
|
return "", errors.New("a source URL is required")
|
|
}
|
|
model := strings.TrimSpace(r.FormValue("model"))
|
|
version := strings.TrimSpace(r.FormValue("version"))
|
|
|
|
// Read before anything is downloaded: the header says whether the
|
|
// nodes can load it, and a file that cannot be served is an hour
|
|
// of download and a replica failing on every node.
|
|
if r.FormValue("force") == "" {
|
|
if v := p.checkSource(ctx, strings.TrimSpace(sources[0])); !v.Servable {
|
|
return "", fmt.Errorf("not fetched: %s. Tick \"fetch anyway\" to take it regardless", v.Reason)
|
|
}
|
|
}
|
|
|
|
var started int
|
|
for i, src := range sources {
|
|
src = strings.TrimSpace(src)
|
|
if src == "" {
|
|
continue
|
|
}
|
|
req := mirror.FetchRequest{Source: src, Model: model, Version: version}
|
|
if i < len(files) {
|
|
req.File = strings.TrimSpace(files[i])
|
|
}
|
|
if _, err := m.Fetch(ctx, req); err != nil {
|
|
// Reported with what already went, so a set that fails on
|
|
// its fourth shard does not read as though nothing started.
|
|
return "", fmt.Errorf("%d of %d started, then %s: %w",
|
|
started, len(sources), mirror.LastPathElement(src), err)
|
|
}
|
|
started++
|
|
}
|
|
// Named as started, not as done. The mirror answered 202 and the
|
|
// bytes are still moving; a message saying "added" would invite
|
|
// somebody to point the catalog at a partial file.
|
|
return fmt.Sprintf("started downloading %s/%s (%s)",
|
|
model, version, plural(started, "file", "files")), nil
|
|
}))
|
|
|
|
if d.Catalog != nil {
|
|
// How a model runs, changed where the file it runs is seen.
|
|
mux.HandleFunc("POST /admin/mirror/mode", p.mirrorAction(m,
|
|
func(ctx context.Context, admin store.User, r *http.Request) (string, error) {
|
|
name, mode := r.FormValue("model"), r.FormValue("mode")
|
|
if name == "" || (mode != modeAlways && mode != modeOnDemand && mode != modeStopped) {
|
|
return "", errors.New("choose a model and how it should run")
|
|
}
|
|
replicas, _ := strconv.Atoi(r.FormValue("replicas"))
|
|
sc := scalingFor(mode, replicas, 0)
|
|
if _, err := d.Catalog.Scale(ctx, name, sc, admin.Email); err != nil {
|
|
return "", err
|
|
}
|
|
switch mode {
|
|
case modeOnDemand:
|
|
return name + " now loads when asked for and stops after 15 minutes unused", nil
|
|
case modeStopped:
|
|
return name + " is stopped; its replicas are being taken down", nil
|
|
}
|
|
return name + " now runs all the time", nil
|
|
}))
|
|
|
|
mux.HandleFunc("POST /admin/mirror/replicas", p.mirrorAction(m, p.setReplicas))
|
|
|
|
mux.HandleFunc("POST /admin/mirror/serve", p.mirrorAction(m,
|
|
func(ctx context.Context, admin store.User, r *http.Request) (string, error) {
|
|
model, err := buildModel(r.FormValue)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
// The same check `llmgw ctl model add` makes. This page builds
|
|
// the source out of a row the mirror reported, so it is right
|
|
// far more often than something typed — but a file removed
|
|
// between the listing and the click, or a mirror that has gone
|
|
// down since, both end as a replica failing on a node minutes
|
|
// later with an error naming the node. Asking costs one HEAD.
|
|
if _, err := sourcecheck.Check(ctx, model.Source); err != nil {
|
|
return "", err
|
|
}
|
|
if err := d.Catalog.Add(ctx, model, admin.Email); err != nil {
|
|
return "", err
|
|
}
|
|
// Placed on the next pass, not now: saying it is serving would
|
|
// be wrong for however long the weights take to reach a node.
|
|
return fmt.Sprintf(
|
|
"%s added to the catalog, %s; it is placed on the next pass",
|
|
model.Name, plural(model.Scaling.Min, "replica", "replicas")), nil
|
|
}))
|
|
}
|
|
|
|
mux.HandleFunc("POST /admin/mirror/{model}/{version}/remove", p.mirrorAction(m,
|
|
func(ctx context.Context, _ store.User, r *http.Request) (string, error) {
|
|
model, ver := r.PathValue("model"), r.PathValue("version")
|
|
removed, err := m.Remove(ctx, model, ver)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return "removed " + model + "/" + ver +
|
|
" (" + plural(len(removed), "file", "files") + ")", nil
|
|
}))
|
|
}
|
|
|
|
// mirrorAction wraps a change: admin session, CSRF, audit line, re-render.
|
|
//
|
|
// The audit line is not optional. Removing a version deletes weights every node
|
|
// may be about to fetch, and the only record that it happened on purpose is
|
|
// this log entry naming who clicked it.
|
|
func (p *AccountPages) mirrorAction(m MirrorAdmin,
|
|
do func(ctx context.Context, admin store.User, r *http.Request) (string, error),
|
|
) http.HandlerFunc {
|
|
return p.adminOnly(func(w http.ResponseWriter, r *http.Request, admin store.User) {
|
|
sess, _, _ := p.session(r)
|
|
if !p.accounts.CheckCSRF(sess, r.FormValue("csrf")) {
|
|
http.Error(w, "bad request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
msg, err := do(r.Context(), admin, r)
|
|
// Asked for from the page's own script — a fetch started from a
|
|
// repository opened in the list — the answer is the outcome alone,
|
|
// so the list, and the repository open in it, stay where they are.
|
|
wantJSON := strings.Contains(r.Header.Get("Accept"), "application/json")
|
|
if err != nil {
|
|
p.log.Warn("a mirror action was refused",
|
|
"admin", admin.Email, "mirror", m.Addr(), "err", err)
|
|
if wantJSON {
|
|
writeJSONStatus(w, http.StatusUnprocessableEntity, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
p.renderMirror(w, r, m, "", err.Error())
|
|
return
|
|
}
|
|
p.log.Info("the mirror was changed",
|
|
"admin", admin.Email, "mirror", m.Addr(), "what", msg)
|
|
if wantJSON {
|
|
writeJSONStatus(w, http.StatusOK, map[string]string{"message": msg})
|
|
return
|
|
}
|
|
p.renderMirror(w, r, m, msg, "")
|
|
})
|
|
}
|
|
|
|
func (p *AccountPages) renderMirror(w http.ResponseWriter, r *http.Request,
|
|
m MirrorAdmin, msg, errMsg string,
|
|
) {
|
|
d := adminMirrorData{
|
|
Nav: navFrom(r, "mirror"),
|
|
Addr: m.Addr(),
|
|
Message: msg,
|
|
Error: errMsg,
|
|
CSRF: csrfFor(r),
|
|
Version: p.version,
|
|
Now: time.Now().UTC().Format(time.RFC3339),
|
|
}
|
|
|
|
d.HasCatalog = p.catalog != nil
|
|
d.HasRegistry = p.registry != nil
|
|
d.Tab = mirrorTabFor(r, errMsg)
|
|
// Only the tab shown does its work: the registry is a server across the
|
|
// internet, and asking it for sixty repositories to show somebody who came
|
|
// to look at a download's progress made every visit wait on it.
|
|
if d.Tab == tabExplore {
|
|
p.explore(r, &d)
|
|
}
|
|
|
|
got, err := m.List(r.Context())
|
|
if err != nil {
|
|
p.log.Error("could not read the mirror", "mirror", m.Addr(), "err", err)
|
|
d.Unreachable = true
|
|
if d.Error == "" {
|
|
d.Error = err.Error()
|
|
}
|
|
d.Tabs = mirrorTabs(d)
|
|
render(w, "admin_mirror", d)
|
|
return
|
|
}
|
|
|
|
d.Base = got.Base
|
|
var sources map[string][]catalog.Model
|
|
if p.catalog != nil {
|
|
if sources, err = p.catalog.Sources(r.Context()); err != nil {
|
|
p.log.Warn("could not read which models use the mirror", "err", err)
|
|
}
|
|
}
|
|
held := map[string]int{} // model/version -> index of its row
|
|
for _, st := range got.Models {
|
|
switch st.State {
|
|
case mirror.Fetching:
|
|
d.Fetching++
|
|
case mirror.Failed:
|
|
d.Failed++
|
|
default:
|
|
key := st.Model + "/" + st.Version
|
|
if i, ok := held[key]; ok {
|
|
// Another file of a version already shown: counted into it.
|
|
d.Rows[i].Files++
|
|
d.Rows[i].BytesDone += st.BytesDone
|
|
d.Rows[i].URL = strings.TrimSuffix(
|
|
mirror.Entry{Model: st.Model, Version: st.Version}.URL(got.Base), "/") + "/"
|
|
continue
|
|
}
|
|
held[key] = len(d.Rows)
|
|
d.Present++ // versions, not files: what the rows below count
|
|
}
|
|
row := mirrorRow{
|
|
Status: st,
|
|
Files: 1,
|
|
URL: mirror.Entry{Model: st.Model, Version: st.Version, File: st.File}.
|
|
URL(got.Base),
|
|
}
|
|
if pc, ok := st.Percent(); ok {
|
|
row.Percent, row.HasPercent = pc, true
|
|
}
|
|
d.Rows = append(d.Rows, row)
|
|
}
|
|
var views map[string]adminapi.ModelView
|
|
if p.fleet != nil && len(sources) > 0 {
|
|
views = map[string]adminapi.ModelView{}
|
|
for _, v := range p.fleet.Models() {
|
|
views[v.Name] = v
|
|
}
|
|
}
|
|
for i := range d.Rows {
|
|
for _, m := range sources[d.Rows[i].URL] {
|
|
u := modelUse{
|
|
Name: m.Name, Mode: modeOf(m.Scaling), Replicas: max(m.Scaling.Max, 1),
|
|
Min: m.Scaling.Min, Max: m.Scaling.Max,
|
|
Declared: p.catalog.Declared(m.Name),
|
|
}
|
|
if v, ok := views[m.Name]; ok {
|
|
u.Fleet, u.Ready, u.Wanted = true, v.Ready, v.Desired
|
|
u.Parked, u.Unplaceable = v.Parked, v.Unplaceable
|
|
u.Nodes, u.Starting = runningOn(p.fleet.Replicas(m.Name))
|
|
}
|
|
d.Rows[i].UsedBy = append(d.Rows[i].UsedBy, u)
|
|
}
|
|
}
|
|
|
|
// The one row whose card is open. From the form too, so a change made in
|
|
// the card comes back with the card still open to show it.
|
|
open := r.FormValue("open")
|
|
for i := range d.Rows {
|
|
d.Rows[i].Open = openKey(d.Rows[i].Model, d.Rows[i].Version)
|
|
if open != "" && d.Rows[i].Open == open && d.Rows[i].State == mirror.Present {
|
|
d.Rows[i].Detail = p.heldDetailFor(d.Rows[i])
|
|
}
|
|
}
|
|
|
|
// The one row whose serve form is open, and only when the catalog can be
|
|
// changed at all.
|
|
if p.catalog != nil {
|
|
if want := r.URL.Query().Get("serve"); want != "" {
|
|
byVersion := map[string][]mirror.Status{}
|
|
for _, st := range got.Models {
|
|
byVersion[st.Model+"/"+st.Version] = append(byVersion[st.Model+"/"+st.Version], st)
|
|
}
|
|
for i := range d.Rows {
|
|
key := d.Rows[i].Model + ":" + d.Rows[i].Version
|
|
if key != want || d.Rows[i].State != mirror.Present {
|
|
continue
|
|
}
|
|
form := planServe(d.Rows[i].Status,
|
|
byVersion[d.Rows[i].Model+"/"+d.Rows[i].Version], got.Base, p.fleet, p.catalog, func(src string) gguf.Verdict { return p.checkSource(r.Context(), src) })
|
|
d.Rows[i].Serve = &form
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// Downloads first, then failures, then what is simply here. The two states
|
|
// somebody opened this page for are at the top; the rest is a list they
|
|
// scroll. Within a state the order is fixed so rows do not shuffle between
|
|
// reloads while a byte count ticks.
|
|
sort.SliceStable(d.Rows, func(i, j int) bool {
|
|
a, b := d.Rows[i], d.Rows[j]
|
|
if ra, rb := mirrorRank(a.State), mirrorRank(b.State); ra != rb {
|
|
return ra < rb
|
|
}
|
|
if a.Model != b.Model {
|
|
return a.Model < b.Model
|
|
}
|
|
if a.Version != b.Version {
|
|
return a.Version < b.Version
|
|
}
|
|
return a.File < b.File
|
|
})
|
|
|
|
d.Groups = groupByModel(d.Rows)
|
|
|
|
// Only on the list: a reload anywhere else takes with it a search, an
|
|
// open repository or a URL half typed.
|
|
if d.Fetching > 0 && d.Tab == tabHeld && open == "" {
|
|
d.RefreshSec = 5
|
|
}
|
|
d.Tabs = mirrorTabs(d)
|
|
render(w, "admin_mirror", d)
|
|
}
|
|
|
|
const (
|
|
tabHeld = "held"
|
|
tabExplore = "explore"
|
|
tabFetch = "fetch"
|
|
)
|
|
|
|
// mirrorTab is one of the page's tabs.
|
|
type mirrorTab struct {
|
|
Label string
|
|
Count string // beside the label, empty for none
|
|
Href string
|
|
Active bool
|
|
}
|
|
|
|
// mirrorTabFor is the tab a request is for.
|
|
//
|
|
// Named in the address when it is chosen, and otherwise read from what the
|
|
// address carries, so links made before the page had tabs — a search, an
|
|
// open repository — still land where they meant. After a change the list is
|
|
// shown, where its effect is, unless it was refused: then the form it came
|
|
// from, to try again.
|
|
func mirrorTabFor(r *http.Request, errMsg string) string {
|
|
valid := func(t string) bool { return t == tabHeld || t == tabExplore || t == tabFetch }
|
|
if r.Method == http.MethodPost {
|
|
if t := r.PostFormValue("tab"); errMsg != "" && valid(t) {
|
|
return t
|
|
}
|
|
return tabHeld
|
|
}
|
|
q := r.URL.Query()
|
|
if t := q.Get("tab"); valid(t) {
|
|
return t
|
|
}
|
|
for _, k := range []string{"q", "repo", "view", "task", "size", "x"} {
|
|
if q.Has(k) {
|
|
return tabExplore
|
|
}
|
|
}
|
|
return tabHeld
|
|
}
|
|
|
|
func mirrorTabs(d adminMirrorData) []mirrorTab {
|
|
held := mirrorTab{Label: "Held", Href: "/admin/mirror?tab=held", Active: d.Tab == tabHeld}
|
|
switch {
|
|
case d.Unreachable:
|
|
held.Count = "unreachable"
|
|
case d.Fetching > 0:
|
|
held.Count = fmt.Sprintf("%d · %d downloading", d.Present, d.Fetching)
|
|
default:
|
|
held.Count = strconv.Itoa(d.Present)
|
|
}
|
|
if d.Failed > 0 {
|
|
held.Count += fmt.Sprintf(" · %d failed", d.Failed)
|
|
}
|
|
tabs := []mirrorTab{held}
|
|
if d.HasRegistry {
|
|
tabs = append(tabs, mirrorTab{Label: "Explore Hugging Face",
|
|
Href: "/admin/mirror?tab=explore", Active: d.Tab == tabExplore})
|
|
}
|
|
return append(tabs, mirrorTab{Label: "Fetch by URL",
|
|
Href: "/admin/mirror?tab=fetch", Active: d.Tab == tabFetch})
|
|
}
|
|
|
|
// mirrorGroup is a family of models the mirror holds — every qwen, every
|
|
// llama — and their versions.
|
|
type mirrorGroup struct {
|
|
Family string
|
|
Rows []mirrorRow
|
|
// Models is how many different models are in it; a family of one is
|
|
// shown without a heading, which would only repeat its row.
|
|
Models int
|
|
// Bytes is what the versions take on disk, downloads so far included.
|
|
Bytes int64
|
|
Fetching, Failed int
|
|
InUse bool
|
|
}
|
|
|
|
// modelFamily is the family a model's name puts it in: the letters its name
|
|
// starts with. "qwen2.5-coder-7b-instruct" and "qwen3-8b" are qwen,
|
|
// "llama-3.1-8b-instruct" is llama, "deepseek-r1-distill-qwen-14b" is
|
|
// deepseek — which is how people talk about them, and all a name says.
|
|
func modelFamily(model string) string {
|
|
name := strings.ToLower(model)
|
|
end := strings.IndexFunc(name, func(r rune) bool { return r < 'a' || r > 'z' })
|
|
if end <= 0 {
|
|
// Starts with a digit or a symbol: the first part whole.
|
|
first, _, _ := strings.Cut(name, "-")
|
|
return first
|
|
}
|
|
return name[:end]
|
|
}
|
|
|
|
// groupByModel gathers rows by family, in the order the rows are already in:
|
|
// a family with a download or a failure comes first because its first row
|
|
// does, and its models and versions stay in that order beneath it.
|
|
//
|
|
// Grouped because a family is what somebody looks for. Sorted by state and
|
|
// name alone, "which qwens do we have" meant reading the whole table.
|
|
func groupByModel(rows []mirrorRow) []mirrorGroup {
|
|
var out []mirrorGroup
|
|
at := map[string]int{}
|
|
models := map[string]map[string]bool{}
|
|
for _, r := range rows {
|
|
fam := modelFamily(r.Model)
|
|
i, ok := at[fam]
|
|
if !ok {
|
|
i = len(out)
|
|
at[fam] = i
|
|
out = append(out, mirrorGroup{Family: fam})
|
|
models[fam] = map[string]bool{}
|
|
}
|
|
g := &out[i]
|
|
g.Rows = append(g.Rows, r)
|
|
g.Bytes += r.BytesDone
|
|
models[fam][r.Model] = true
|
|
switch r.State {
|
|
case mirror.Fetching:
|
|
g.Fetching++
|
|
case mirror.Failed:
|
|
g.Failed++
|
|
}
|
|
if len(r.UsedBy) > 0 {
|
|
g.InUse = true
|
|
}
|
|
}
|
|
for i := range out {
|
|
out[i].Models = len(models[out[i].Family])
|
|
}
|
|
return out
|
|
}
|
|
|
|
func mirrorRank(s mirror.State) int {
|
|
switch s {
|
|
case mirror.Fetching:
|
|
return 0
|
|
case mirror.Failed:
|
|
return 1
|
|
default:
|
|
return 2
|
|
}
|
|
}
|
|
|
|
func plural(n int, one, many string) string {
|
|
if n == 1 {
|
|
return "1 " + one
|
|
}
|
|
return strconv.Itoa(n) + " " + many
|
|
}
|
|
|
|
// explore fills in the registry half of the page: a listing, or one repository.
|
|
//
|
|
// A failure here is reported in its own line rather than as the page's error.
|
|
// The registry is somebody else's server on the other side of the internet, and
|
|
// it being down says nothing about the mirror — which is the thing this page is
|
|
// actually for, and which must keep working when Hugging Face does not.
|
|
func (p *AccountPages) explore(r *http.Request, d *adminMirrorData) {
|
|
if p.registry == nil {
|
|
return
|
|
}
|
|
d.HasRegistry = true
|
|
d.Query = strings.TrimSpace(r.URL.Query().Get("q"))
|
|
|
|
if repo := strings.TrimSpace(r.URL.Query().Get("repo")); repo != "" {
|
|
d.Back = exploreState(r.URL.Query()).href(d.Query, "", "")
|
|
p.openRepo(r, d, repo)
|
|
return
|
|
}
|
|
|
|
p.exploreList(r, d)
|
|
}
|
|
|
|
// openRepo reads one repository: its card, and its weights grouped into what
|
|
// can be fetched.
|
|
func (p *AccountPages) openRepo(r *http.Request, d *adminMirrorData, repo string) {
|
|
got, err := p.registry.Repo(r.Context(), repo)
|
|
if err != nil {
|
|
d.RegistryErr = err.Error()
|
|
return
|
|
}
|
|
d.Repo = &got
|
|
d.RepoURL = strings.TrimSuffix(got.DownloadURL("", ""), "/resolve/"+revOrMain(got.Revision)+"/")
|
|
d.Card = renderCard(got.Card, d.RepoURL+"/blob/"+revOrMain(got.Revision)+"/")
|
|
var weights []hf.File
|
|
for _, f := range got.Files {
|
|
if !f.IsWeights() {
|
|
// Thirty files of which four are worth gigabytes. The rest are
|
|
// a README and a tokenizer, and listing them buries the ones
|
|
// somebody came for.
|
|
continue
|
|
}
|
|
weights = append(weights, f)
|
|
}
|
|
for _, set := range hf.Group(weights) {
|
|
rs := repoSet{
|
|
FileSet: set,
|
|
Model: defaultModelName(got.ID),
|
|
// The revision, not the date: two fetches of the same commit
|
|
// land in the same place, and a repository re-uploaded under
|
|
// the same branch lands somewhere else.
|
|
Version: got.ShortRevision(),
|
|
}
|
|
rs.Verdict = p.verdictFor(got.Architecture, set.Name)
|
|
for _, f := range set.Files {
|
|
rs.Sources = append(rs.Sources,
|
|
repoSource{URL: got.DownloadURL("", f.Path), Path: f.Path})
|
|
}
|
|
d.RepoSets = append(d.RepoSets, rs)
|
|
}
|
|
}
|
|
|
|
// defaultModelName is the catalog name a repository suggests.
|
|
//
|
|
// The last element, lowercased, with the quantisation marker dropped: the owner
|
|
// is not part of what anybody calls the model, and "-GGUF" describes the file
|
|
// format rather than the model, which is already clear from the file being
|
|
// fetched.
|
|
func defaultModelName(repoID string) string { return hf.ModelName(repoID) }
|
|
|
|
// verdictFor judges a registry file set by the repository's architecture. A
|
|
// vision projector is its own architecture whatever the repository says.
|
|
func (p *AccountPages) verdictFor(arch, name string) gguf.Verdict {
|
|
if strings.Contains(strings.ToLower(name), "mmproj") {
|
|
return gguf.LlamaCpp(p.llamaVersion, "clip")
|
|
}
|
|
// Published beside the quantisations they were made with, and a GGUF
|
|
// file like them, so the repository's architecture would pass it.
|
|
if strings.Contains(strings.ToLower(name), "imatrix") {
|
|
return gguf.Verdict{Checked: true,
|
|
Reason: "it is an importance matrix for quantising, not a model"}
|
|
}
|
|
if arch == "" || !gguf.IsGGUF(name) {
|
|
return gguf.Verdict{Servable: true}
|
|
}
|
|
return gguf.LlamaCpp(p.llamaVersion, arch)
|
|
}
|
|
|
|
// checkSource reads a GGUF source's header and judges it; anything else, or a
|
|
// header that cannot be read, passes.
|
|
func (p *AccountPages) checkSource(ctx context.Context, source string) gguf.Verdict {
|
|
url, ok := sourcecheck.HTTPSource(source)
|
|
if !ok || !gguf.IsGGUF(url) || p.llamaVersion == "" {
|
|
return gguf.Verdict{Servable: true}
|
|
}
|
|
return gguf.Check(ctx, url, p.llamaVersion)
|
|
}
|
|
|
|
func revOrMain(rev string) string {
|
|
if rev == "" {
|
|
return "main"
|
|
}
|
|
return rev
|
|
}
|
|
|
|
func writeJSONStatus(w http.ResponseWriter, code int, v any) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(code)
|
|
_ = json.NewEncoder(w).Encode(v)
|
|
}
|
|
|
|
// runningOn is the nodes a model's replicas are on, each once and in order,
|
|
// and how many of its replicas are not ready yet.
|
|
func runningOn(reps []adminapi.ReplicaView) (nodes []string, starting int) {
|
|
for _, r := range reps {
|
|
if r.State != "ready" {
|
|
starting++
|
|
}
|
|
for _, sh := range r.Shards {
|
|
if !slices.Contains(nodes, sh.Node) {
|
|
nodes = append(nodes, sh.Node)
|
|
}
|
|
}
|
|
}
|
|
slices.Sort(nodes)
|
|
return nodes, starting
|
|
}
|