Compare commits
1
Commits
v0.10.4
...
v0.10.5-rc.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ae12ae1fe |
No files matched your search
@@ -20,6 +20,22 @@ is only released once its **exit criterion** is met, not when its code lands.
|
||||
Until v1.0.0 the minor version is the breaking one: configuration formats and Go
|
||||
APIs change between minors without a deprecation period.
|
||||
|
||||
## Unreleased
|
||||
|
||||
Changes since v0.10.4, published as release candidates (v0.10.5-rc.N) until
|
||||
there are enough for v0.10.5.
|
||||
|
||||
### The mirror page in tabs
|
||||
|
||||
/admin/mirror was one long page: what the mirror holds, the Hugging Face
|
||||
explorer and the fetch form, one under another, with every visit waiting on
|
||||
Hugging Face for a list it might not show. It is now three tabs — Held (with
|
||||
how many are held, downloading and failed), Explore Hugging Face, and Fetch by
|
||||
URL — each with its own address. Only the tab shown does its work, and only
|
||||
Held reloads while something downloads. A download started from the explorer
|
||||
links to Held, where its progress is; a fetch the mirror refused comes back on
|
||||
the form it was sent from.
|
||||
|
||||
## v0.10.4 — 2026-09-30
|
||||
|
||||
Published first as release candidates v0.10.4-rc.1 to rc.9.
|
||||
|
||||
@@ -100,7 +100,11 @@ type mirrorRow struct {
|
||||
}
|
||||
|
||||
type adminMirrorData struct {
|
||||
Nav Nav
|
||||
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
|
||||
@@ -361,7 +365,14 @@ func (p *AccountPages) renderMirror(w http.ResponseWriter, r *http.Request,
|
||||
}
|
||||
|
||||
d.HasCatalog = p.catalog != nil
|
||||
p.explore(r, &d)
|
||||
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 {
|
||||
@@ -370,6 +381,7 @@ func (p *AccountPages) renderMirror(w http.ResponseWriter, r *http.Request,
|
||||
if d.Error == "" {
|
||||
d.Error = err.Error()
|
||||
}
|
||||
d.Tabs = mirrorTabs(d)
|
||||
render(w, "admin_mirror", d)
|
||||
return
|
||||
}
|
||||
@@ -462,12 +474,78 @@ func (p *AccountPages) renderMirror(w http.ResponseWriter, r *http.Request,
|
||||
|
||||
d.Groups = groupByModel(d.Rows)
|
||||
|
||||
if d.Fetching > 0 {
|
||||
// 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 {
|
||||
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 {
|
||||
|
||||
@@ -341,16 +341,18 @@ func TestAFailedRowOffersRetryRatherThanRemove(t *testing.T) {
|
||||
}
|
||||
|
||||
type fakeRegistry struct {
|
||||
results []hf.Model
|
||||
repo hf.Repo
|
||||
err error
|
||||
asked string
|
||||
query hf.Query
|
||||
results []hf.Model
|
||||
repo hf.Repo
|
||||
err error
|
||||
asked string
|
||||
query hf.Query
|
||||
explored int
|
||||
}
|
||||
|
||||
func (f *fakeRegistry) Explore(_ context.Context, q hf.Query) ([]hf.Model, error) {
|
||||
f.asked = q.Search
|
||||
f.query = q
|
||||
f.explored++
|
||||
return f.results, f.err
|
||||
}
|
||||
|
||||
@@ -441,7 +443,11 @@ func TestWithNoRegistryThereIsNoSearchBox(t *testing.T) {
|
||||
if strings.Contains(body, "Search Hugging Face") {
|
||||
t.Error("the explorer should be absent without a registry")
|
||||
}
|
||||
if !strings.Contains(body, "Fetch a model") {
|
||||
if strings.Contains(body, "Explore Hugging Face") {
|
||||
t.Error("a tab for an explorer that is not there")
|
||||
}
|
||||
fetch := mirrorGet(t, mirrorPage(t, &fakeMirror{}, true), "/admin/mirror?tab=fetch").Body.String()
|
||||
if !strings.Contains(fetch, `name="source" required`) {
|
||||
t.Error("fetching by hand must still be there")
|
||||
}
|
||||
}
|
||||
@@ -464,8 +470,7 @@ func TestAShardedModelIsOneRowAndOneButton(t *testing.T) {
|
||||
"/admin/mirror?repo=Qwen/Qwen2.5-7B-Instruct").Body.String()
|
||||
|
||||
// Fifteen shards is one decision, not fifteen. Four here, and one form.
|
||||
if n := strings.Count(body, `action="/admin/mirror/fetch"`); n != 2 {
|
||||
// One for the set, one for the manual form at the bottom.
|
||||
if n := strings.Count(body, `action="/admin/mirror/fetch"`); n != 1 {
|
||||
t.Errorf("expected the four shards as a single form, found %d forms", n)
|
||||
}
|
||||
if !strings.Contains(body, "4 shards, fetched together") {
|
||||
@@ -534,8 +539,8 @@ func TestAlternativeQuantisationsStaySeparateRows(t *testing.T) {
|
||||
|
||||
// Two GGUFs are alternatives, not halves. Offering "fetch all" here would
|
||||
// spend gigabytes on a copy nobody asked for.
|
||||
if n := strings.Count(body, `action="/admin/mirror/fetch"`); n != 3 {
|
||||
t.Errorf("expected a form each plus the manual one, found %d", n)
|
||||
if n := strings.Count(body, `action="/admin/mirror/fetch"`); n != 2 {
|
||||
t.Errorf("expected a form each, found %d", n)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -655,3 +660,53 @@ func privateOK(m *mirror.Manager) *mirror.Manager {
|
||||
m.AllowPrivateSources()
|
||||
return m
|
||||
}
|
||||
|
||||
func TestEachTabDoesOnlyItsOwnWork(t *testing.T) {
|
||||
reg := &fakeRegistry{}
|
||||
mux := mirrorPageWith(t, &fakeMirror{models: []mirror.Status{
|
||||
{Model: "m", Version: "v", File: "m.gguf", State: mirror.Present, BytesDone: 1},
|
||||
{Model: "n", Version: "v", File: "n.gguf", State: mirror.Fetching, BytesDone: 1},
|
||||
}}, reg, true)
|
||||
|
||||
held := mirrorGet(t, mux, "/admin/mirror").Body.String()
|
||||
if reg.explored != 0 {
|
||||
t.Error("the list asked the registry for repositories it does not show")
|
||||
}
|
||||
if !strings.Contains(held, `class="on" aria-current="page">Held <span class="count">1 · 1 downloading</span>`) {
|
||||
t.Errorf("the Held tab should be chosen and count what it holds: %s", held)
|
||||
}
|
||||
if strings.Contains(held, `name="source" required`) || strings.Contains(held, "Search Hugging Face") {
|
||||
t.Error("other tabs' forms are on the list")
|
||||
}
|
||||
if !strings.Contains(held, `http-equiv="refresh"`) {
|
||||
t.Error("the list stopped following a download")
|
||||
}
|
||||
|
||||
explore := mirrorGet(t, mux, "/admin/mirror?tab=explore").Body.String()
|
||||
if reg.explored != 1 || !strings.Contains(explore, "Search Hugging Face") {
|
||||
t.Error("the explorer tab did not explore")
|
||||
}
|
||||
if strings.Contains(explore, `id="held"`) || strings.Contains(explore, `http-equiv="refresh"`) {
|
||||
t.Error("the explorer shows the list, or reloads away a search")
|
||||
}
|
||||
// A link from before there were tabs still lands on the explorer.
|
||||
if old := mirrorGet(t, mux, "/admin/mirror?q=qwen").Body.String(); !strings.Contains(old, "Search Hugging Face") {
|
||||
t.Error("a search address no longer opens the explorer")
|
||||
}
|
||||
}
|
||||
|
||||
func TestARefusedFetchReturnsToItsFormAndAStartedOneToTheList(t *testing.T) {
|
||||
m := &fakeMirror{fetchErr: errors.New("upstream said 404")}
|
||||
mux := mirrorPage(t, m, true)
|
||||
body := mirrorPost(t, mux, "/admin/mirror/fetch", url.Values{"csrf": {"tok"}, "source": {"https://h.example/m.gguf"}, "model": {"m"},
|
||||
"version": {"v"}, "force": {"1"}, "tab": {"fetch"}}).Body.String()
|
||||
if !strings.Contains(body, "upstream said 404") || !strings.Contains(body, `name="source" required`) {
|
||||
t.Errorf("a refusal should come back on the form it came from: %s", body)
|
||||
}
|
||||
m.fetchErr = nil
|
||||
body = mirrorPost(t, mux, "/admin/mirror/fetch", url.Values{"csrf": {"tok"}, "source": {"https://h.example/m.gguf"}, "model": {"m"},
|
||||
"version": {"v"}, "force": {"1"}, "tab": {"fetch"}}).Body.String()
|
||||
if !strings.Contains(body, `id="held"`) {
|
||||
t.Errorf("a started download should be shown where it is followed: %s", body)
|
||||
}
|
||||
}
|
||||
@@ -155,7 +155,7 @@ func oneOf(s string, options ...string) bool {
|
||||
|
||||
// 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"}}
|
||||
v := url.Values{"tab": {tabExplore}, "view": {e.View}, "task": {e.Task}, "size": {e.Size}, "x": {"1"}}
|
||||
if e.Here {
|
||||
v.Set("here", "1")
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ func TestTheExplorerOpensOnTrendingModelsTheNodesCanRun(t *testing.T) {
|
||||
ggufModel("odd/Kev-GGUF", "ggmlc", 5e8, now.Add(-2*24*time.Hour)),
|
||||
}}
|
||||
body := mirrorGet(t, mirrorPageLlama(t, MirrorDeps{Mirror: &fakeMirror{}, Registry: reg}),
|
||||
"/admin/mirror").Body.String()
|
||||
"/admin/mirror?tab=explore").Body.String()
|
||||
|
||||
q := reg.query
|
||||
if q.Sort != hf.SortTrending || q.Task != "text-generation" || !q.GGUF || q.Search != "" {
|
||||
@@ -96,7 +96,7 @@ func TestAnAdapterIsNotListedAsAModel(t *testing.T) {
|
||||
ggufModel("a/Tiny-GGUF", "qwen2", 135e6, time.Now()),
|
||||
}}
|
||||
body := mirrorGet(t, mirrorPageLlama(t, MirrorDeps{Mirror: &fakeMirror{}, Registry: reg}),
|
||||
"/admin/mirror").Body.String()
|
||||
"/admin/mirror?tab=explore").Body.String()
|
||||
if strings.Contains(body, "Some-LoRA") || !strings.Contains(body, "a/Tiny-GGUF") {
|
||||
t.Errorf("adapters should be left out and small models kept: %s", body)
|
||||
}
|
||||
|
||||
@@ -456,6 +456,15 @@ table.held select.mode { width: 100%; padding: .15rem .3rem; font-size: .8rem; }
|
||||
/* Link pills: the same look as .tabs, for choices that are addresses rather
|
||||
than panels on one page — a filtered listing should survive a reload and be
|
||||
worth sending to somebody. */
|
||||
/* A page's own sections, one address each. Underlined rather than pills so
|
||||
* they do not read as one more filter beside the ones inside a section. */
|
||||
.page-tabs { display: flex; flex-wrap: wrap; gap: 0 1.5rem; margin: -1rem 0 1.5rem;
|
||||
border-bottom: 1px solid var(--line); }
|
||||
.page-tabs a { padding: .55rem 0; margin-bottom: -1px; font-weight: 600; color: var(--muted);
|
||||
text-decoration: none; border-bottom: 2px solid transparent; }
|
||||
.page-tabs a:hover { color: var(--fg); }
|
||||
.page-tabs a.on { color: var(--fg); border-bottom-color: var(--accent); }
|
||||
.page-tabs .count { font-weight: 500; font-size: .8rem; color: var(--muted); margin-left: .25rem; }
|
||||
.pills { display: flex; flex-wrap: wrap; gap: .35rem; margin: .5rem 0; }
|
||||
.pills a { font-size: .85rem; font-weight: 600; color: var(--muted); padding: .35rem .85rem;
|
||||
border: 1px solid transparent; border-radius: 999px; text-decoration: none; }
|
||||
|
||||
@@ -8,16 +8,20 @@
|
||||
</head><body><div class="wrap">
|
||||
<header>
|
||||
<h1>Model mirror</h1>
|
||||
<p class="sub">
|
||||
{{if .Unreachable}}{{.Addr}} did not answer{{else}}{{.Present}} held ·
|
||||
{{.Fetching}} downloading · {{.Failed}} failed · {{.Addr}}{{end}}
|
||||
</p>
|
||||
<p class="sub">{{.Addr}}{{if .Unreachable}} did not answer{{end}}</p>
|
||||
{{template "nav" .Nav}}
|
||||
</header>
|
||||
|
||||
{{/* Links rather than a script's panels: each tab has an address, the back
|
||||
button works, and only the one shown asks anything of the registry. */}}
|
||||
<nav class="page-tabs" aria-label="Mirror">
|
||||
{{range .Tabs}}<a href="{{.Href}}"{{if .Active}} class="on" aria-current="page"{{end}}>{{.Label}}{{with .Count}} <span class="count">{{.}}</span>{{end}}</a>
|
||||
{{end}}</nav>
|
||||
|
||||
{{if .Error}}<p class="error">{{.Error}}</p>{{end}}
|
||||
{{if .Message}}<p class="ok">{{.Message}}</p>{{end}}
|
||||
|
||||
{{if eq .Tab "held"}}
|
||||
{{if .Unreachable}}
|
||||
{{/* Said outright rather than shown as an empty table. A mirror holding
|
||||
nothing and a mirror that is not there look the same in a list of zero
|
||||
@@ -29,7 +33,6 @@ reachable from this machine.</p>
|
||||
{{else}}
|
||||
|
||||
<div id="held">
|
||||
<h2>What the mirror holds</h2>
|
||||
{{if .Rows}}
|
||||
<table class="compact held">
|
||||
<tr><th>Model</th><th>Files</th><th class="num">Size</th><th>Runs</th><th></th></tr>
|
||||
@@ -190,11 +193,14 @@ document.addEventListener('click', function (e) {
|
||||
} else { window.prompt('Address', b.dataset.url); }
|
||||
});
|
||||
</script>
|
||||
{{else}}<p class="empty">The mirror is reachable and holds nothing yet.</p>{{end}}
|
||||
{{else}}<p class="empty">The mirror is reachable and holds nothing yet.
|
||||
{{if .HasRegistry}}<a href="/admin/mirror?tab=explore">Explore Hugging Face</a> for a model, or
|
||||
{{end}}<a href="/admin/mirror?tab=fetch">fetch one by URL</a>.</p>{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
{{if .HasRegistry}}
|
||||
<h2>Explore models</h2>
|
||||
{{if and (eq .Tab "explore") .HasRegistry}}
|
||||
{{if not .Repo}}{{with .Explore}}
|
||||
<nav class="pills" aria-label="Which models">{{range .Views}}<a href="{{.Href}}"{{if .Active}} class="on" aria-current="page"{{end}}>{{.Label}}</a>{{end}}</nav>
|
||||
<div class="explore-filters">
|
||||
@@ -203,6 +209,7 @@ document.addEventListener('click', function (e) {
|
||||
</div>
|
||||
{{end}}
|
||||
<form method="get" action="/admin/mirror" class="explore-search">
|
||||
<input type="hidden" name="tab" value="explore">
|
||||
{{with .Explore}}<input type="hidden" name="view" value="{{.View}}">
|
||||
<input type="hidden" name="task" value="{{.Task}}">
|
||||
<input type="hidden" name="size" value="{{.Size}}">
|
||||
@@ -307,9 +314,12 @@ document.querySelector("table.explore").addEventListener("submit", async functio
|
||||
return;
|
||||
}
|
||||
note.classList.remove("bad");
|
||||
note.textContent = "✓ " + out.message + " — its progress is in the table above.";
|
||||
note.textContent = "✓ " + out.message + " — ";
|
||||
const link = document.createElement("a");
|
||||
link.href = "/admin/mirror?tab=held";
|
||||
link.textContent = "follow it under Held";
|
||||
note.appendChild(link);
|
||||
btn.textContent = "Started";
|
||||
followHeld();
|
||||
} catch (err) {
|
||||
note.textContent = "Could not reach the gateway: " + err;
|
||||
note.classList.add("bad");
|
||||
@@ -317,28 +327,6 @@ document.querySelector("table.explore").addEventListener("submit", async functio
|
||||
btn.textContent = was;
|
||||
}
|
||||
});
|
||||
|
||||
// Refreshes only the mirror's own table, every five seconds while anything
|
||||
// in it is downloading.
|
||||
let following = false;
|
||||
async function followHeld() {
|
||||
if (following) return;
|
||||
following = true;
|
||||
try {
|
||||
for (;;) {
|
||||
const r = await fetch(location.pathname + location.search, { headers: { Accept: "text/html" } });
|
||||
if (!r.ok) break;
|
||||
const doc = new DOMParser().parseFromString(await r.text(), "text/html");
|
||||
const fresh = doc.getElementById("held"), held = document.getElementById("held");
|
||||
if (!fresh || !held) break;
|
||||
held.replaceWith(fresh);
|
||||
if (!fresh.querySelector(".state-starting")) break;
|
||||
await new Promise((ok) => setTimeout(ok, 5000));
|
||||
}
|
||||
} finally {
|
||||
following = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{{if .Hidden}}<p class="note">{{.Hidden}} more {{if eq .Hidden 1}}is{{else}}are{{end}} hidden: their architecture is one the nodes' llama.cpp cannot load.
|
||||
Untick “only what these nodes can run” to see them.</p>{{end}}
|
||||
@@ -354,10 +342,11 @@ directory, or give the mirror a token.</p>
|
||||
{{if .Repo}}{{template "mirror_repo" .}}{{end}}
|
||||
{{end}}
|
||||
|
||||
<h2>Fetch a model</h2>
|
||||
{{if eq .Tab "fetch"}}
|
||||
<p class="note">By hand, for anything the search does not reach.</p>
|
||||
<form method="post" action="/admin/mirror/fetch">
|
||||
<input type="hidden" name="csrf" value="{{.CSRF}}">
|
||||
<input type="hidden" name="tab" value="fetch">
|
||||
<label>Source URL
|
||||
<input type="text" name="source" required
|
||||
placeholder="https://huggingface.co/…/resolve/main/model.gguf"></label>
|
||||
@@ -373,7 +362,7 @@ directory, or give the mirror a token.</p>
|
||||
</form>
|
||||
<p class="note">The mirror answers as soon as the download is under way, not when
|
||||
it finishes — a model is tens of gigabytes and nothing here holds a connection
|
||||
open for that. The row above will show it arriving. A version is never
|
||||
open for that. The Held tab shows it arriving. A version is never
|
||||
overwritten: fetching into one that exists is refused, so a new build is a new
|
||||
version and the old weights stay where they are.</p>
|
||||
{{end}}
|
||||
@@ -415,6 +404,7 @@ version and the old weights stay where they are.</p>
|
||||
{{if .Verdict.Architecture}}<span class="opt" title="{{.Verdict.Reason}}">{{.Verdict.Architecture}}{{if .Verdict.Checked}} ✓{{end}}</span>{{end}}
|
||||
<form method="post" action="/admin/mirror/fetch" class="inline">
|
||||
<input type="hidden" name="csrf" value="{{$.CSRF}}">
|
||||
<input type="hidden" name="tab" value="explore">
|
||||
{{range .Sources}}<input type="hidden" name="source" value="{{.URL}}">
|
||||
<input type="hidden" name="file" value="{{.Path}}">
|
||||
{{end}}
|
||||
|
||||
Reference in new issue
Block a user