CI / test (push) Successful in 8m58s
Judged by the nodes as they are now, through modelplan.Options, the planner that serving a model uses: GGUF at Q4_K_M from the parameter count, safetensors at the size the registry's dtype breakdown gives (packed AWQ, GPTQ and MXFP4 at four bits), FP8 only where a card has compute capability 8.9, MLX safetensors never. A picture model is judged against one card. A cluster with cards asks the registry for safetensors too, a hundred at a time; one without asks for GGUF only. The page says what it judged by: the largest card, its capability, the most RAM on one node. On a repository's page a file set no node holds is marked with what it needs, against what the best node offers, and has no Fetch button. Tried against the PoC's seven nodes and Hugging Face's trending hundred: 63 shown, the rest too big for one 24 GB Turing card or FP8. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
621 lines
34 KiB
HTML
621 lines
34 KiB
HTML
{{define "admin_mirror"}}<!doctype html>
|
|
<html lang="{{lang}}"><head>
|
|
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
{{/* Only while something is downloading, and not while a form is being
|
|
filled in: pages.js holds the reload off then. */}}
|
|
{{if .RefreshSec}}<meta name="llmgw-refresh" content="{{.RefreshSec}}">{{end}}
|
|
<script defer src="{{asset "pages.js"}}"></script>
|
|
<title>{{t "mirror.title"}} · llmgw</title><link rel="stylesheet" href="{{stylesheet}}">
|
|
</head><body><div class="wrap">
|
|
<header>
|
|
<h1>{{t "mirror.heading"}}</h1>
|
|
<p class="sub">{{.Addr}}{{if .Unreachable}} {{t "mirror.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="{{t "mirror.tabs_aria"}}">
|
|
{{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
|
|
rows, and they call for opposite responses. */}}
|
|
<p class="note">{{th "mirror.unreachable" .Addr}}</p>
|
|
{{else}}
|
|
|
|
<div id="held">
|
|
{{if .Rows}}
|
|
<table class="rows compact held">
|
|
<tr><th>{{t "mirror.col.model"}}</th><th>{{t "mirror.col.files"}}</th><th class="num">{{t "mirror.col.size"}}</th><th>{{t "mirror.col.runs"}}</th><th></th></tr>
|
|
{{range .Groups}}{{$grouped := gt .Models 1}}
|
|
{{if $grouped}}
|
|
<tr class="model-group"><td colspan="5"><strong>{{.Family}}</strong>
|
|
<span class="opt">{{tn "mirror.group.models" .Models (bytes .Bytes)}}</span>
|
|
{{if .Fetching}}<span class="tag new">{{t "mirror.state.fetching"}}</span>{{end}}
|
|
{{if .Failed}}<span class="tag bad">{{t "mirror.state.failed"}}</span>{{end}}
|
|
{{if .InUse}}<span class="tag">{{t "mirror.group.in_use"}}</span>{{end}}</td></tr>
|
|
{{end}}
|
|
{{range .Rows}}{{$row0 := .}}<tr{{if $grouped}} class="model-version"{{end}}{{if .Detail}} class="open"{{end}}>
|
|
<td>{{if eq (printf "%s" .State) "present"}}<a class="open-held" aria-expanded="{{if .Detail}}true{{else}}false{{end}}"
|
|
href="/admin/mirror?tab=held{{if not .Detail}}&open={{.Open}}{{end}}"><strong>{{.Model}}</strong></a>
|
|
{{else}}<strong>{{.Model}}</strong>{{end}} <span class="opt">{{.Version}}</span>
|
|
{{if eq .Kind "adapter"}}<span class="tag new" title="{{t "mirror.adapter_title" .Arch}}">{{t "mirror.adapter"}}</span>{{end}}</td>
|
|
<td class="file" title="{{if .SHA256}}{{t "mirror.file_sha" .File .SHA256}}{{else}}{{.File}}{{end}}">
|
|
{{if gt .Files 1}}{{tn "mirror.files" .Files}}{{else}}<code>{{.File}}</code>{{end}}</td>
|
|
<td class="num">
|
|
{{if eq (printf "%s" .State) "fetching"}}
|
|
{{if .HasPercent}}{{t "mirror.percent_of" .Percent (bytes .BytesTotal)}}
|
|
{{else}}{{/* No total came from upstream, so no percentage is invented —
|
|
a bar that guesses its own end and then stops moving is worse
|
|
than a number that only goes up. */}}
|
|
{{t "mirror.so_far" (bytes .BytesDone)}}{{end}}
|
|
{{else if eq (printf "%s" .State) "failed"}}—
|
|
{{else}}{{bytes .BytesDone}}{{end}}
|
|
</td>
|
|
<td>
|
|
{{if eq (printf "%s" .State) "fetching"}}<span class="state state-starting">{{t "mirror.state.fetching"}}</span>
|
|
{{else if eq (printf "%s" .State) "failed"}}<span class="state state-failed">{{t "mirror.state.failed"}}</span>
|
|
{{else if .UsedBy}}{{$row := .}}{{range .UsedBy}}<div class="use">
|
|
{{/* Named only when the catalog calls it something other than the
|
|
mirror does; otherwise it is the model column again. */}}
|
|
{{if ne .Name $row.Model}}<span class="as" title="{{.Name}}">{{t "mirror.use.as" .Name}}</span>{{end}}
|
|
{{if .Declared}}<span class="mode-fixed" title="{{t "mirror.use.yaml_title"}}">
|
|
{{- t "mirror.use.yaml" (t (print "mirror.mode." .Mode))}}</span>
|
|
{{else}}
|
|
{{/* Submitted on change: the choice is the action, and a second click
|
|
to confirm it would only be a way to forget to. */}}
|
|
<form method="post" action="/admin/mirror/mode" class="inline">
|
|
<input type="hidden" name="csrf" value="{{$.CSRF}}">
|
|
<input type="hidden" name="model" value="{{.Name}}">
|
|
<input type="hidden" name="replicas" value="{{.Replicas}}">
|
|
<select name="mode" class="mode" data-autosubmit aria-label="{{t "mirror.use.mode_aria" .Name}}">
|
|
<option value="always"{{if eq .Mode "always"}} selected{{end}}>{{t "mirror.mode.always"}}</option>
|
|
<option value="ondemand"{{if eq .Mode "ondemand"}} selected{{end}}>{{t "mirror.mode.ondemand"}}</option>
|
|
<option value="stopped"{{if eq .Mode "stopped"}} selected{{end}}>{{t "mirror.mode.stopped"}}</option>
|
|
</select>
|
|
{{/* The button is in the catalogue: what a noscript holds is read as text, not markup. */}}<noscript>{{th "mirror.use.set"}}</noscript>
|
|
</form>
|
|
{{end}}
|
|
{{if .Fleet}}<span class="runs-now{{if or .Parked .Unplaceable}} bad{{else if lt .Ready .Wanted}} short{{end}}" title="{{if .Starting}}{{t "mirror.use.runs_title_starting"}}{{else}}{{t "mirror.use.runs_title"}}{{end}}">
|
|
{{- if .Parked}}{{t "mirror.use.parked"}}
|
|
{{- else if and (eq .Wanted 0) (eq .Ready 0)}}{{t "mirror.use.none_running"}}
|
|
{{- else}}{{.Ready}}/{{.Wanted}}{{if .Starting}} +{{.Starting}}{{end}}{{end}}
|
|
{{- if .Unplaceable}} · {{t "mirror.use.no_room"}}{{end}}
|
|
{{- with .Nodes}} · {{range $i, $n := .}}{{if $i}}, {{end}}<span class="node" title="{{$n.Full}}">{{$n.Short}}</span>{{end}}{{end}}</span>{{end}}
|
|
</div>{{end}}
|
|
{{else}}<span class="opt">{{t "mirror.not_served"}}</span>{{end}}
|
|
</td>
|
|
<td class="actions">
|
|
{{if eq (printf "%s" .State) "failed"}}
|
|
{{/* Retry, not remove. A fetch that failed left nothing on disk, so the
|
|
remove would come back "the mirror holds nothing for this" — a button
|
|
whose only outcome is a confusing error. Starting the same download
|
|
again is what somebody looking at a failed row wants, and the mirror
|
|
allows it: only a download still running is refused. */}}
|
|
<form method="post" action="/admin/mirror/fetch" class="inline">
|
|
<input type="hidden" name="csrf" value="{{$.CSRF}}">
|
|
<input type="hidden" name="source" value="{{.Source}}">
|
|
<input type="hidden" name="model" value="{{.Model}}">
|
|
<input type="hidden" name="version" value="{{.Version}}">
|
|
<input type="hidden" name="file" value="{{.File}}">
|
|
<button type="submit" class="link">{{t "mirror.retry"}}</button>
|
|
</form>
|
|
{{else if eq (printf "%s" .State) "present"}}
|
|
<button type="button" class="link copy-url" data-url="{{.URL}}" title="{{.URL}}">{{t "mirror.copy_address"}}</button>
|
|
{{if and $.HasCatalog (not .UsedBy)}}{{if .Serve}}<a href="/admin/mirror">{{t "mirror.close"}}</a>
|
|
{{else}}<a href="/admin/mirror?serve={{.Model}}:{{.Version}}">{{t "mirror.serve"}}</a>{{end}}{{end}}
|
|
<form method="post" action="/admin/mirror/{{.Model}}/{{.Version}}/remove"
|
|
class="inline" data-confirm="{{if .UsedBy}}{{t "mirror.remove_confirm_in_use" .Model .Version}}{{else}}{{t "mirror.remove_confirm" .Model .Version}}{{end}}">
|
|
<input type="hidden" name="csrf" value="{{$.CSRF}}">
|
|
<button type="submit" class="link danger">{{t "mirror.remove"}}</button>
|
|
</form>
|
|
{{end}}
|
|
</td>
|
|
</tr>
|
|
{{if .Err}}<tr><td colspan="5" class="error-row">{{.Err}}</td></tr>{{end}}
|
|
{{with .Detail}}<tr class="held-detail"><td colspan="5">{{template "mirror_held_detail" dict "D" . "Row" $row0 "CSRF" $.CSRF}}</td></tr>{{end}}
|
|
|
|
{{with .Serve}}
|
|
<tr class="editing"><td colspan="5">
|
|
{{if .Refusal}}
|
|
{{/* Offered and then failing at placement is worse than not offered at all. */}}
|
|
<p class="error">{{t "serve.refusal" .Refusal}}</p>
|
|
{{else}}
|
|
{{if .Warning}}<p class="note error-row">{{.Warning}}.</p>{{end}}
|
|
<form method="post" action="/admin/mirror/serve" class="serve-form" data-engine="{{.Engine}}" data-free-gpus="{{.FreeGPUs}}">
|
|
<input type="hidden" name="csrf" value="{{$.CSRF}}">
|
|
<input type="hidden" name="source" value="{{.Source}}">
|
|
<input type="hidden" name="engine" value="{{.Engine}}">
|
|
<p class="sub">{{th "serve.serving" .Source .Engine}}
|
|
{{if .Files}}{{t "serve.whole_version" .Files}}{{end}}</p>
|
|
<div class="field-row">
|
|
<label>{{t "serve.name"}} <input type="text" name="name" value="{{.Model}}" required></label>
|
|
<label>{{t "serve.aliases"}} <span class="opt">{{t "serve.aliases_hint"}}</span>
|
|
<input type="text" name="aliases" placeholder="{{t "serve.aliases_example"}}"></label>
|
|
<label>{{t "serve.replicas"}} <input type="text" name="replicas" value="1" required></label>
|
|
<label>{{t "serve.runs"}}
|
|
<select name="mode">
|
|
<option value="always">{{t "mirror.mode.always"}}</option>
|
|
<option value="ondemand">{{t "serve.mode.ondemand"}}</option>
|
|
</select></label>
|
|
</div>
|
|
{{/* Reaches clients through /v1/models, which for most of them is the only
|
|
view of what this gateway serves. Nine names and nothing to choose
|
|
between them is a choice made by guesswork. */}}
|
|
<label>{{t "serve.description"}} <span class="opt">{{t "serve.description_hint"}}</span>
|
|
<input type="text" name="description"
|
|
placeholder="{{t "serve.description_example"}}"></label>
|
|
<div class="field-row">
|
|
<label>{{t "serve.context_length"}}
|
|
<input type="text" name="context_length" value="4096" required></label>
|
|
<label>{{t "serve.max_output"}} <span class="opt">{{t "serve.optional"}}</span>
|
|
<input type="text" name="max_output" value="1024"></label>
|
|
<label>{{t "serve.ctx_pool"}} <span class="opt">{{t "serve.ctx_pool_hint"}}</span>
|
|
<input type="text" name="ctx_size" value="{{.CtxPool}}"></label>
|
|
</div>
|
|
{{/* Where it runs is a choice with consequences, so it is a choice: the
|
|
GPU classes the fleet has, and the CPU for llama.cpp. A profile name
|
|
field stood here once, and reading as though naming a profile "gpu"
|
|
would put it on one, it put GGUF models on the CPU of GPU clusters. */}}
|
|
<div class="field-row">
|
|
<label>{{t "serve.placement"}}
|
|
<select name="placement">
|
|
{{range .Placements}}<option value="{{.Value}}"{{if .Selected}} selected{{end}}>{{.Label}}</option>{{end}}
|
|
</select></label>
|
|
<label>{{t "serve.gpus"}} <span class="opt">{{t "serve.gpus_hint"}}</span>
|
|
<input type="text" name="gpus" value="{{.GPUs}}" size="3"></label>
|
|
{{if eq .Engine "llamacpp"}}<label>{{t "serve.parallel"}} <span class="opt">{{t "serve.parallel_hint"}}</span>
|
|
<input type="text" name="parallel" value="{{.Parallel}}" size="3"></label>{{end}}
|
|
</div>
|
|
{{if .FreeGPUs}}<label class="cpu-ok"><input type="checkbox" name="cpu_ok" value="yes">
|
|
{{t "serve.cpu_ok" .FreeGPUs}}</label>{{end}}
|
|
<div class="field-row">
|
|
<label>{{t "serve.memory"}} <span class="opt">{{t "serve.memory_hint"}}</span>
|
|
<input type="text" name="min_memory_gb" value="{{.MinMemoryGB}}" required></label>
|
|
<label>{{t "serve.cpu_cores"}} <span class="opt">{{t "serve.cpu_cores_hint"}}</span>
|
|
<input type="text" name="cpu_cores" value="{{.CPUCores}}"></label>
|
|
<label>{{t "serve.selector"}} <span class="opt">{{t "serve.selector_hint"}}</span>
|
|
<input type="text" name="selector_key" placeholder="{{t "serve.selector_key_example"}}" size="8">
|
|
<input type="text" name="selector_value" placeholder="{{t "serve.selector_value_example"}}" size="10"></label>
|
|
</div>
|
|
<p class="sub serve-summary" hidden></p>
|
|
<button type="submit">{{t "serve.add"}}</button>
|
|
</form>
|
|
<script>
|
|
// What it will run as, in one line, kept up to date as the form changes:
|
|
// engine, where, how many cards, memory, context and how it is shared. The
|
|
// sentences are the ones the server describes a model with, in the page's
|
|
// language; fmt fills their %s and %d, numbered ones included.
|
|
(function () {
|
|
const f = document.currentScript.previousElementSibling;
|
|
if (!f || !f.classList.contains("serve-form")) return;
|
|
const T = {
|
|
cpu: {{t "serve.on_cpu"}}, gpu1: {{t "serve.on_gpu_one"}}, gpus: {{t "serve.on_gpus"}},
|
|
mem: {{t "serve.host_memory"}}, cores: {{t "serve.cores.other"}},
|
|
poolPar: {{t "serve.pool_parallel"}}, pool: {{t "serve.pool"}},
|
|
runAs: {{t "serve.will_run_as"}}, free: {{t "serve.free_gpus_warning"}},
|
|
};
|
|
const fmt = (s, ...a) => {
|
|
let i = 0;
|
|
return s.replace(/%(?:\[(\d+)\])?[sd]/g, (_, n) => { const k = n ? +n - 1 : i; i = k + 1; return String(a[k]); });
|
|
};
|
|
const out = f.querySelector(".serve-summary"), ok = f.querySelector(".cpu-ok");
|
|
const v = (n) => (f.elements[n] ? f.elements[n].value.trim() : "");
|
|
const engine = { llamacpp: "llama.cpp", vllm: "vLLM", sglang: "SGLang" }[f.dataset.engine] || f.dataset.engine;
|
|
function draw() {
|
|
const place = v("placement"), gpus = parseInt(v("gpus"), 10) || 1;
|
|
const onCPU = place === "cpu" || place === "";
|
|
let where = fmt(T.cpu, engine);
|
|
if (!onCPU) {
|
|
const cls = place.replace(/^gpu:/, "");
|
|
where = gpus > 1 ? fmt(T.gpus, engine, gpus, cls) : fmt(T.gpu1, engine, cls);
|
|
}
|
|
const parts = [where];
|
|
if (v("min_memory_gb")) parts.push(fmt(T.mem, v("min_memory_gb")));
|
|
if (onCPU && v("cpu_cores")) parts.push(fmt(T.cores, v("cpu_cores")));
|
|
const pool = parseInt(v("ctx_size"), 10), par = parseInt(v("parallel"), 10);
|
|
if (pool && par) parts.push(fmt(T.poolPar, pool, par, Math.floor(pool / par)));
|
|
else if (pool) parts.push(fmt(T.pool, pool));
|
|
out.textContent = fmt(T.runAs, parts.join(", "));
|
|
const warn = onCPU && +f.dataset.freeGpus > 0;
|
|
out.classList.toggle("error", warn);
|
|
if (warn) out.textContent += " " + fmt(T.free, f.dataset.freeGpus);
|
|
if (ok) ok.hidden = !onCPU;
|
|
out.hidden = false;
|
|
}
|
|
f.addEventListener("input", draw);
|
|
f.addEventListener("change", draw);
|
|
draw();
|
|
})();
|
|
</script>
|
|
<p class="note">{{t "serve.pool_note"}}</p>
|
|
<p class="note">{{t "serve.memory_note" .MemoryBasis}}</p>
|
|
{{end}}
|
|
</td></tr>
|
|
{{end}}
|
|
{{end}}
|
|
{{end}}
|
|
</table>
|
|
<p class="note">{{th "mirror.copy_note"}}</p>
|
|
<script>
|
|
(function () {
|
|
var copied = {{t "common.copied"}}, address = {{t "mirror.address_prompt"}};
|
|
document.addEventListener('click', function (e) {
|
|
var b = e.target.closest('.copy-url');
|
|
if (!b) return;
|
|
var done = function (t) { var o = b.textContent; b.textContent = t; setTimeout(function () { b.textContent = o; }, 1500); };
|
|
if (navigator.clipboard) {
|
|
navigator.clipboard.writeText(b.dataset.url).then(function () { done(copied); },
|
|
function () { window.prompt(address, b.dataset.url); });
|
|
} else { window.prompt(address, b.dataset.url); }
|
|
});
|
|
})();
|
|
</script>
|
|
{{else}}<p class="empty">{{if .HasRegistry}}{{th "mirror.empty_explore"}}{{else}}{{th "mirror.empty"}}{{end}}</p>{{end}}
|
|
</div>
|
|
{{end}}
|
|
{{end}}
|
|
|
|
{{if and (eq .Tab "explore") .HasRegistry}}
|
|
{{if not .Repo}}{{with .Explore}}
|
|
<nav class="pills" aria-label="{{t "explore.views_aria"}}">{{range .Views}}<a href="{{.Href}}"{{if .Active}} class="on" aria-current="page"{{end}}>{{.Label}}</a>{{end}}</nav>
|
|
{{if eq .View "adapters"}}
|
|
<p class="note">{{if .Of}}{{th "explore.adapters_for" .Of}}
|
|
<a href="/admin/mirror?tab=explore&view=adapters">{{t "explore.all_adapters"}}</a>{{else}}{{th "explore.adapters_any"}}{{end}}
|
|
{{t "explore.adapters_note"}}</p>
|
|
{{else}}
|
|
<div class="explore-filters">
|
|
<nav class="pills small" aria-label="{{t "explore.task_aria"}}">{{range .Tasks}}<a href="{{.Href}}"{{if .Active}} class="on" aria-current="true"{{end}}>{{.Label}}</a>{{end}}</nav>
|
|
<nav class="pills small" aria-label="{{t "explore.size_aria"}}">{{range .Sizes}}<a href="{{.Href}}"{{if .Active}} class="on" aria-current="true"{{end}}>{{.Label}}</a>{{end}}</nav>
|
|
</div>
|
|
{{end}}
|
|
{{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}}">
|
|
{{if .Of}}<input type="hidden" name="of" value="{{.Of}}">{{end}}
|
|
<input type="hidden" name="x" value="1">{{end}}
|
|
<input type="search" name="q" value="{{.Query}}" placeholder="{{t "explore.search_placeholder"}}" aria-label="{{t "explore.search_aria"}}">
|
|
<label class="check"><input type="checkbox" name="here" value="1" data-autosubmit{{if .Explore}}{{if .Explore.Here}} checked{{end}}{{end}}> {{t "explore.only_here"}}</label>
|
|
<button type="submit">{{t "explore.search"}}</button>
|
|
</form>
|
|
{{end}}
|
|
|
|
{{if .RegistryErr}}
|
|
{{/* Its own line, not the page's error: the registry is somebody else's server
|
|
and it being down says nothing about the mirror, which is what this page is
|
|
for and has to keep working when Hugging Face does not. */}}
|
|
<p class="note error-row">{{t "explore.registry_err" .RegistryErr}}</p>
|
|
{{end}}
|
|
|
|
{{if not .Repo}}{{with .Explore}}
|
|
{{with .Fit}}<p class="note">{{.}}</p>{{end}}
|
|
{{if .Rows}}
|
|
<table class="rows explore">
|
|
<tr><th>{{t "explore.col.repo"}}</th><th class="num">{{t "explore.col.size"}}</th><th>{{t "explore.col.arch"}}</th><th class="num">{{t "explore.col.context"}}</th>
|
|
<th class="num">{{t "explore.col.downloads"}}</th><th class="num">{{t "explore.col.likes"}}</th><th class="num">{{t "explore.col.updated"}}</th></tr>
|
|
{{range .Rows}}<tr class="explore-row">
|
|
<td><a href="{{.Href}}" class="open-repo" data-repo="{{.ID}}" aria-expanded="false"><code>{{.ID}}</code></a>
|
|
{{if .New}}<span class="tag new">{{t "explore.new"}}</span>{{end}}{{if .Gated.Is}}<span class="tag">{{t "explore.gated"}}</span>{{end}}
|
|
{{if or .BaseModel .License}}<span class="from" title="{{with .BaseModel}}{{t "explore.from" .}}{{end}}{{if and .BaseModel .License}} · {{end}}{{.License}}">{{with .BaseModel}}{{t "explore.from" .}}{{end}}{{if and .BaseModel .License}} · {{end}}{{.License}}</span>{{end}}</td>
|
|
<td class="num">{{.ParamsLabel}}</td>
|
|
<td>{{if .Architecture}}<span{{if not .Verdict.Servable}} class="cannot"{{end}} title="{{.Verdict.Reason}}">{{.Architecture}}{{if .Verdict.Checked}}{{if .Verdict.Servable}} ✓{{else}} ✗{{end}}{{end}}</span>{{else}}—{{end}}</td>
|
|
<td class="num">{{.ContextLabel}}</td>
|
|
<td class="num">{{compact .Downloads}}</td>
|
|
<td class="num">{{compact .Likes}}</td>
|
|
<td class="num">{{.Updated}}</td>
|
|
</tr>{{end}}
|
|
</table>
|
|
<script>
|
|
// A repository opens beneath its row rather than replacing the list: the
|
|
// list is what somebody is choosing from, and losing it to read one card
|
|
// means finding their place again for the next. The link still goes to the
|
|
// repository's own page when this does not run, or with a modifier key.
|
|
const exploreText = {
|
|
loading: {{t "explore.js.loading"}}, loadFailed: {{t "explore.js.load_failed"}},
|
|
starting: {{t "explore.js.starting"}}, refused: {{t "explore.js.refused"}},
|
|
follow: {{t "explore.js.follow"}}, started: {{t "explore.js.started"}},
|
|
unreachable: {{t "explore.js.unreachable"}},
|
|
};
|
|
document.querySelector("table.explore").addEventListener("click", async function (e) {
|
|
const a = e.target.closest("a.open-repo");
|
|
if (!a || e.metaKey || e.ctrlKey || e.shiftKey || e.button !== 0) return;
|
|
e.preventDefault();
|
|
const row = a.closest("tr");
|
|
let detail = row.nextElementSibling;
|
|
if (detail && detail.classList.contains("repo-detail")) {
|
|
detail.hidden = !detail.hidden;
|
|
a.setAttribute("aria-expanded", String(!detail.hidden));
|
|
row.classList.toggle("open", !detail.hidden);
|
|
return;
|
|
}
|
|
detail = document.createElement("tr");
|
|
detail.className = "repo-detail";
|
|
const td = document.createElement("td");
|
|
td.colSpan = row.children.length;
|
|
td.textContent = exploreText.loading;
|
|
detail.appendChild(td);
|
|
row.after(detail);
|
|
row.classList.add("open");
|
|
a.setAttribute("aria-expanded", "true");
|
|
try {
|
|
const resp = await fetch("/admin/mirror/repo?repo=" + encodeURIComponent(a.dataset.repo));
|
|
const text = await resp.text();
|
|
if (!resp.ok) { td.textContent = text.slice(0, 300); td.classList.add("error-row"); return; }
|
|
// Our own server's rendering of the repository, the card already
|
|
// stripped of anything that runs: the same HTML the full page shows.
|
|
td.innerHTML = text;
|
|
} catch (err) {
|
|
td.textContent = exploreText.loadFailed + " " + err;
|
|
}
|
|
});
|
|
|
|
// A fetch started from an opened repository answers where it was asked:
|
|
// the outcome under its button, and the mirror's table above brought up to
|
|
// date and followed until the download is done. A form post would reload
|
|
// the page, closing the repository and losing the list's place.
|
|
document.querySelector("table.explore").addEventListener("submit", async function (e) {
|
|
const form = e.target.closest('form[action="/admin/mirror/fetch"]');
|
|
if (!form) return;
|
|
e.preventDefault();
|
|
const btn = form.querySelector("button[type=submit]");
|
|
const was = btn.textContent;
|
|
btn.disabled = true;
|
|
btn.textContent = exploreText.starting;
|
|
let note = form.parentElement.querySelector(".fetch-note");
|
|
if (!note) {
|
|
note = document.createElement("div");
|
|
note.className = "fetch-note note";
|
|
form.after(note);
|
|
}
|
|
try {
|
|
const resp = await fetch(form.action, {
|
|
method: "POST", body: new URLSearchParams(new FormData(form)),
|
|
headers: { Accept: "application/json" },
|
|
});
|
|
const out = await resp.json().catch(() => ({ error: "(" + resp.status + ")" }));
|
|
if (!resp.ok) {
|
|
note.textContent = out.error || exploreText.refused;
|
|
note.classList.add("bad");
|
|
btn.disabled = false;
|
|
btn.textContent = was;
|
|
return;
|
|
}
|
|
note.classList.remove("bad");
|
|
note.textContent = "✓ " + out.message + " — ";
|
|
const link = document.createElement("a");
|
|
link.href = "/admin/mirror?tab=held";
|
|
link.textContent = exploreText.follow;
|
|
note.appendChild(link);
|
|
btn.textContent = exploreText.started;
|
|
} catch (err) {
|
|
note.textContent = exploreText.unreachable + " " + err;
|
|
note.classList.add("bad");
|
|
btn.disabled = false;
|
|
btn.textContent = was;
|
|
}
|
|
});
|
|
</script>
|
|
{{if .Hidden}}<p class="note">{{tn "explore.hidden" .Hidden}}
|
|
{{t "explore.untick"}}</p>{{end}}
|
|
<p class="note">{{t "explore.gated_note"}}</p>
|
|
{{else if not $.RegistryErr}}
|
|
<p class="empty">{{if $.Query}}{{if .Hidden}}{{tn "explore.empty.query_hidden" .Hidden $.Query}}{{else}}{{t "explore.empty.query" $.Query}}{{end}}
|
|
{{- else}}{{if .Hidden}}{{tn "explore.empty.hidden" .Hidden}}{{else}}{{t "explore.empty.nothing"}}{{end}}{{end}}</p>
|
|
{{end}}
|
|
{{end}}{{end}}
|
|
|
|
{{if .Repo}}{{template "mirror_repo" .}}{{end}}
|
|
{{end}}
|
|
|
|
{{if eq .Tab "fetch"}}
|
|
<p class="note">{{t "mirror.fetch.intro"}}</p>
|
|
<form method="post" action="/admin/mirror/fetch">
|
|
<input type="hidden" name="csrf" value="{{.CSRF}}">
|
|
<input type="hidden" name="tab" value="fetch">
|
|
<label>{{t "mirror.fetch.source"}}
|
|
<input type="text" name="source" required
|
|
placeholder="{{t "mirror.fetch.source_example"}}"></label>
|
|
<div class="field-row">
|
|
<label>{{t "mirror.fetch.model"}} <input type="text" name="model" required placeholder="{{t "mirror.fetch.model_example"}}"></label>
|
|
<label>{{t "mirror.fetch.version"}} <input type="text" name="version" required placeholder="2026-09"></label>
|
|
<label>{{t "mirror.fetch.file"}} <span class="opt">{{t "serve.optional"}}</span>
|
|
<input type="text" name="file" placeholder="{{t "mirror.fetch.file_hint"}}"></label>
|
|
</div>
|
|
<label class="inline-check"><input type="checkbox" name="force" value="1">
|
|
{{t "mirror.fetch.force"}} <span class="opt">{{t "mirror.fetch.force_hint"}}</span></label>
|
|
<button type="submit">{{t "mirror.fetch.start"}}</button>
|
|
</form>
|
|
<p class="note">{{t "mirror.fetch.note"}}</p>
|
|
{{end}}
|
|
|
|
<footer><span>llmgw {{.Version}}</span><span>{{stamp .Now}}</span>{{template "langs"}}</footer>
|
|
</div></body></html>{{end}}
|
|
|
|
{{/* One repository: its card and its files. The whole page when opened by
|
|
address, and the row beneath a listing entry when opened from the list. */}}
|
|
{{define "mirror_repo"}}
|
|
{{if not .Inline}}<p><a href="{{.Back}}">{{t "card.back"}}</a></p>
|
|
<h3>{{.Repo.ID}} <span class="opt">{{t "card.at" .Repo.ShortRevision}}</span></h3>{{end}}
|
|
<p class="repo-meta">
|
|
{{if .Inline}}<span>{{th "card.at_code" .Repo.ShortRevision}}</span>{{end}}
|
|
{{with .Repo.BaseModel}}<span>{{t "card.from"}} {{range $i, $b := .}}{{if $i}}, {{end}}<code>{{$b}}</code>{{end}}</span>{{end}}
|
|
{{with .Repo.License}}<span>{{t "card.license" .}}</span>{{end}}
|
|
{{with .Repo.Languages}}<span>{{join .}}</span>{{end}}
|
|
<span><a href="{{.RepoURL}}" rel="noopener noreferrer">{{t "card.on_registry"}}</a></span>
|
|
</p>
|
|
{{if .Card}}
|
|
<details class="model-card" open>
|
|
<summary>{{t "card.model_card"}}</summary>
|
|
<div class="card-body">{{.Card}}</div>
|
|
</details>
|
|
{{end}}
|
|
{{if .RepoSets}}
|
|
<table class="rows repo-files">
|
|
<tr><th>{{t "card.col.what"}}</th><th class="num">{{t "card.col.size"}}</th><th>{{t "card.col.checksum"}}</th><th></th></tr>
|
|
{{range .RepoSets}}<tr>
|
|
<td><code>{{.Name}}</code>
|
|
{{if .Shards}}<span class="opt">{{t "card.shards" .Shards}}</span>{{end}}
|
|
{{with .Companions}}<span class="opt" title="{{join .}}">{{t "card.companions" (len .)}}</span>{{end}}
|
|
{{if eq .Kind "adapter"}}<span class="tag new" title="{{t "card.adapter_title" .Arch}}">{{t "card.adapter" .Arch}}</span>
|
|
{{else if eq .Kind "model"}}<span class="opt">{{if .Arch}}{{t "card.model_arch" .Arch}}{{else}}{{t "card.model"}}{{end}}</span>{{end}}</td>
|
|
<td class="num">{{bytes .Bytes}}</td>
|
|
<td>{{with .SHA256}}<code title="{{.}}">{{slice . 0 12}}</code>{{else}}—{{end}}</td>
|
|
<td>
|
|
{{if not .Verdict.Servable}}
|
|
<span class="state state-failed" title="{{.Verdict.Reason}}">{{t "card.cannot_serve"}}</span>
|
|
<span class="clip-inline" title="{{.Verdict.Reason}}">{{.Verdict.Reason}}</span>
|
|
{{else}}
|
|
{{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}}
|
|
<input type="text" name="model" value="{{.Model}}" size="24">
|
|
<input type="text" name="version" value="{{.Version}}" size="10">
|
|
<button type="submit">{{t "card.fetch"}}</button>
|
|
</form>
|
|
{{end}}
|
|
</td>
|
|
</tr>{{end}}
|
|
</table>
|
|
{{if .PickleHidden}}<p class="note">{{tn "card.pickle_hidden" .PickleHidden}}</p>{{end}}
|
|
<p class="note">{{t "card.weights_note"}}</p>
|
|
<p class="note">{{th "card.pinned_note" .Repo.ShortRevision}}</p>
|
|
{{else}}
|
|
<p class="empty">{{if .PickleHidden}}{{tn "card.only_pickle" .PickleHidden .Repo.ID}}{{else}}{{t "card.no_weights" .Repo.ID}}{{end}}</p>
|
|
{{end}}
|
|
{{end}}
|
|
|
|
{{/* The card beneath a held version: what runs it, where, and how many. */}}
|
|
{{define "mirror_held_detail"}}{{$row := .Row}}{{$csrf := .CSRF}}{{$scopes := .D.Scopes}}
|
|
<div class="held-card">
|
|
<p class="repo-meta">
|
|
<span>{{if gt $row.Files 1}}{{tn "mirror.files" $row.Files}}{{else}}<code>{{$row.File}}</code>{{end}}</span>
|
|
<span>{{bytes $row.BytesDone}}</span>
|
|
{{with $row.SHA256}}<span translate="no">sha256 <code title="{{.}}">{{slice . 0 12}}</code></span>{{end}}
|
|
<span><code>{{$row.URL}}</code></span>
|
|
</p>
|
|
{{if not .D.Models}}
|
|
<p class="note">{{t "mirror.detail.not_in_catalog"}}
|
|
{{if $row.Serve}}{{else}}{{th "mirror.detail.use_serve"}}{{end}}</p>
|
|
{{end}}
|
|
{{range .D.Models}}
|
|
<section class="held-model">
|
|
<h3>{{.Name}}
|
|
{{with .View}}<span class="opt">{{t "mirror.detail.ready" .Ready .Desired}}{{if ne .Total .Ready}} · {{t "mirror.detail.in_all" .Total}}{{end}}</span>
|
|
{{if .Parked}}<span class="tag bad">{{t "mirror.use.parked"}}</span>{{else if .Unplaceable}}<span class="tag bad">{{t "mirror.detail.does_not_fit"}}</span>{{end}}{{end}}
|
|
</h3>
|
|
{{with .View.Reason}}<p class="note error-row">{{.}}</p>{{end}}
|
|
|
|
{{if .Replicas}}
|
|
<table class="rows compact replicas">
|
|
<tr><th>{{t "mirror.replica.col.replica"}}</th><th>{{t "mirror.replica.col.node"}}</th><th>{{t "mirror.replica.col.state"}}</th><th class="num">{{t "mirror.replica.col.age"}}</th><th class="num">CPU</th><th class="num">{{t "mirror.replica.col.memory"}}</th></tr>
|
|
{{range .Replicas}}{{$rep := .}}{{range $i, $s := .Nodes}}<tr>
|
|
<td>{{if eq $i 0}}<code>{{$rep.ID}}</code>{{end}}</td>
|
|
<td><a href="/admin/nodes/{{$s.Node}}">{{$s.Node}}</a>{{if $s.MemoryGB}} <span class="opt">{{$s.MemoryGB}} GB</span>{{end}}</td>
|
|
<td>{{if eq $i 0}}<span class="state state-{{if eq $rep.State "ready"}}ready{{else if eq $rep.State "failed"}}failed{{else}}starting{{end}}">{{$rep.StateText}}</span>
|
|
{{with $rep.Restart}}<span class="opt">{{t "mirror.replica.restart" .}}</span>{{end}}{{end}}</td>
|
|
<td class="num">{{if eq $i 0}}{{age $rep.Age}}{{end}}</td>
|
|
<td class="num">{{if $s.CPU}}{{t "mirror.replica.cores" (num $s.CPU 1)}}{{else}}—{{end}}</td>
|
|
<td class="num">{{if $s.MemBytes}}{{bytes $s.MemBytes}}{{else}}—{{end}}</td>
|
|
</tr>{{end}}{{end}}
|
|
</table>
|
|
{{else}}<p class="note">{{if eq .Mode "ondemand"}}{{t "mirror.detail.none_running_ondemand"}}{{else}}{{t "mirror.detail.none_running"}}{{end}}</p>{{end}}
|
|
|
|
{{if .Headroom}}
|
|
<p class="headroom">
|
|
{{.RoomText}}
|
|
{{with .PreemptText}}<br><span class="note">{{.}}</span>{{end}}
|
|
</p>
|
|
{{if and (not .Headroom.Limited) .Headroom.Blocked}}<details class="blocked"><summary>{{if .Headroom.More}}{{t "mirror.room.why_not_more"}}{{else}}{{t "mirror.room.why_not_another"}}{{end}}</summary>
|
|
<ul>{{range .Headroom.Blocked}}<li>{{.}}</li>{{end}}</ul></details>{{end}}
|
|
{{end}}
|
|
|
|
{{if not .Declared}}{{$mname := .Name}}
|
|
<div class="adapters">
|
|
<h4>{{t "mirror.adapters.heading"}} <a class="note" href="{{.FindAdapters}}">{{t "mirror.adapters.find"}}</a></h4>
|
|
{{if .Adapters}}<table class="compact rows">
|
|
<tr><th>{{t "mirror.adapters.asked_as"}}</th><th>{{t "mirror.adapters.file"}}</th><th class="num">{{t "mirror.adapters.scale"}}</th><th></th></tr>
|
|
{{range .Adapters}}{{$a := .}}<tr>
|
|
<td><code>{{.Name}}</code></td>
|
|
<td class="clip" title="{{.Source}}">{{.Source}}</td>
|
|
<td class="num">{{.EffectiveScale}}</td>
|
|
<td class="actions"><form method="post" action="/admin/mirror/adapters" class="inline">
|
|
<input type="hidden" name="csrf" value="{{$csrf}}">
|
|
<input type="hidden" name="model" value="{{$mname}}">
|
|
<input type="hidden" name="open" value="{{$row.Open}}">
|
|
<input type="hidden" name="op" value="remove">
|
|
<input type="hidden" name="name" value="{{$a.Name}}">
|
|
<button type="submit" class="link danger">{{t "mirror.remove"}}</button></form></td>
|
|
</tr>{{end}}</table>
|
|
{{else}}<p class="note">{{t "mirror.adapters.none"}}</p>{{end}}
|
|
{{if .AdapterChoices}}{{$m := .}}
|
|
<form method="post" action="/admin/mirror/adapters" class="adapter-form">
|
|
<input type="hidden" name="csrf" value="{{$csrf}}">
|
|
<input type="hidden" name="model" value="{{.Name}}">
|
|
<input type="hidden" name="open" value="{{$row.Open}}">
|
|
<input type="hidden" name="op" value="add">
|
|
<div class="field-row">
|
|
<label>{{t "mirror.adapters.adapter_file"}} <select name="source" required data-suggest-into="name">
|
|
{{range .AdapterChoices}}<option value="{{.URL}}" data-suggest="{{.Suggest}}">{{.Label}}</option>{{end}}
|
|
</select></label>
|
|
<label>{{t "mirror.adapters.asked_as"}} <input type="text" name="name" value="{{(index .AdapterChoices 0).Suggest}}" required></label>
|
|
<label>{{t "mirror.adapters.scale"}} <input type="text" name="scale" value="1" size="4"></label>
|
|
<button type="submit">{{t "mirror.adapters.attach"}}</button>
|
|
</div>
|
|
</form>{{else}}<p class="note">{{t "mirror.adapters.no_choice"}}</p>{{end}}
|
|
</div>
|
|
{{end}}
|
|
|
|
{{if .Declared}}
|
|
<p class="note">{{t "mirror.detail.declared"}}</p>
|
|
{{else}}
|
|
<form method="post" action="/admin/mirror/replicas" class="replicas-form">
|
|
<input type="hidden" name="csrf" value="{{$csrf}}">
|
|
<input type="hidden" name="model" value="{{.Name}}">
|
|
<input type="hidden" name="open" value="{{$row.Open}}">
|
|
<div class="field-row">
|
|
<label>{{t "mirror.form.min"}}
|
|
<input type="number" name="min" value="{{.Min}}" min="0"{{if .MostNow}} max="{{.MostNow}}"{{end}} required></label>
|
|
<label>{{t "mirror.form.max"}}
|
|
<input type="number" name="max" value="{{.Max}}" min="0" max="64" required></label>
|
|
<label>{{t "mirror.form.class"}}
|
|
<select name="class">
|
|
<option value=""{{if eq .Class ""}} selected{{end}}>{{if eq .Mode "ondemand"}}{{t "mirror.class.default" (t "mirror.class.opportunistic")}}{{else}}{{t "mirror.class.default" (t "mirror.class.standard")}}{{end}}</option>
|
|
<option value="critical"{{if eq .Class "critical"}} selected{{end}}>{{t "mirror.class.critical"}}</option>
|
|
<option value="standard"{{if eq .Class "standard"}} selected{{end}}>{{t "mirror.class.standard"}}</option>
|
|
<option value="opportunistic"{{if eq .Class "opportunistic"}} selected{{end}}>{{t "mirror.class.opportunistic"}}</option>
|
|
</select></label>
|
|
{{if $scopes}}{{$owner := .Owner}}<label>{{t "mirror.form.owner"}}
|
|
<select name="owner">
|
|
<option value=""{{if eq $owner ""}} selected{{end}}>{{t "mirror.form.shared"}}</option>
|
|
{{range $scopes}}<option value="{{.Key}}"{{if eq .Key $owner}} selected{{end}}>{{.Name}}</option>{{end}}
|
|
</select></label>{{end}}
|
|
</div>
|
|
<div class="field-row">
|
|
<label>{{t "mirror.form.memory"}} <span class="opt">{{t "mirror.form.per_replica"}}</span>
|
|
<input type="number" name="memory" value="{{.MemoryGB}}" min="0"></label>
|
|
<label>{{t "mirror.form.cores"}} <span class="opt">{{t "mirror.form.cores_hint"}}</span>
|
|
<input type="number" name="cores" value="{{.CPUCores}}" min="0"></label>
|
|
<button type="submit">{{t "common.save"}}</button>
|
|
</div>
|
|
<p class="note">{{if .MostNow}}{{if and .Headroom .Headroom.ByPreempting}}{{t "mirror.form.most_now_preempting" .MostNow}}{{else}}{{t "mirror.form.most_now" .MostNow}}{{end}} {{end}}{{t "mirror.form.note"}}</p>
|
|
</form>
|
|
{{end}}
|
|
</section>
|
|
{{end}}
|
|
</div>
|
|
{{end}}
|