revert: remove Hugging Face model integration

This commit is contained in:
Hermes Agent
2026-08-28 19:30:35 +10:00
parent bc47cf8152
commit 309947d353
7 changed files with 9 additions and 423 deletions
+1 -5
View File
@@ -83,8 +83,6 @@ The full validation scan covered 7,230 raw public variants: 604 were classified
Available downloads now combine Ollama's popular API response with the public `https://ollama.com/library` index and each public family tag page. This means models that are not currently popular and are not installed locally—such as `ornith-1.5:9b` and `ornith-1.5:35b`—are discoverable. The catalog refresh found 235 public family slugs and 7,230 raw variants during validation. The existing MLX exclusion and host-RAM fit filter still apply, so very large variants such as `ornith-1.5:397b` remain hidden when they cannot fit the detected host RAM. Available downloads now combine Ollama's popular API response with the public `https://ollama.com/library` index and each public family tag page. This means models that are not currently popular and are not installed locally—such as `ornith-1.5:9b` and `ornith-1.5:35b`—are discoverable. The catalog refresh found 235 public family slugs and 7,230 raw variants during validation. The existing MLX exclusion and host-RAM fit filter still apply, so very large variants such as `ornith-1.5:397b` remain hidden when they cannot fit the detected host RAM.
Model search can optionally query the public Hugging Face Hub API when **Search Hugging Face** is checked and the query contains at least two characters. Hugging Face results are labeled **Hugging Face**, show repository metadata such as pipeline, library, downloads, and likes, and include an **Open on Hugging Face** link. Repositories with GGUF files also show **Download GGUF**. The picker groups split GGUF shards into one complete selectable set, downloads all required files, checks available disk space, and imports the first shard into local Ollama with `ollama create` when Ollama shares the dashboard filesystem. Transformers/safetensors/FP8-only repositories remain viewable but are not falsely offered as Ollama downloads. For example, `Qwen/Qwen3.8-Flash-Next` is searchable, while `unsloth/Qwen3.8-Flash-Next-GGUF` exposes complete GGUF sets.
The Live runtime panel now shows overall CPU usage, logical CPU count, load averages, overall GPU utilization, and per-GPU VRAM usage. When multiple logical CPUs are detected, it expands into a scrollable responsive per-core grid. When multiple GPUs are detected, it expands into a responsive per-GPU grid showing utilization, VRAM used/free, temperature, and power when the driver reports them. The grids use auto-fit sizing and bounded scrolling so the panel scales to larger CPU and GPU counts without overflowing the dashboard. The Live runtime panel now shows overall CPU usage, logical CPU count, load averages, overall GPU utilization, and per-GPU VRAM usage. When multiple logical CPUs are detected, it expands into a scrollable responsive per-core grid. When multiple GPUs are detected, it expands into a responsive per-GPU grid showing utilization, VRAM used/free, temperature, and power when the driver reports them. The grids use auto-fit sizing and bounded scrolling so the panel scales to larger CPU and GPU counts without overflowing the dashboard.
@@ -158,9 +156,7 @@ The dashboard plugin API is mounted when the dashboard starts. Restart Hermes af
## Download storage ## Download storage
The plugin does not store normal Ollama model blobs in Hermes. It sends Ollama's native `POST /api/pull` request to the selected endpoint. Therefore a normal Ollama download goes to the Ollama instance shown in the job message, and the Ollama service owns the model storage location. The exact path is controlled by Ollama's `OLLAMA_MODELS` setting; common Linux service/user locations are `/usr/share/ollama/.ollama/models` and `~/.ollama/models`. Check the Ollama service environment on the target host to determine the authoritative path. The plugin does not store model blobs in Hermes. It sends Ollama's native `POST /api/pull` request to the selected endpoint. Therefore a download goes to the Ollama instance shown in the job message, and the Ollama service owns the model storage location. The exact path is controlled by Ollama's `OLLAMA_MODELS` setting; common Linux service/user locations are `/usr/share/ollama/.ollama/models` and `~/.ollama/models`. Check the Ollama service environment on the target host to determine the authoritative path.
Hugging Face downloads are separate: the checkbox enables Hub search, and **Download GGUF** downloads a selected complete GGUF file or shard set under the Hermes home `huggingface/<owner>/<repository>/` directory. The plugin revalidates the repository metadata and filename, checks free disk space when the Hub publishes sizes, and reports progress in the jobs panel. On a non-containerized host where Ollama is on the same filesystem, it writes a temporary Modelfile and runs `ollama create` to import the GGUF. If Ollama is remote or containerized with a different filesystem, the file is downloaded but automatic import is not attempted; the UI says so explicitly. Transformers, safetensors, and FP8-only repositories remain browseable through their Hugging Face link but are not treated as Ollama-compatible downloads.
## Security limits ## Security limits
+5 -43
View File
@@ -126,10 +126,9 @@
} }
function ModelCard(props) { function ModelCard(props) {
var model = props.model, installed = props.installed, busy = props.busy, action = props.action, onHuggingFace = props.onHuggingFace; var model = props.model, installed = props.installed, busy = props.busy, action = props.action;
var openState = React.useState(false), open = openState[0], setOpen = openState[1]; var openState = React.useState(false), open = openState[0], setOpen = openState[1];
var badges = []; var badges = [];
if (model.source === "huggingface") badges.push(h(Badge, { key: "huggingface", tone: "popular" }, "Hugging Face"));
if (installed && model.loaded) badges.push(h(Badge, { key: "loaded", tone: "live" }, "loaded")); if (installed && model.loaded) badges.push(h(Badge, { key: "loaded", tone: "live" }, "loaded"));
if (installed) badges.push(h(Badge, { key: "installed", tone: "installed" }, "installed")); if (installed) badges.push(h(Badge, { key: "installed", tone: "installed" }, "installed"));
if (!installed) badges.push(h(Badge, { key: "available", tone: "download" }, "available")); if (!installed) badges.push(h(Badge, { key: "available", tone: "download" }, "available"));
@@ -142,9 +141,7 @@
h("div", { className: "ollama-card-actions" }, h("div", { className: "ollama-card-actions" },
installed && h(Button, { disabled: !!busy, onClick: function () { action("redownload", model.name); } }, busy === model.name + ":redownload" ? "Updating…" : "Update / re-download"), installed && h(Button, { disabled: !!busy, onClick: function () { action("redownload", model.name); } }, busy === model.name + ":redownload" ? "Updating…" : "Update / re-download"),
installed && h(Button, { disabled: !!busy, className: "ollama-button danger", onClick: function () { action("delete", model.name); } }, busy === model.name + ":delete" ? "Removing…" : "Remove"), installed && h(Button, { disabled: !!busy, className: "ollama-button danger", onClick: function () { action("delete", model.name); } }, busy === model.name + ":delete" ? "Removing…" : "Remove"),
!installed && model.source === "huggingface" && model.hf_url && h("a", { className: "ollama-button secondary", href: model.hf_url, target: "_blank", rel: "noreferrer" }, "Open on Hugging Face"), !installed && h(Button, { disabled: !!busy, onClick: function () { action("pull", model.name); } }, busy === model.name + ":pull" ? "Downloading…" : "Download")
!installed && model.source === "huggingface" && onHuggingFace && h(Button, { disabled: !!busy, onClick: function () { onHuggingFace(model); } }, "Download GGUF"),
!installed && model.source !== "huggingface" && h(Button, { disabled: !!busy, onClick: function () { action("pull", model.name); } }, busy === model.name + ":pull" ? "Downloading…" : "Download")
) )
), ),
h("div", { className: "ollama-model-summary" }, h("div", { className: "ollama-model-summary" },
@@ -153,7 +150,7 @@
h("div", null, h("small", null, "Type"), h("strong", null, model.architecture || "Unknown")), h("div", null, h("small", null, "Type"), h("strong", null, model.architecture || "Unknown")),
h("div", null, h("small", null, "Parameters"), h("strong", null, model.parameter_size || "Unknown"), model.activated_parameter_size && h("small", { className: "ollama-activated-parameters" }, model.activated_parameter_size, " activated")) h("div", null, h("small", null, "Parameters"), h("strong", null, model.parameter_size || "Unknown"), model.activated_parameter_size && h("small", { className: "ollama-activated-parameters" }, model.activated_parameter_size, " activated"))
), ),
h("div", { className: "ollama-card-meta" }, h("span", null, (model.quantization || "Unknown") + " · " + (model.format || "Unknown")), model.hf_downloads != null && h("span", null, Number(model.hf_downloads).toLocaleString() + " downloads"), model.hf_likes != null && h("span", null, Number(model.hf_likes).toLocaleString() + " likes"), model.context_length && h("span", null, "Context " + Number(model.context_length).toLocaleString()), model.modified_at ? h("span", null, "Last updated " + fmtDate(model.modified_at)) : h("span", { className: "ollama-date-unavailable" }, "Last updated unavailable")), h("div", { className: "ollama-card-meta" }, h("span", null, (model.quantization || "Unknown") + " · " + (model.format || "Unknown")), model.context_length && h("span", null, "Context " + Number(model.context_length).toLocaleString()), model.modified_at ? h("span", null, "Last updated " + fmtDate(model.modified_at)) : h("span", { className: "ollama-date-unavailable" }, "Last updated unavailable")),
h("div", { className: "ollama-strengths" }, h("strong", null, "Excels at: "), (model.strengths || []).join(" · ")), h("div", { className: "ollama-strengths" }, h("strong", null, "Excels at: "), (model.strengths || []).join(" · ")),
installed && h(VariantTable, { model: model, action: action, busy: busy }), installed && h(VariantTable, { model: model, action: action, busy: busy }),
h(Button, { className: "ollama-details-toggle", onClick: function () { setOpen(!open); } }, open ? "Hide capability breakdown" : "Show capability breakdown"), h(Button, { className: "ollama-details-toggle", onClick: function () { setOpen(!open); } }, open ? "Hide capability breakdown" : "Show capability breakdown"),
@@ -162,7 +159,6 @@
h("h4", null, "Runtime estimate"), h("h4", null, "Runtime estimate"),
h("p", null, model.expected_ram_basis || "No estimate basis available.", " Actual memory varies with context length, KV cache, GPU offload, and concurrent requests."), h("p", null, model.expected_ram_basis || "No estimate basis available.", " Actual memory varies with context length, KV cache, GPU offload, and concurrent requests."),
h("div", { className: "ollama-detail-grid" }, h("div", { className: "ollama-detail-grid" },
h("span", null, "Source: ", h("strong", null, model.source_label || model.source || "Ollama")),
h("span", null, "Family: ", h("strong", null, model.family || "unknown")), h("span", null, "Family: ", h("strong", null, model.family || "unknown")),
h("span", null, "Digest: ", h("strong", null, model.digest ? model.digest.slice(0, 16) + "…" : "unknown")), h("span", null, "Digest: ", h("strong", null, model.digest ? model.digest.slice(0, 16) + "…" : "unknown")),
h("span", null, "Embedding: ", h("strong", null, model.embedding_length || "unknown")), h("span", null, "Embedding: ", h("strong", null, model.embedding_length || "unknown")),
@@ -511,30 +507,13 @@
var busyState = React.useState(""), busy = busyState[0], setBusy = busyState[1]; var busyState = React.useState(""), busy = busyState[0], setBusy = busyState[1];
var noticeState = React.useState(null), notice = noticeState[0], setNotice = noticeState[1]; var noticeState = React.useState(null), notice = noticeState[0], setNotice = noticeState[1];
var targetDialogState = React.useState(null), targetDialog = targetDialogState[0], setTargetDialog = targetDialogState[1]; var targetDialogState = React.useState(null), targetDialog = targetDialogState[0], setTargetDialog = targetDialogState[1];
var hfFileDialogState = React.useState(null), hfFileDialog = hfFileDialogState[0], setHfFileDialog = hfFileDialogState[1];
var hfSearchState = React.useState(false), includeHuggingFace = hfSearchState[0], setIncludeHuggingFace = hfSearchState[1];
var loadingState = React.useState(true), loading = loadingState[0], setLoading = loadingState[1]; var loadingState = React.useState(true), loading = loadingState[0], setLoading = loadingState[1];
var externalSearchState = React.useState([]), externalSearch = externalSearchState[0], setExternalSearch = externalSearchState[1];
var externalSearchBusyState = React.useState(false), externalSearchBusy = externalSearchBusyState[0], setExternalSearchBusy = externalSearchBusyState[1];
var loadSequence = React.useRef(0); var loadSequence = React.useRef(0);
var searchSequence = React.useRef(0);
function load() { function load() {
var sequence = ++loadSequence.current; var sequence = ++loadSequence.current;
return fetchJSON(API + "/status").then(function (value) { if (sequence !== loadSequence.current) return value; setData(value); setLoading(false); return value; }).catch(function (err) { if (sequence === loadSequence.current) { setNotice({ error: err.message || String(err) }); setLoading(false); } }); return fetchJSON(API + "/status").then(function (value) { if (sequence !== loadSequence.current) return value; setData(value); setLoading(false); return value; }).catch(function (err) { if (sequence === loadSequence.current) { setNotice({ error: err.message || String(err) }); setLoading(false); } });
} }
React.useEffect(function () { load(); var timer = setInterval(load, 5000); return function () { clearInterval(timer); }; }, []); React.useEffect(function () { load(); var timer = setInterval(load, 5000); return function () { clearInterval(timer); }; }, []);
React.useEffect(function () {
var term = query.trim();
var sequence = ++searchSequence.current;
if (term.length < 2 || !includeHuggingFace) { setExternalSearch([]); setExternalSearchBusy(false); return; }
setExternalSearchBusy(true);
var timer = setTimeout(function () {
fetchJSON(API + "/catalog/search?q=" + encodeURIComponent(term) + "&limit=30&include_huggingface=true").then(function (value) {
if (sequence === searchSequence.current) setExternalSearch(value.huggingface_results || []);
}).catch(function () { if (sequence === searchSequence.current) setExternalSearch([]); }).finally(function () { if (sequence === searchSequence.current) setExternalSearchBusy(false); });
}, 250);
return function () { clearTimeout(timer); };
}, [query, includeHuggingFace]);
function action(kind, name, selectedTarget) { function action(kind, name, selectedTarget) {
if (kind === "delete" && !window.confirm("Remove " + name + " from Ollama?")) return; if (kind === "delete" && !window.confirm("Remove " + name + " from Ollama?")) return;
if ((kind === "pull" || kind === "redownload") && !selectedTarget) { if ((kind === "pull" || kind === "redownload") && !selectedTarget) {
@@ -547,16 +526,6 @@
fetchJSON(API + (kind === "delete" ? "/model" : "/" + kind), { method: kind === "delete" ? "DELETE" : "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: name, target: selectedTarget || "local" }) }).then(function (result) { setNotice({ ok: result.message || "Action started." }); load(); }).catch(function (err) { setNotice({ error: err.message || String(err) }); }).finally(function () { setBusy(""); }); fetchJSON(API + (kind === "delete" ? "/model" : "/" + kind), { method: kind === "delete" ? "DELETE" : "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: name, target: selectedTarget || "local" }) }).then(function (result) { setNotice({ ok: result.message || "Action started." }); load(); }).catch(function (err) { setNotice({ error: err.message || String(err) }); }).finally(function () { setBusy(""); });
} }
function refreshCatalog() { setBusy("catalog"); setNotice(null); fetchJSON(API + "/catalog/refresh", { method: "POST" }).then(function (result) { setNotice({ ok: "Catalog refreshed: " + result.count + " models." }); load(); }).catch(function (err) { setNotice({ error: err.message || String(err) }); }).finally(function () { setBusy(""); }); } function refreshCatalog() { setBusy("catalog"); setNotice(null); fetchJSON(API + "/catalog/refresh", { method: "POST" }).then(function (result) { setNotice({ ok: "Catalog refreshed: " + result.count + " models." }); load(); }).catch(function (err) { setNotice({ error: err.message || String(err) }); }).finally(function () { setBusy(""); }); }
function openHuggingFaceFiles(model) {
if (!model || !model.name) return;
setBusy("hf-files"); setNotice(null); setHfFileDialog({ model: model, files: null, import_supported: false });
fetchJSON(API + "/huggingface/files?repo_id=" + encodeURIComponent(model.name)).then(function (value) { setHfFileDialog({ model: model, files: value.files || [], import_supported: !!value.import_supported }); }).catch(function (err) { setHfFileDialog(null); setNotice({ error: err.message || String(err) }); }).finally(function () { setBusy(""); });
}
function downloadHuggingFaceFile(model, file) {
if (!model || !file || !file.filename) return;
setBusy("hf-download"); setNotice(null);
fetchJSON(API + "/huggingface/download", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ repo_id: model.name, filename: file.filename }) }).then(function (result) { setHfFileDialog(null); setNotice({ ok: result.message || "Hugging Face GGUF download started." }); load(); }).catch(function (err) { setNotice({ error: err.message || String(err) }); }).finally(function () { setBusy(""); });
}
var baseModels = data ? (tab === "installed" ? data.models || [] : tab === "popular" ? data.popular || [] : tab === "catalog" ? (showOversized ? data.catalog_all || data.catalog || [] : data.catalog || []) : []) : []; var baseModels = data ? (tab === "installed" ? data.models || [] : tab === "popular" ? data.popular || [] : tab === "catalog" ? (showOversized ? data.catalog_all || data.catalog || [] : data.catalog || []) : []) : [];
var models = baseModels; var models = baseModels;
if (tab === "catalog") { if (tab === "catalog") {
@@ -574,10 +543,6 @@
if (catalogSort === "name") return String(a.name).localeCompare(String(b.name)); if (catalogSort === "name") return String(a.name).localeCompare(String(b.name));
return (Number(a.popularity_rank) || 999999) - (Number(b.popularity_rank) || 999999); return (Number(a.popularity_rank) || 999999) - (Number(b.popularity_rank) || 999999);
}); });
if (query.trim().length >= 2 && externalSearch.length) {
var existingNames = new Set(models.map(function (model) { return model.name; }));
models = models.concat(externalSearch.filter(function (model) { return !existingNames.has(model.name); }));
}
} }
var needle = query.toLowerCase().trim(); if (needle) models = models.filter(function (model) { return (model.name + " " + model.family + " " + (model.strengths || []).join(" ") + " " + (model.capabilities || []).join(" ")).toLowerCase().indexOf(needle) >= 0; }); var needle = query.toLowerCase().trim(); if (needle) models = models.filter(function (model) { return (model.name + " " + model.family + " " + (model.strengths || []).join(" ") + " " + (model.capabilities || []).join(" ")).toLowerCase().indexOf(needle) >= 0; });
var catalogCapabilities = data && data.catalog_filter_options ? data.catalog_filter_options.capabilities || [] : []; var catalogCapabilities = data && data.catalog_filter_options ? data.catalog_filter_options.capabilities || [] : [];
@@ -603,20 +568,17 @@
h("label", { className: "ollama-catalog-checkbox ollama-catalog-memory-bypass", title: "This only bypasses the catalog display filter; loading remains protected by the 95% RAM safety guard." }, h("input", { type: "checkbox", checked: showOversized, onChange: function (event) { setShowOversized(event.target.checked); } }), h("span", null, "Show models above estimated RAM")) h("label", { className: "ollama-catalog-checkbox ollama-catalog-memory-bypass", title: "This only bypasses the catalog display filter; loading remains protected by the 95% RAM safety guard." }, h("input", { type: "checkbox", checked: showOversized, onChange: function (event) { setShowOversized(event.target.checked); } }), h("span", null, "Show models above estimated RAM"))
); );
var browseToolbar = tab !== "chat" && h("div", { className: "ollama-browse-row" }, var browseToolbar = tab !== "chat" && h("div", { className: "ollama-browse-row" },
h("input", { className: "ollama-search", value: query, placeholder: includeHuggingFace ? "Search Ollama + Hugging Face models…" : "Search Ollama models…", onChange: function (event) { setQuery(event.target.value); } }), h("input", { className: "ollama-search", value: query, placeholder: "Search models, capabilities, or strengths…", onChange: function (event) { setQuery(event.target.value); } }),
tab === "catalog" && h("label", { className: "ollama-catalog-checkbox ollama-huggingface-checkbox", title: "Search the public Hugging Face Hub in addition to Ollama." }, h("input", { type: "checkbox", checked: includeHuggingFace, onChange: function (event) { setIncludeHuggingFace(event.target.checked); } }), h("span", null, "Search Hugging Face")),
externalSearchBusy && h("small", { className: "ollama-search-status" }, "Searching Hugging Face…"),
catalogControls catalogControls
); );
return h("main", { className: "ollama-page" }, h("header", { className: "ollama-hero" }, h("div", null, h("div", { className: "ollama-eyebrow" }, "LOCAL MODEL OPERATIONS"), h("h1", null, "Ollama Models"), h("p", null, "Inspect, chat with, download, update, and remove models from the local Ollama runtime.")), h("div", { className: "ollama-health" }, h(Badge, { tone: data && data.ollama && data.ollama.available ? "live" : "danger" }, data && data.ollama && data.ollama.available ? "Ollama online" : "Ollama unavailable"), data && data.ollama && h("span", null, "v" + (data.ollama.version || "unknown")), h(Button, { disabled: busy === "catalog", onClick: refreshCatalog }, busy === "catalog" ? "Refreshing…" : "Refresh catalog")), h(ConnectionPanel, { data: data, reload: load })), return h("main", { className: "ollama-page" }, h("header", { className: "ollama-hero" }, h("div", null, h("div", { className: "ollama-eyebrow" }, "LOCAL MODEL OPERATIONS"), h("h1", null, "Ollama Models"), h("p", null, "Inspect, chat with, download, update, and remove models from the local Ollama runtime.")), h("div", { className: "ollama-health" }, h(Badge, { tone: data && data.ollama && data.ollama.available ? "live" : "danger" }, data && data.ollama && data.ollama.available ? "Ollama online" : "Ollama unavailable"), data && data.ollama && h("span", null, "v" + (data.ollama.version || "unknown")), h(Button, { disabled: busy === "catalog", onClick: refreshCatalog }, busy === "catalog" ? "Refreshing…" : "Refresh catalog")), h(ConnectionPanel, { data: data, reload: load })),
targetDialog && h("div", { className: "ollama-target-modal" }, h("div", { className: "ollama-target-card" }, h("h3", null, "Where should " + targetDialog.name + " be downloaded?"), h("p", null, "Both local and remote Ollama instances are online. Choose the destination for this model."), targetDialog.targets.map(function (item) { return h(Button, { key: item.kind, onClick: function () { var chosen = targetDialog; setTargetDialog(null); action(chosen.kind, chosen.name, item.kind); } }, (item.kind || "local").toUpperCase(), " · ", item.url, " · v", item.version, " · ", item.models, " models"); }), h(Button, { className: "secondary", onClick: function () { setTargetDialog(null); } }, "Cancel"))), targetDialog && h("div", { className: "ollama-target-modal" }, h("div", { className: "ollama-target-card" }, h("h3", null, "Where should " + targetDialog.name + " be downloaded?"), h("p", null, "Both local and remote Ollama instances are online. Choose the destination for this model."), targetDialog.targets.map(function (item) { return h(Button, { key: item.kind, onClick: function () { var chosen = targetDialog; setTargetDialog(null); action(chosen.kind, chosen.name, item.kind); } }, (item.kind || "local").toUpperCase(), " · ", item.url, " · v", item.version, " · ", item.models, " models"); }), h(Button, { className: "secondary", onClick: function () { setTargetDialog(null); } }, "Cancel"))),
hfFileDialog && h("div", { className: "ollama-target-modal" }, h("div", { className: "ollama-target-card ollama-huggingface-file-card" }, h("h3", null, "Download GGUF from ", hfFileDialog.model.name), h("p", null, hfFileDialog.import_supported ? "Choose a GGUF file. It will be downloaded and imported into the local Ollama runtime." : "Choose a GGUF file. It will be downloaded to Hermes storage; automatic Ollama import is unavailable in this deployment."), hfFileDialog.files === null ? h("p", null, "Loading repository files…") : hfFileDialog.files.length ? hfFileDialog.files.map(function (file) { return h("div", { className: "ollama-huggingface-file-row", key: file.filename }, h("span", null, h("strong", null, file.filename), h("small", null, (file.size_label || "Unknown size") + (file.split ? " · complete " + file.file_count + "-shard set" : ""))), h(Button, { disabled: !!busy, onClick: function () { downloadHuggingFaceFile(hfFileDialog.model, file); } }, busy === "hf-download" ? "Starting…" : (file.split ? "Download set" : "Download"))); }) : h("p", null, "This repository has no compatible GGUF files. Open the repository to inspect Transformers, safetensors, or other formats."), h("div", { className: "ollama-huggingface-file-actions" }, h("a", { className: "ollama-button secondary", href: hfFileDialog.model.hf_url, target: "_blank", rel: "noreferrer" }, "Open repository"), h(Button, { className: "secondary", onClick: function () { setHfFileDialog(null); } }, "Cancel")))),
notice && h("div", { className: "ollama-notice " + (notice.error ? "error" : notice.warning ? "warning" : "ok") }, notice.error || notice.warning || notice.ok), notice && h("div", { className: "ollama-notice " + (notice.error ? "error" : notice.warning ? "warning" : "ok") }, notice.error || notice.warning || notice.ok),
h("section", { className: "ollama-toolbar" }, h("div", { className: "ollama-nav-row" }, navTabs, h("div", { className: "ollama-toolbar-disk" }, h("span", null, "Disk"), h("strong", null, disk.used_percent == null ? "n/a" : Number(disk.used_percent).toFixed(1) + "%"), h("small", null, disk.available ? fmtBytes(disk.free_bytes) + " free" : "Unavailable"))), browseToolbar), h("section", { className: "ollama-toolbar" }, h("div", { className: "ollama-nav-row" }, navTabs, h("div", { className: "ollama-toolbar-disk" }, h("span", null, "Disk"), h("strong", null, disk.used_percent == null ? "n/a" : Number(disk.used_percent).toFixed(1) + "%"), h("small", null, disk.available ? fmtBytes(disk.free_bytes) + " free" : "Unavailable"))), browseToolbar),
tab !== "chat" && h("div", { className: "ollama-info-strip" }, h("span", null, data && data.models ? data.models.filter(function (m) { return m.loaded; }).length + " currently loaded" : "Loading runtime state…"), h("span", null, "Catalog checked " + (data && data.catalog_updated_at ? fmtDate(data.catalog_updated_at) : "not yet")), h("span", null, "Next daily check " + (data && data.next_catalog_refresh ? fmtDate(data.next_catalog_refresh) : "01:00 Melbourne time") + " (1:00 AM Melbourne time)")), tab !== "chat" && h("div", { className: "ollama-info-strip" }, h("span", null, data && data.models ? data.models.filter(function (m) { return m.loaded; }).length + " currently loaded" : "Loading runtime state…"), h("span", null, "Catalog checked " + (data && data.catalog_updated_at ? fmtDate(data.catalog_updated_at) : "not yet")), h("span", null, "Next daily check " + (data && data.next_catalog_refresh ? fmtDate(data.next_catalog_refresh) : "01:00 Melbourne time") + " (1:00 AM Melbourne time)")),
tab === "chat" && h(ChatPanel, { models: data && data.models ? data.models : [], refresh: load }), tab === "chat" && h(ChatPanel, { models: data && data.models ? data.models : [], refresh: load }),
tab === "popular" && h("p", { className: "ollama-popular-note" }, "Popular is limited to models with known size and RAM estimates at or below the detected system RAM (" + (data && data.popular_filter && data.popular_filter.max_expected_ram_gib ? data.popular_filter.max_expected_ram_gib + " GiB" : "detecting…") + "). Oversized families are represented by a smaller fitting variant when available."), jobs.length > 0 && h("section", { className: "ollama-jobs" }, jobs.map(function (job) { return h("div", { key: job.id }, h("strong", null, job.action + " · " + job.name + " · " + (job.target || "local") + (job.endpoint ? " · " + job.endpoint : "")), h("span", null, job.percent == null ? job.status : job.percent + "%")); })), tab === "popular" && h("p", { className: "ollama-popular-note" }, "Popular is limited to models with known size and RAM estimates at or below the detected system RAM (" + (data && data.popular_filter && data.popular_filter.max_expected_ram_gib ? data.popular_filter.max_expected_ram_gib + " GiB" : "detecting…") + "). Oversized families are represented by a smaller fitting variant when available."), jobs.length > 0 && h("section", { className: "ollama-jobs" }, jobs.map(function (job) { return h("div", { key: job.id }, h("strong", null, job.action + " · " + job.name + " · " + (job.target || "local") + (job.endpoint ? " · " + job.endpoint : "")), h("span", null, job.percent == null ? job.status : job.percent + "%")); })),
tab !== "chat" && loading && h(Empty, null, "Loading local Ollama inventory…"), tab !== "chat" && !loading && !models.length && h(Empty, null, tab === "installed" ? "No local models found." : tab === "popular" ? "No popular catalog entries available." : query.trim().length >= 2 ? "No Ollama or Hugging Face models matched this search." : "No catalog entries available. Try Refresh catalog."), tab !== "chat" && h("section", { className: "ollama-grid" }, models.map(function (model) { return h(ModelCard, { key: model.source + ":" + model.name, model: model, installed: tab === "installed" || !!model.installed, busy: busy, action: action, onHuggingFace: openHuggingFaceFiles }); })) tab !== "chat" && loading && h(Empty, null, "Loading local Ollama inventory…"), tab !== "chat" && !loading && !models.length && h(Empty, null, tab === "installed" ? "No local models found." : tab === "popular" ? "No popular catalog entries available." : "No catalog entries available. Try Refresh catalog."), tab !== "chat" && h("section", { className: "ollama-grid" }, models.map(function (model) { return h(ModelCard, { key: model.name, model: model, installed: tab === "installed" || !!model.installed, busy: busy, action: action }); }))
); );
} }
registry.register("ollama-manager", Page); registry.register("ollama-manager", Page);
+1 -1
View File
@@ -8,7 +8,7 @@
@media(max-width:600px){.ollama-runtime-grid{grid-template-columns:1fr}.ollama-chat-model{display:block}.ollama-chat-model select{width:100%;margin-bottom:8px}.ollama-message.user{margin-left:0}.ollama-message.assistant{margin-right:0}}.ollama-connection-panel{min-width:0;max-width:620px;width:100%;box-sizing:border-box;container-type:inline-size;margin-top:14px;padding:12px;border:1px solid rgba(164,211,199,.2);border-radius:10px;background:rgba(10,31,28,.72);box-shadow:0 8px 24px rgba(0,0,0,.12)}.ollama-connection-heading{display:flex;justify-content:space-between;gap:10px;color:#d7ebe5}.ollama-connection-heading strong{font-size:12px}.ollama-connection-heading small{display:block;margin-top:3px;color:#8fa9a4;font-size:10px}.ollama-connection-form{display:grid;grid-template-columns:auto minmax(0,1fr) auto auto;gap:6px;align-items:stretch;margin-top:9px}.ollama-connection-role,.ollama-connection-input{box-sizing:border-box;min-width:0;width:100%;height:36px;border:1px solid rgba(155,205,194,.28);border-radius:6px;background:#102d29;color:#e8f2ef;padding:7px;font:inherit;font-size:11px}.ollama-connection-input{overflow:hidden;text-overflow:ellipsis}.ollama-connection-form .ollama-button{height:36px;min-width:0;padding:7px 9px;white-space:nowrap}.ollama-connection-result{margin-top:7px;font-size:10px}.ollama-connection-result.ok{color:#9af1c7}.ollama-connection-result.error{color:#ffb1b1}.ollama-connection-list{display:grid;gap:5px;margin-top:8px}.ollama-connection-row{display:flex;align-items:center;gap:7px;width:100%;border:0;border-top:1px solid rgba(164,211,199,.1);padding:7px 0;background:none;color:#c5ddd7;text-align:left;cursor:pointer;font:inherit}.ollama-connection-row span:nth-child(2){display:flex;flex-direction:column;gap:2px;min-width:0}.ollama-connection-row strong{font-size:10px}.ollama-connection-row small{color:#8fa9a4;font-size:9px;overflow-wrap:anywhere}.ollama-connection-dot{width:7px;height:7px;border-radius:50%;background:#b36d6d;flex:0 0 auto}.ollama-connection-dot.online{background:#75d2b7;box-shadow:0 0 8px rgba(117,210,183,.55)}.ollama-connection-row-main{display:flex;align-items:center;gap:7px;flex:1;min-width:0;border:0;padding:0;background:none;color:inherit;text-align:left;cursor:pointer;font:inherit}.ollama-connection-remove{flex:0 0 auto;padding:5px 7px;font-size:9px}.ollama-connection-row-main>span:nth-child(2){display:flex;flex-direction:column;gap:2px;min-width:0}.ollama-connection-row-main strong{font-size:10px}.ollama-connection-row-main small{color:#8fa9a4;font-size:9px;overflow-wrap:anywhere} @media(max-width:600px){.ollama-runtime-grid{grid-template-columns:1fr}.ollama-chat-model{display:block}.ollama-chat-model select{width:100%;margin-bottom:8px}.ollama-message.user{margin-left:0}.ollama-message.assistant{margin-right:0}}.ollama-connection-panel{min-width:0;max-width:620px;width:100%;box-sizing:border-box;container-type:inline-size;margin-top:14px;padding:12px;border:1px solid rgba(164,211,199,.2);border-radius:10px;background:rgba(10,31,28,.72);box-shadow:0 8px 24px rgba(0,0,0,.12)}.ollama-connection-heading{display:flex;justify-content:space-between;gap:10px;color:#d7ebe5}.ollama-connection-heading strong{font-size:12px}.ollama-connection-heading small{display:block;margin-top:3px;color:#8fa9a4;font-size:10px}.ollama-connection-form{display:grid;grid-template-columns:auto minmax(0,1fr) auto auto;gap:6px;align-items:stretch;margin-top:9px}.ollama-connection-role,.ollama-connection-input{box-sizing:border-box;min-width:0;width:100%;height:36px;border:1px solid rgba(155,205,194,.28);border-radius:6px;background:#102d29;color:#e8f2ef;padding:7px;font:inherit;font-size:11px}.ollama-connection-input{overflow:hidden;text-overflow:ellipsis}.ollama-connection-form .ollama-button{height:36px;min-width:0;padding:7px 9px;white-space:nowrap}.ollama-connection-result{margin-top:7px;font-size:10px}.ollama-connection-result.ok{color:#9af1c7}.ollama-connection-result.error{color:#ffb1b1}.ollama-connection-list{display:grid;gap:5px;margin-top:8px}.ollama-connection-row{display:flex;align-items:center;gap:7px;width:100%;border:0;border-top:1px solid rgba(164,211,199,.1);padding:7px 0;background:none;color:#c5ddd7;text-align:left;cursor:pointer;font:inherit}.ollama-connection-row span:nth-child(2){display:flex;flex-direction:column;gap:2px;min-width:0}.ollama-connection-row strong{font-size:10px}.ollama-connection-row small{color:#8fa9a4;font-size:9px;overflow-wrap:anywhere}.ollama-connection-dot{width:7px;height:7px;border-radius:50%;background:#b36d6d;flex:0 0 auto}.ollama-connection-dot.online{background:#75d2b7;box-shadow:0 0 8px rgba(117,210,183,.55)}.ollama-connection-row-main{display:flex;align-items:center;gap:7px;flex:1;min-width:0;border:0;padding:0;background:none;color:inherit;text-align:left;cursor:pointer;font:inherit}.ollama-connection-remove{flex:0 0 auto;padding:5px 7px;font-size:9px}.ollama-connection-row-main>span:nth-child(2){display:flex;flex-direction:column;gap:2px;min-width:0}.ollama-connection-row-main strong{font-size:10px}.ollama-connection-row-main small{color:#8fa9a4;font-size:9px;overflow-wrap:anywhere}
@container (max-width: 460px){.ollama-connection-form{grid-template-columns:minmax(0,1fr) minmax(0,1fr)}.ollama-connection-role,.ollama-connection-input{grid-column:span 1}.ollama-connection-form .ollama-button{width:100%}} @container (max-width: 460px){.ollama-connection-form{grid-template-columns:minmax(0,1fr) minmax(0,1fr)}.ollama-connection-role,.ollama-connection-input{grid-column:span 1}.ollama-connection-form .ollama-button{width:100%}}
.ollama-target-modal{position:fixed;inset:0;z-index:20;display:flex;align-items:center;justify-content:center;padding:20px;background:rgba(4,15,14,.72)}.ollama-target-card{display:grid;gap:10px;max-width:560px;width:100%;padding:20px;border:1px solid rgba(141,210,193,.38);border-radius:12px;background:#102d29;box-shadow:0 14px 50px rgba(0,0,0,.35)}.ollama-target-card h3{margin:0;color:#effcf8}.ollama-target-card p{margin:0;color:#a5bfba;font-size:12px}.ollama-target-card .ollama-button{text-align:left}.ollama-huggingface-file-card{max-height:min(720px,90vh);overflow:auto}.ollama-huggingface-file-row{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:9px 0;border-top:1px solid rgba(164,211,199,.14)}.ollama-huggingface-file-row span{display:grid;gap:3px;min-width:0}.ollama-huggingface-file-row strong{overflow-wrap:anywhere;font-size:12px}.ollama-huggingface-file-row small,.ollama-search-status{color:#8fa9a4;font-size:11px}.ollama-huggingface-file-actions{display:flex;gap:8px;flex-wrap:wrap}@media(max-width:900px){.ollama-connection-panel{min-width:0;max-width:none}.ollama-connection-form{flex-wrap:wrap}.ollama-connection-input{min-width:160px}} .ollama-target-modal{position:fixed;inset:0;z-index:20;display:flex;align-items:center;justify-content:center;padding:20px;background:rgba(4,15,14,.72)}.ollama-target-card{display:grid;gap:10px;max-width:560px;width:100%;padding:20px;border:1px solid rgba(141,210,193,.38);border-radius:12px;background:#102d29;box-shadow:0 14px 50px rgba(0,0,0,.35)}.ollama-target-card h3{margin:0;color:#effcf8}.ollama-target-card p{margin:0;color:#a5bfba;font-size:12px}.ollama-target-card .ollama-button{text-align:left}@media(max-width:900px){.ollama-connection-panel{min-width:0;max-width:none}.ollama-connection-form{flex-wrap:wrap}.ollama-connection-input{min-width:160px}}
.ollama-catalog-controls{display:grid;grid-template-columns:repeat(3,minmax(130px,1fr));gap:8px;align-items:end;margin-top:0;padding:10px;border:1px solid rgba(164,211,199,.16);border-radius:10px;background:rgba(10,31,28,.55)}.ollama-catalog-controls label{display:flex;flex-direction:column;gap:5px;color:#a5bfba;font-size:10px;text-transform:uppercase;letter-spacing:.06em}.ollama-catalog-checkbox{display:flex!important;flex-direction:row!important;align-items:center;gap:8px;grid-column:1 / -1;padding:8px 4px;color:#b8ead9!important;text-transform:none!important;letter-spacing:normal!important;cursor:pointer}.ollama-catalog-checkbox input{width:15px;height:15px;margin:0;accent-color:#75d2b7}.ollama-catalog-checkbox span{font-size:11px}.ollama-catalog-memory-bypass{color:#ffd89a!important;background:rgba(142,90,25,.12);border-radius:7px}.ollama-catalog-select{min-width:145px;border:1px solid rgba(155,205,194,.28);border-radius:7px;background:#102d29;color:#e8f2ef;padding:8px;font:inherit;font-size:11px;text-transform:none;letter-spacing:normal} .ollama-catalog-controls{display:grid;grid-template-columns:repeat(3,minmax(130px,1fr));gap:8px;align-items:end;margin-top:0;padding:10px;border:1px solid rgba(164,211,199,.16);border-radius:10px;background:rgba(10,31,28,.55)}.ollama-catalog-controls label{display:flex;flex-direction:column;gap:5px;color:#a5bfba;font-size:10px;text-transform:uppercase;letter-spacing:.06em}.ollama-catalog-checkbox{display:flex!important;flex-direction:row!important;align-items:center;gap:8px;grid-column:1 / -1;padding:8px 4px;color:#b8ead9!important;text-transform:none!important;letter-spacing:normal!important;cursor:pointer}.ollama-catalog-checkbox input{width:15px;height:15px;margin:0;accent-color:#75d2b7}.ollama-catalog-checkbox span{font-size:11px}.ollama-catalog-memory-bypass{color:#ffd89a!important;background:rgba(142,90,25,.12);border-radius:7px}.ollama-catalog-select{min-width:145px;border:1px solid rgba(155,205,194,.28);border-radius:7px;background:#102d29;color:#e8f2ef;padding:8px;font:inherit;font-size:11px;text-transform:none;letter-spacing:normal}
@media(max-width:1000px){.ollama-nav-row{display:grid;grid-template-columns:1fr}.ollama-toolbar-disk{justify-self:end}.ollama-browse-row{grid-template-columns:1fr}.ollama-catalog-controls{margin-top:0}} @media(max-width:1000px){.ollama-nav-row{display:grid;grid-template-columns:1fr}.ollama-toolbar-disk{justify-self:end}.ollama-browse-row{grid-template-columns:1fr}.ollama-catalog-controls{margin-top:0}}
@media(max-width:760px){.ollama-tabs{grid-template-columns:repeat(2,minmax(0,1fr))}.ollama-nav-row{gap:8px}.ollama-toolbar-disk{justify-self:stretch;grid-template-columns:auto auto;min-width:0}.ollama-browse-row{gap:8px}.ollama-catalog-controls{grid-template-columns:1fr;align-items:stretch}.ollama-catalog-select{width:100%}}.ollama-harness-primary,.ollama-harness-validators{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.ollama-harness-primary{min-width:260px}.ollama-harness-primary label{display:flex;align-items:center;gap:8px;color:#a5bfba;font-size:11px}.ollama-harness-primary select{border:1px solid rgba(155,205,194,.28);border-radius:7px;background:#102d29;color:#e8f2ef;padding:8px;font:inherit;font-size:11px;max-width:260px}.ollama-harness-validators{flex-basis:100%;padding-top:8px;border-top:1px solid rgba(164,211,199,.14)}.ollama-harness-validators>strong{color:#d5e8e2;font-size:11px}.ollama-harness-ready,.ollama-harness-warning{flex-basis:100%;font-size:10px}.ollama-harness-ready{color:#9af1c7}.ollama-harness-warning{color:#ffd89a}.ollama-validation-evidence{margin-top:10px;padding:10px 12px;border:1px solid rgba(141,210,193,.22);border-radius:8px;background:rgba(10,31,28,.5);color:#a5bfba;font-size:11px}.ollama-validation-evidence summary{cursor:pointer;color:#b8ead9;font-weight:700}.ollama-validation-report{margin-top:10px;padding-top:8px;border-top:1px solid rgba(164,211,199,.12)}.ollama-validation-report strong{color:#effcf8;font-size:11px}.ollama-validation-report p{margin:4px 0 0;white-space:pre-wrap;line-height:1.45}.ollama-storage-panel{margin-top:14px;padding:14px 16px;border:1px solid rgba(141,210,193,.22);border-radius:10px;background:rgba(10,31,28,.5)}.ollama-storage-copy{display:flex;flex-direction:column;gap:4px;margin-top:10px}.ollama-storage-copy strong{color:#effcf8;font-size:12px}.ollama-storage-copy small,.ollama-storage-note{color:#a5bfba;font-size:10px}.ollama-storage-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:12px}.ollama-storage-note{display:block;margin-top:10px} @media(max-width:760px){.ollama-tabs{grid-template-columns:repeat(2,minmax(0,1fr))}.ollama-nav-row{gap:8px}.ollama-toolbar-disk{justify-self:stretch;grid-template-columns:auto auto;min-width:0}.ollama-browse-row{gap:8px}.ollama-catalog-controls{grid-template-columns:1fr;align-items:stretch}.ollama-catalog-select{width:100%}}.ollama-harness-primary,.ollama-harness-validators{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.ollama-harness-primary{min-width:260px}.ollama-harness-primary label{display:flex;align-items:center;gap:8px;color:#a5bfba;font-size:11px}.ollama-harness-primary select{border:1px solid rgba(155,205,194,.28);border-radius:7px;background:#102d29;color:#e8f2ef;padding:8px;font:inherit;font-size:11px;max-width:260px}.ollama-harness-validators{flex-basis:100%;padding-top:8px;border-top:1px solid rgba(164,211,199,.14)}.ollama-harness-validators>strong{color:#d5e8e2;font-size:11px}.ollama-harness-ready,.ollama-harness-warning{flex-basis:100%;font-size:10px}.ollama-harness-ready{color:#9af1c7}.ollama-harness-warning{color:#ffd89a}.ollama-validation-evidence{margin-top:10px;padding:10px 12px;border:1px solid rgba(141,210,193,.22);border-radius:8px;background:rgba(10,31,28,.5);color:#a5bfba;font-size:11px}.ollama-validation-evidence summary{cursor:pointer;color:#b8ead9;font-weight:700}.ollama-validation-report{margin-top:10px;padding-top:8px;border-top:1px solid rgba(164,211,199,.12)}.ollama-validation-report strong{color:#effcf8;font-size:11px}.ollama-validation-report p{margin:4px 0 0;white-space:pre-wrap;line-height:1.45}.ollama-storage-panel{margin-top:14px;padding:14px 16px;border:1px solid rgba(141,210,193,.22);border-radius:10px;background:rgba(10,31,28,.5)}.ollama-storage-copy{display:flex;flex-direction:column;gap:4px;margin-top:10px}.ollama-storage-copy strong{color:#effcf8;font-size:12px}.ollama-storage-copy small,.ollama-storage-note{color:#a5bfba;font-size:10px}.ollama-storage-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:12px}.ollama-storage-note{display:block;margin-top:10px}
+1 -1
View File
@@ -3,7 +3,7 @@
"label": "Ollama Models", "label": "Ollama Models",
"description": "Inspect, manage, and chat with local Ollama models, including shared persistent conversations, performance metrics, images, PDFs, URLs, and live memory telemetry.", "description": "Inspect, manage, and chat with local Ollama models, including shared persistent conversations, performance metrics, images, PDFs, URLs, and live memory telemetry.",
"icon": "Cpu", "icon": "Cpu",
"version": "1.7.4", "version": "1.7.5",
"tab": {"path": "/ollama-manager", "position": "after:models"}, "tab": {"path": "/ollama-manager", "position": "after:models"},
"entry": "dist/index.js", "entry": "dist/index.js",
"css": "dist/style.css", "css": "dist/style.css",
-298
View File
@@ -70,9 +70,6 @@ def _discover_ollama_endpoint() -> None:
LOCAL_OLLAMA = _ollama_base_url() LOCAL_OLLAMA = _ollama_base_url()
REMOTE_OLLAMA = "https://ollama.com" REMOTE_OLLAMA = "https://ollama.com"
HUGGINGFACE_API = "https://huggingface.co/api"
HUGGINGFACE_DOWNLOAD_ROOT = "huggingface"
HF_REPO_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,95}/[A-Za-z0-9][A-Za-z0-9._-]{0,95}$")
CATALOG_FILE = "catalog.json" CATALOG_FILE = "catalog.json"
MODEL_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:/-]{0,190}$") MODEL_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:/-]{0,190}$")
MELBOURNE = ZoneInfo("Australia/Melbourne") MELBOURNE = ZoneInfo("Australia/Melbourne")
@@ -750,14 +747,6 @@ def _json_request(url: str, method: str = "GET", payload: Any = None, timeout: i
return value if isinstance(value, dict) else {} return value if isinstance(value, dict) else {}
def _json_list_request(url: str, timeout: int = 30) -> list[dict[str, Any]]:
request = Request(url, headers={"Accept": "application/json", "User-Agent": "Hermes-Ollama-Models/1.7.4"})
with urlopen(request, timeout=timeout) as response:
raw = response.read()
value = json.loads(raw.decode("utf-8")) if raw else []
return [item for item in value if isinstance(item, dict)] if isinstance(value, list) else []
def _valid_name(name: str) -> str: def _valid_name(name: str) -> str:
name = str(name or "").strip() name = str(name or "").strip()
if not MODEL_RE.fullmatch(name): if not MODEL_RE.fullmatch(name):
@@ -1372,251 +1361,6 @@ def _model_view(raw: dict[str, Any], loaded: dict[str, Any] | None = None, sourc
} }
def _huggingface_model_view(raw: dict[str, Any]) -> dict[str, Any]:
repo_id = str(raw.get("id") or raw.get("modelId") or "").strip()
raw_tags = raw.get("tags")
tags = [str(tag).strip() for tag in raw_tags if str(tag).strip()][:32] if isinstance(raw_tags, list) else []
pipeline = str(raw.get("pipeline_tag") or "").strip()
library = str(raw.get("library_name") or "").strip()
searchable = " ".join([repo_id, pipeline, library, *tags])
capabilities = _infer_capabilities(repo_id, library, {}, ["completion"] if pipeline in {"text-generation", "text2text-generation", "image-text-to-text"} else [])
if pipeline == "image-text-to-text" and "vision" not in capabilities:
capabilities.append("vision")
if pipeline in {"text-to-image", "image-to-image", "image-classification"} and "vision" not in capabilities:
capabilities.append("vision")
is_moe = bool(re.search(r"(?:moe|mixture.of.experts|a\d+b)", searchable, re.I))
return {
"name": repo_id,
"source": "huggingface",
"source_label": "Hugging Face",
"downloadable": False,
"installed": False,
"loaded": False,
"size_bytes": 0,
"size_gb": None,
"size_label": "Hub repository",
"loaded_bytes": 0,
"loaded_vram_bytes": 0,
"digest": str(raw.get("sha") or ""),
"modified_at": raw.get("lastModified"),
"family": library or "Hugging Face model",
"architecture": pipeline or "Unknown",
"is_moe": is_moe,
"parameter_size": "unknown",
"activated_parameter_size": None,
"parameter_summary": "unknown",
"description": f"Hugging Face model · {pipeline or 'pipeline unavailable'}" + (f" · {library}" if library else ""),
"quantization": "see repository files",
"format": library or "Hub format",
"context_length": None,
"input_modalities": ["Text", "Image"] if "vision" in capabilities else ["Text"],
"embedding_length": None,
"capabilities": list(dict.fromkeys(capabilities)),
"capability_breakdown": {cap: CAPABILITY_INFO[cap] for cap in capabilities if cap in CAPABILITY_INFO},
"strengths": [pipeline or "model repository", "Hugging Face Hub metadata"],
"expected_ram_gb": None,
"expected_ram_label": "Unknown · inspect repository requirements",
"expected_ram_basis": "Hugging Face does not provide a reliable universal runtime RAM estimate in search results.",
"hf_url": f"https://huggingface.co/{repo_id}",
"hf_downloads": int(raw.get("downloads") or 0),
"hf_likes": int(raw.get("likes") or 0),
"hf_pipeline_tag": pipeline,
"hf_library": library,
"hf_tags": tags,
}
def _search_huggingface(query: str, limit: int = 30) -> list[dict[str, Any]]:
query = str(query or "").strip()
if len(query) < 2:
return []
limit = max(1, min(int(limit), 50))
url = f"{HUGGINGFACE_API}/models?{urlencode({'search': query[:120], 'limit': limit, 'sort': 'downloads', 'direction': '-1', 'full': 'false'})}"
try:
rows = _json_list_request(url, timeout=20)
except (HTTPError, URLError, OSError, ValueError):
return []
return [_huggingface_model_view(row) for row in rows if (row.get("id") or row.get("modelId"))]
def _valid_huggingface_repo(repo_id: str) -> str:
repo_id = str(repo_id or "").strip()
if not HF_REPO_RE.fullmatch(repo_id):
raise HTTPException(400, "Invalid Hugging Face repository id")
return repo_id
def _valid_huggingface_filename(filename: str) -> str:
filename = unquote(str(filename or "")).strip().replace("\\", "/")
path = Path(filename)
if not filename or path.is_absolute() or any(part in {"", ".", ".."} for part in path.parts) or not filename.lower().endswith(".gguf"):
raise HTTPException(400, "Only safe Hugging Face GGUF filenames are supported")
return filename
def _format_bytes(value: int) -> str:
amount = float(max(0, int(value or 0)))
for unit in ("B", "KiB", "MiB", "GiB", "TiB"):
if amount < 1024 or unit == "TiB":
return f"{amount:.1f} {unit}" if unit != "B" else f"{int(amount)} B"
amount /= 1024
return "Unknown"
def _huggingface_repo_files(repo_id: str) -> list[dict[str, Any]]:
repo_id = _valid_huggingface_repo(repo_id)
url = f"{HUGGINGFACE_API}/models/{repo_id}?full=true"
try:
metadata = _json_request(url, timeout=30)
except (HTTPError, URLError, OSError, ValueError) as exc:
raise HTTPException(502, "Hugging Face repository metadata is unavailable") from exc
raw_files: list[dict[str, Any]] = []
siblings = metadata.get("siblings", []) if isinstance(metadata, dict) else []
for item in siblings if isinstance(siblings, list) else []:
if not isinstance(item, dict):
continue
filename = str(item.get("rfilename") or item.get("path") or "").strip()
if not filename.lower().endswith(".gguf"):
continue
try:
filename = _valid_huggingface_filename(filename)
except HTTPException:
continue
lfs: dict[str, Any] = {}
raw_lfs = item.get("lfs")
if isinstance(raw_lfs, dict):
lfs = raw_lfs
size = int(lfs.get("size") or item.get("size") or 0)
raw_files.append({"filename": filename, "size": size, "download_url": f"https://huggingface.co/{repo_id}/resolve/main/{filename}?download=true"})
groups: dict[str, list[dict[str, Any]]] = {}
split_pattern = re.compile(r"^(.*)-\d{5}-of-\d{5}(\.gguf)$", re.I)
for item in raw_files:
match = split_pattern.match(item["filename"])
group_key = f"{match.group(1)}{match.group(2)}" if match else item["filename"]
groups.setdefault(group_key, []).append(item)
files: list[dict[str, Any]] = []
for group_key, group in groups.items():
group.sort(key=lambda item: item["filename"])
size = sum(int(item.get("size") or 0) for item in group)
files.append({
"filename": group[0]["filename"],
"filenames": [item["filename"] for item in group],
"size": size,
"size_label": _format_bytes(size) if size else (f"{len(group)} shards · size unavailable" if len(group) > 1 else "Unknown"),
"file_count": len(group),
"split": len(group) > 1,
"download_url": group[0]["download_url"],
})
return sorted(files, key=lambda item: (item.get("size") or 0, item["filename"]))
def _hf_ollama_import_available() -> bool:
parsed = urlparse(LOCAL_OLLAMA)
return parsed.hostname in {"localhost", "127.0.0.1", "::1"} and not _running_in_container() and bool(shutil.which("ollama"))
def _ollama_model_name_for_hf(repo_id: str, filename: str) -> str:
owner, repo = repo_id.split("/", 1)
stem = re.sub(r"-\d{5}-of-\d{5}$", "", Path(filename).stem.lower())
value = re.sub(r"[^a-z0-9._-]+", "-", f"hf-{owner}-{repo}-{stem}").strip("-._")
return value[:190] or "hf-imported-model"
def _run_huggingface_download(job_id: str, repo_id: str, filename: str) -> None:
temporary: Path | None = None
try:
repo_id = _valid_huggingface_repo(repo_id)
filename = _valid_huggingface_filename(filename)
file_info = next((item for item in _huggingface_repo_files(repo_id) if filename in item.get("filenames", [item["filename"]])), None)
if not file_info:
raise RuntimeError("Requested GGUF file was not found in the public Hugging Face repository")
expected = int(file_info.get("size") or 0)
free_bytes = shutil.disk_usage(_home()).free
if expected and free_bytes < expected + 1024 ** 3:
raise RuntimeError("Insufficient free disk space for the Hugging Face GGUF download")
destination_root = _home() / HUGGINGFACE_DOWNLOAD_ROOT / repo_id
destination_root.mkdir(parents=True, exist_ok=True)
download_files = list(file_info.get("filenames") or [filename])
completed = 0
total = expected
if not total:
for remote_filename in download_files:
head_url = f"https://huggingface.co/{repo_id}/resolve/main/{remote_filename}?download=true"
try:
head_request = Request(head_url, method="HEAD", headers={"User-Agent": "Hermes-Ollama-Models/1.7.4"})
with urlopen(head_request, timeout=30) as head_response:
total += int(head_response.headers.get("Content-Length") or 0)
except (HTTPError, URLError, OSError, ValueError):
continue
free_bytes = shutil.disk_usage(_home()).free
if total and free_bytes < total + 1024 ** 3:
raise RuntimeError("Insufficient free disk space for the complete Hugging Face GGUF download")
for remote_filename in download_files:
destination = destination_root / remote_filename
destination.parent.mkdir(parents=True, exist_ok=True)
temporary = destination.with_name(f".{destination.name}.{job_id}.part")
download_url = f"https://huggingface.co/{repo_id}/resolve/main/{remote_filename}?download=true"
request = Request(download_url, headers={"Accept": "application/octet-stream", "User-Agent": "Hermes-Ollama-Models/1.7.4"})
with urlopen(request, timeout=60) as response, temporary.open("wb") as output:
total = max(total, completed + int(response.headers.get("Content-Length") or 0))
for chunk in iter(lambda: response.read(8 * 1024 * 1024), b""):
output.write(chunk)
completed += len(chunk)
_set_job(job_id, status="downloading", completed=completed, total=total, percent=round(completed * 100 / total, 1) if total else None)
os.replace(temporary, destination)
destination = destination_root / download_files[0]
model_name = _ollama_model_name_for_hf(repo_id, filename)
imported = False
if _hf_ollama_import_available():
modelfile = destination.with_name(f".{destination.name}.Modelfile")
modelfile.write_text(f"FROM {destination}\n", encoding="utf-8")
try:
env = dict(os.environ)
env["OLLAMA_HOST"] = LOCAL_OLLAMA
result = subprocess.run(["ollama", "create", model_name, "-f", str(modelfile)], capture_output=True, text=True, timeout=3600, check=False, env=env)
if result.returncode != 0:
raise RuntimeError((result.stderr or result.stdout or "ollama create failed")[-1000:])
imported = True
finally:
try:
modelfile.unlink()
except FileNotFoundError:
pass
_set_job(job_id, state="completed", status="success", percent=100, path=str(destination), model_name=model_name, imported=imported, message="Downloaded and imported into local Ollama" if imported else "Downloaded GGUF; Ollama import was not available on this filesystem")
except Exception as exc:
if temporary is not None:
try:
temporary.unlink()
except FileNotFoundError:
pass
_set_job(job_id, state="failed", status="error", error=str(exc))
def _search_ollama_catalog(query: str, limit: int = 50) -> list[dict[str, Any]]:
needle = str(query or "").strip().lower()
if len(needle) < 2:
return []
catalog = _ensure_catalog()
rows = list(catalog.get("models", []))
family_map: dict[str, Any] = {}
raw_families = catalog.get("families")
if isinstance(raw_families, dict):
family_map = raw_families
rows.extend(variant for variants in family_map.values() if isinstance(variants, list) for variant in variants if isinstance(variant, dict))
matches: list[dict[str, Any]] = []
seen: set[str] = set()
for raw in rows:
name = str(raw.get("name") or raw.get("model") or "")
if not name or name in seen:
continue
view = _model_view(raw, source="catalog")
haystack = " ".join([name, view.get("family", ""), view.get("description", ""), *view.get("capabilities", []), *view.get("strengths", [])]).lower()
if needle in haystack:
seen.add(name)
matches.append(view)
return matches[:max(1, min(int(limit), 100))]
class _VariantPageParser(HTMLParser): class _VariantPageParser(HTMLParser):
"""Extract the public Ollama tag rows without depending on third-party HTML packages.""" """Extract the public Ollama tag rows without depending on third-party HTML packages."""
@@ -1895,11 +1639,6 @@ class ModelRequest(BaseModel):
placement: str = "gpu_ram" placement: str = "gpu_ram"
class HuggingFaceDownloadRequest(BaseModel):
repo_id: str
filename: str
class ConnectionRequest(BaseModel): class ConnectionRequest(BaseModel):
url: str url: str
role: str = "local" role: str = "local"
@@ -2940,43 +2679,6 @@ def catalog_refresh() -> dict[str, Any]:
return {"ok": bool(catalog.get("models")), "updated_at": catalog.get("fetched_at"), "count": len(catalog.get("models", [])), "error": catalog.get("last_error")} return {"ok": bool(catalog.get("models")), "updated_at": catalog.get("fetched_at"), "count": len(catalog.get("models", [])), "error": catalog.get("last_error")}
@router.get("/catalog/search")
def catalog_search(q: str = "", limit: int = 30, include_huggingface: bool = False) -> dict[str, Any]:
query = str(q or "").strip()[:120]
if len(query) < 2:
return {"query": query, "results": [], "sources": ["ollama", "huggingface"] if include_huggingface else ["ollama"]}
ollama = _search_ollama_catalog(query, limit=limit)
huggingface = _search_huggingface(query, limit=limit) if include_huggingface else []
combined = ollama + huggingface
return {
"query": query,
"results": combined,
"ollama_results": ollama,
"huggingface_results": huggingface,
"sources": ["ollama", "huggingface"] if include_huggingface else ["ollama"],
}
@router.get("/huggingface/files")
def huggingface_files(repo_id: str) -> dict[str, Any]:
repo_id = _valid_huggingface_repo(repo_id)
files = _huggingface_repo_files(repo_id)
return {"repo_id": repo_id, "files": [{key: value for key, value in item.items() if key != "download_url"} for item in files], "import_supported": _hf_ollama_import_available()}
@router.post("/huggingface/download")
def huggingface_download(body: HuggingFaceDownloadRequest) -> dict[str, Any]:
repo_id = _valid_huggingface_repo(body.repo_id)
filename = _valid_huggingface_filename(body.filename)
if not any(item["filename"] == filename for item in _huggingface_repo_files(repo_id)):
raise HTTPException(400, "Requested GGUF file was not found in the public Hugging Face repository")
job_id = uuid.uuid4().hex
with _jobs_lock:
_jobs[job_id] = {"id": job_id, "name": f"{repo_id}/{filename}", "action": "huggingface-download", "target": "local", "state": "running", "status": "starting", "percent": 0, "created_at": time.time(), "updated_at": time.time()}
threading.Thread(target=_run_huggingface_download, args=(job_id, repo_id, filename), daemon=True, name=f"huggingface-download-{job_id[:8]}").start()
return {"ok": True, "job_id": job_id, "repo_id": repo_id, "filename": filename, "message": "Hugging Face GGUF download started"}
@router.post("/pull") @router.post("/pull")
def pull_model(body: ModelRequest) -> dict[str, Any]: def pull_model(body: ModelRequest) -> dict[str, Any]:
name = _valid_name(body.name) name = _valid_name(body.name)
+1 -1
View File
@@ -1,5 +1,5 @@
name: ollama-manager name: ollama-manager
version: 1.7.4 version: 1.7.5
description: Native dashboard manager and chat interface for local Ollama models, attachments, URLs, shared persistent conversations, performance metrics, and live runtime telemetry. description: Native dashboard manager and chat interface for local Ollama models, attachments, URLs, shared persistent conversations, performance metrics, and live runtime telemetry.
auto_install_dependencies: true auto_install_dependencies: true
python_dependencies: python_dependencies:
-74
View File
@@ -89,80 +89,6 @@ class ValidationHarnessTests(unittest.TestCase):
self.assertEqual(response, {"jobs": active}) self.assertEqual(response, {"jobs": active})
listed.assert_called_once_with(active_only=True, conversation_id="conversation", limit=20) listed.assert_called_once_with(active_only=True, conversation_id="conversation", limit=20)
def test_huggingface_search_normalizes_public_model_metadata(self):
with patch.object(
api,
"_json_list_request",
return_value=[
{
"id": "Qwen/Qwen3.8-Flash-Next",
"downloads": 4810,
"likes": 3966,
"pipeline_tag": "image-text-to-text",
"library_name": "transformers",
"lastModified": "2026-08-27T00:00:00.000Z",
"sha": "abc123",
"tags": ["qwen", "conversational"],
}
],
):
results = api._search_huggingface("Qwen3.8-Flash-Next", limit=10)
self.assertEqual(len(results), 1)
model = results[0]
self.assertEqual(model["name"], "Qwen/Qwen3.8-Flash-Next")
self.assertEqual(model["source"], "huggingface")
self.assertEqual(model["hf_url"], "https://huggingface.co/Qwen/Qwen3.8-Flash-Next")
self.assertIn("vision", model["capabilities"])
self.assertFalse(model["downloadable"])
def test_catalog_search_combines_ollama_and_huggingface_sources(self):
ollama = [{"name": "qwen3.8:latest", "source": "catalog"}]
huggingface = [{"name": "Qwen/Qwen3.8-Flash-Next", "source": "huggingface"}]
with patch.object(api, "_search_ollama_catalog", return_value=ollama) as ollama_search, patch.object(
api, "_search_huggingface", return_value=huggingface
) as hf_search:
response = api.catalog_search("Qwen3.8", limit=10, include_huggingface=True)
self.assertEqual(response["results"], ollama + huggingface)
self.assertEqual(response["sources"], ["ollama", "huggingface"])
ollama_search.assert_called_once_with("Qwen3.8", limit=10)
hf_search.assert_called_once_with("Qwen3.8", limit=10)
def test_huggingface_groups_split_gguf_files_into_complete_sets(self):
metadata = {
"siblings": [
{"rfilename": "Q4/Qwen-00001-of-00002.gguf"},
{"rfilename": "Q4/Qwen-00002-of-00002.gguf"},
{"rfilename": "Q8/Qwen.gguf"},
{"rfilename": "Q8/README.md"},
]
}
with patch.object(api, "_json_request", return_value=metadata):
files = api._huggingface_repo_files("owner/repository")
self.assertEqual(len(files), 2)
split = next(item for item in files if item["split"])
self.assertEqual(split["file_count"], 2)
self.assertEqual(len(split["filenames"]), 2)
single = next(item for item in files if not item["split"])
self.assertEqual(single["filename"], "Q8/Qwen.gguf")
def test_huggingface_download_rejects_non_gguf_paths(self):
with self.assertRaises(api.HTTPException):
api._valid_huggingface_filename("../model.safetensors")
with self.assertRaises(api.HTTPException):
api._valid_huggingface_filename("model.bin")
self.assertEqual(api._valid_huggingface_filename("Q4_K_M/model.gguf"), "Q4_K_M/model.gguf")
def test_huggingface_download_queues_validated_file(self):
body = api.HuggingFaceDownloadRequest(repo_id="unsloth/Qwen3.8-Flash-Next-GGUF", filename="Q4_K_M/model.gguf")
fake_thread = type("Thread", (), {"start": lambda self: None})
with patch.object(api, "_huggingface_repo_files", return_value=[{"filename": body.filename, "size": 123}]), patch.object(
api.threading, "Thread", return_value=fake_thread()
) as thread:
response = api.huggingface_download(body)
self.assertTrue(response["ok"])
self.assertEqual(response["filename"], body.filename)
thread.assert_called_once()
def test_primary_draft_validators_and_primary_compilation_produce_one_answer(self): def test_primary_draft_validators_and_primary_compilation_produce_one_answer(self):
body = api.ChatRequest( body = api.ChatRequest(
primary_model="primary", primary_model="primary",