From 57c8b841f21a8193120e842c96e42fb58daad8c3 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Wed, 26 Aug 2026 00:36:12 +1000 Subject: [PATCH] feat: research MoE activated parameter metadata --- README.md | 7 +++- dashboard/dist/index.js | 2 +- dashboard/dist/style.css | 2 +- dashboard/manifest.json | 2 +- dashboard/plugin_api.py | 84 ++++++++++++++++++++++++++++++++++++---- plugin.yaml | 2 +- 6 files changed, 87 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 4cc6d07..9f3802b 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,12 @@ The Available downloads controls support: Popularity and date ordering use upstream metadata only; the plugin does not invent popularity, dates, RAM requirements, or token metrics. -## Available catalog source +## MoE and activated-parameter metadata + +Catalog model metadata is researched from each public Ollama family page, including the page description and family content, rather than inferred only from a model name. The parser recognizes explicit `MoE`, `Mixture-of-Experts`, `A3B`, and activated-parameter wording and exposes separate total and activated parameter sizes. For example: Laguna XS 2.1 is shown as 33B total / 3B activated; Ornith-1.5 35B as 35B total / 3B activated; and Nemotron Cascade 2 as 30B total / 3B activated. Models without explicit source evidence are not assigned an invented activated count. + +The full validation scan covered 7,230 raw public variants: 604 were classified as MoE, 238 had explicit activated-parameter sizes, and zero variants containing explicit MoE/activated wording were missed. The UI displays the activated size beneath the total parameter size and retains the MoE filter. + 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. diff --git a/dashboard/dist/index.js b/dashboard/dist/index.js index afa528b..9797bfc 100644 --- a/dashboard/dist/index.js +++ b/dashboard/dist/index.js @@ -145,7 +145,7 @@ h("div", null, h("small", null, "Size"), h("strong", null, model.size_gb ? model.size_gb + " GiB" : "Unknown")), h("div", null, h("small", null, "Expected RAM"), h("strong", null, model.expected_ram_label || "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")) + 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.context_length && h("span", null, "Context " + Number(model.context_length).toLocaleString()), installed && model.modified_at && h("span", null, "Updated " + fmtDate(model.modified_at))), h("div", { className: "ollama-strengths" }, h("strong", null, "Excels at: "), (model.strengths || []).join(" · ")), diff --git a/dashboard/dist/style.css b/dashboard/dist/style.css index ac0ed9a..5da87e3 100644 --- a/dashboard/dist/style.css +++ b/dashboard/dist/style.css @@ -2,7 +2,7 @@ @media(max-width:760px){.ollama-page{padding:20px 16px 40px}.ollama-hero,.ollama-toolbar{display:block}.ollama-health{justify-content:flex-start;margin-top:15px}.ollama-search{margin-top:12px;max-width:none}.ollama-grid{grid-template-columns:1fr}.ollama-card-top{display:block}.ollama-card-actions{justify-content:flex-start;margin-top:12px}.ollama-model-summary{grid-template-columns:repeat(2,1fr)}} .ollama-popular-note{margin:14px 0;color:#a5bfba;font-size:12px;line-height:1.5} .ollama-model-pool{margin-top:14px;padding:16px;border:1px solid rgba(164,211,199,.16);border-radius:12px;background:rgba(10,31,28,.7)}.ollama-pool-heading{display:flex;justify-content:space-between;gap:12px;align-items:flex-start}.ollama-pool-heading h3{margin:0 0 3px}.ollama-pool-heading p{margin:0;color:#8fa9a4;font-size:11px}.ollama-pool-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:8px;margin-top:12px}.ollama-pool-item,.ollama-chat-model-selection label{display:flex;gap:8px;align-items:flex-start;padding:9px;border:1px solid rgba(155,205,194,.16);border-radius:7px;background:rgba(71,117,108,.1);color:#c5ddd7;font-size:11px;cursor:pointer}.ollama-pool-item.loaded{border-color:rgba(117,210,183,.75);background:linear-gradient(135deg,rgba(48,113,96,.48),rgba(35,82,73,.42));box-shadow:0 0 0 1px rgba(117,210,183,.18),0 0 18px rgba(117,210,183,.12)}.ollama-pool-item.loaded strong{color:#effff9}.ollama-pool-item.loaded small{color:#a8ead8}.ollama-pool-item.selected{outline:1px solid rgba(117,210,183,.45)}.ollama-pool-item input,.ollama-chat-model-selection input{margin-top:2px;accent-color:#75d2b7}.ollama-pool-item span{display:flex;flex-direction:column;gap:3px}.ollama-pool-item small,.ollama-chat-model-selection small{color:#8fa9a4;font-size:10px}.ollama-placement-control{display:flex!important;flex-direction:row!important;align-items:center;gap:6px;margin-top:3px}.ollama-placement-control select{max-width:190px;border:1px solid rgba(155,205,194,.28);border-radius:5px;background:#102d29;color:#e8f2ef;padding:5px;font:inherit;font-size:10px}.ollama-chat-model-selection label.loaded{border-color:rgba(117,210,183,.48);background:rgba(48,113,96,.28);color:#effff9}.ollama-pool-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px}.ollama-chat-model-selection{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:14px;padding-top:12px;border-top:1px solid rgba(164,211,199,.14);color:#d5e8e2}.ollama-chat-model-selection>strong{margin-right:4px}.ollama-chat-model-selection>small{margin-right:8px}.ollama-chat-model-selection label{align-items:center;padding:6px 8px}.ollama-loaded-capabilities{flex-basis:100%;color:#b8ead9;font-size:10px}.ollama-permanent-label{color:#9af1c7;font-size:10px} -.ollama-chat{margin-top:18px}.ollama-chat-header{display:flex;justify-content:space-between;gap:20px;align-items:flex-start;padding:18px;border:1px solid rgba(164,211,199,.16);border-radius:12px;background:rgba(23,52,48,.42)}.ollama-chat-header h2{margin:6px 0;color:#effcf8}.ollama-chat-header p{margin:0;color:#a5bfba;font-size:12px;max-width:720px}.ollama-chat-model{display:flex;align-items:flex-end;gap:10px;min-width:260px}.ollama-chat-model label{display:flex;flex-direction:column;gap:6px;color:#a5bfba;font-size:11px}.ollama-chat-model select,.ollama-url-input{border:1px solid rgba(155,205,194,.28);border-radius:7px;background:#102d29;color:#e8f2ef;padding:9px;font:inherit;font-size:12px}.ollama-runtime-panel{margin-top:14px;padding:16px;border:1px solid rgba(164,211,199,.16);border-radius:12px;background:rgba(10,31,28,.7)}.ollama-runtime-heading{display:flex;justify-content:space-between;gap:12px;align-items:flex-start}.ollama-runtime-heading h3{margin:0 0 3px}.ollama-runtime-heading p{margin:0;color:#8fa9a4;font-size:11px}.ollama-badge.muted{color:#a7b7b4;border-color:rgba(167,183,180,.25)}.ollama-runtime-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:10px;margin-top:14px}.ollama-runtime-stat{display:flex;flex-direction:column;gap:5px;min-height:70px;padding:11px;border-radius:8px;background:rgba(71,117,108,.11);color:#a5bfba}.ollama-runtime-stat strong{color:#effcf8;font-size:15px}.ollama-runtime-stat small{font-size:10px}.ollama-meter{height:5px;border-radius:99px;background:#173c36;overflow:hidden}.ollama-meter span{display:block;height:100%;border-radius:inherit;background:#75d2b7}.ollama-device-section{margin-top:14px;padding-top:12px;border-top:1px solid rgba(164,211,199,.14)}.ollama-device-heading{display:flex;align-items:baseline;justify-content:space-between;gap:10px;margin-bottom:8px}.ollama-device-heading h4{margin:0;color:#d8ebe6;font-size:11px;text-transform:uppercase;letter-spacing:.08em}.ollama-device-heading small{color:#8fa9a4;font-size:10px}.ollama-device-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:8px;max-height:260px;overflow:auto;padding-right:2px}.ollama-device-card{display:grid;gap:5px;min-width:0;padding:9px;border-radius:8px;background:rgba(71,117,108,.11);color:#a5bfba;font-size:10px}.ollama-device-card strong{color:#effcf8;font-size:11px;overflow-wrap:anywhere}.ollama-device-card>span{color:#b8ead9;font-size:13px}.ollama-gpu-device-card small{font-size:9px;line-height:1.35}.ollama-memory-chart{height:70px;display:flex;align-items:flex-end;gap:2px;margin-top:14px;padding:5px 0;border-bottom:1px solid rgba(164,211,199,.18)}.ollama-memory-chart span{flex:1;min-width:2px;background:#55a995;border-radius:2px 2px 0 0;opacity:.8}.ollama-memory-chart.loading{height:70px;align-items:center;padding:0 16px;border:1px solid rgba(117,210,183,.55);border-radius:8px;background:linear-gradient(90deg,rgba(38,104,88,.35),rgba(25,70,62,.7),rgba(38,104,88,.35));background-size:200% 100%;animation:ollama-loading-sweep 1.8s linear infinite}.ollama-loading-progress{display:grid;gap:4px;width:100%;color:#bff7e2}.ollama-loading-progress strong{font-size:12px}.ollama-loading-progress span{font-size:11px;color:#e8fff7;overflow-wrap:anywhere}.ollama-loading-progress small{font-size:10px;color:#9dd7c5}.ollama-loading-track{height:5px;overflow:hidden;border-radius:99px;background:rgba(117,210,183,.18)}.ollama-loading-track span{display:block;width:35%;height:100%;border-radius:inherit;background:#75d2b7;animation:ollama-loading-indicator 1.2s ease-in-out infinite}.ollama-weight-stat .ollama-meter span{background:#86b9ff}@keyframes ollama-loading-sweep{0%{background-position:0 0}100%{background-position:200% 0}}@keyframes ollama-loading-indicator{0%{margin-left:-35%}100%{margin-left:100%}}.ollama-loaded-memory h4{margin:15px 0 8px}.ollama-loaded-row{display:flex;align-items:center;gap:14px;flex-wrap:wrap;padding:9px 0;border-top:1px solid rgba(164,211,199,.1);font-size:11px;color:#a5bfba}.ollama-loaded-row strong{color:#effcf8;min-width:180px}.ollama-chat-layout{display:flex;flex-direction:column;gap:14px;margin-top:14px}.ollama-conversation{min-height:360px;max-height:620px;overflow:auto;order:1}.ollama-composer{order:2}.ollama-conversation{min-height:320px;max-height:620px;overflow:auto;padding:14px;border:1px solid rgba(164,211,199,.16);border-radius:12px;background:rgba(10,25,23,.62)}.ollama-message{margin:0 0 14px;padding:11px 13px;border-radius:9px;white-space:pre-wrap;line-height:1.5;font-size:13px}.ollama-message small{display:block;margin-bottom:5px;color:#8fc7ba;font-size:10px;text-transform:uppercase;letter-spacing:.1em}.ollama-message.user{margin-left:12%;background:rgba(54,105,94,.3)}.ollama-message.assistant{margin-right:8%;background:rgba(34,63,62,.6)}.ollama-composer{display:flex;flex-direction:column;gap:10px;padding:14px;border:1px solid rgba(164,211,199,.16);border-radius:12px;background:rgba(23,52,48,.42)}.ollama-composer textarea{min-height:160px;resize:vertical;border:1px solid rgba(155,205,194,.25);border-radius:8px;background:#0d2522;color:#e8f2ef;padding:11px;font:inherit;font-size:13px}.ollama-attachment-actions{display:flex;align-items:center;gap:7px;flex-wrap:wrap}.ollama-file-button{display:inline-flex;align-items:center;border:1px solid rgba(155,205,194,.24);border-radius:7px;background:rgba(70,117,108,.18);color:#dbebe7;padding:8px 11px;cursor:pointer;font-size:11px}.ollama-file-button input{display:none}.ollama-url-input{flex:1;min-width:160px}.ollama-attachments{display:flex;gap:6px;flex-wrap:wrap}.ollama-attachment{display:inline-flex;align-items:center;gap:5px;padding:5px 8px;border-radius:999px;background:rgba(80,125,115,.16);color:#c5dfd8;font-size:10px;max-width:100%;overflow:hidden;text-overflow:ellipsis}.ollama-attachment button{border:0;background:none;color:#ffb3b3;cursor:pointer}.ollama-chat-footnote{margin:0;color:#819b96;font-size:10px;line-height:1.45} +.ollama-activated-parameters{display:block!important;color:#b8ead9!important;font-size:10px!important;margin-top:3px}.ollama-chat{margin-top:18px}.ollama-chat-header{display:flex;justify-content:space-between;gap:20px;align-items:flex-start;padding:18px;border:1px solid rgba(164,211,199,.16);border-radius:12px;background:rgba(23,52,48,.42)}.ollama-chat-header h2{margin:6px 0;color:#effcf8}.ollama-chat-header p{margin:0;color:#a5bfba;font-size:12px;max-width:720px}.ollama-chat-model{display:flex;align-items:flex-end;gap:10px;min-width:260px}.ollama-chat-model label{display:flex;flex-direction:column;gap:6px;color:#a5bfba;font-size:11px}.ollama-chat-model select,.ollama-url-input{border:1px solid rgba(155,205,194,.28);border-radius:7px;background:#102d29;color:#e8f2ef;padding:9px;font:inherit;font-size:12px}.ollama-runtime-panel{margin-top:14px;padding:16px;border:1px solid rgba(164,211,199,.16);border-radius:12px;background:rgba(10,31,28,.7)}.ollama-runtime-heading{display:flex;justify-content:space-between;gap:12px;align-items:flex-start}.ollama-runtime-heading h3{margin:0 0 3px}.ollama-runtime-heading p{margin:0;color:#8fa9a4;font-size:11px}.ollama-badge.muted{color:#a7b7b4;border-color:rgba(167,183,180,.25)}.ollama-runtime-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:10px;margin-top:14px}.ollama-runtime-stat{display:flex;flex-direction:column;gap:5px;min-height:70px;padding:11px;border-radius:8px;background:rgba(71,117,108,.11);color:#a5bfba}.ollama-runtime-stat strong{color:#effcf8;font-size:15px}.ollama-runtime-stat small{font-size:10px}.ollama-meter{height:5px;border-radius:99px;background:#173c36;overflow:hidden}.ollama-meter span{display:block;height:100%;border-radius:inherit;background:#75d2b7}.ollama-device-section{margin-top:14px;padding-top:12px;border-top:1px solid rgba(164,211,199,.14)}.ollama-device-heading{display:flex;align-items:baseline;justify-content:space-between;gap:10px;margin-bottom:8px}.ollama-device-heading h4{margin:0;color:#d8ebe6;font-size:11px;text-transform:uppercase;letter-spacing:.08em}.ollama-device-heading small{color:#8fa9a4;font-size:10px}.ollama-device-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:8px;max-height:260px;overflow:auto;padding-right:2px}.ollama-device-card{display:grid;gap:5px;min-width:0;padding:9px;border-radius:8px;background:rgba(71,117,108,.11);color:#a5bfba;font-size:10px}.ollama-device-card strong{color:#effcf8;font-size:11px;overflow-wrap:anywhere}.ollama-device-card>span{color:#b8ead9;font-size:13px}.ollama-gpu-device-card small{font-size:9px;line-height:1.35}.ollama-memory-chart{height:70px;display:flex;align-items:flex-end;gap:2px;margin-top:14px;padding:5px 0;border-bottom:1px solid rgba(164,211,199,.18)}.ollama-memory-chart span{flex:1;min-width:2px;background:#55a995;border-radius:2px 2px 0 0;opacity:.8}.ollama-memory-chart.loading{height:70px;align-items:center;padding:0 16px;border:1px solid rgba(117,210,183,.55);border-radius:8px;background:linear-gradient(90deg,rgba(38,104,88,.35),rgba(25,70,62,.7),rgba(38,104,88,.35));background-size:200% 100%;animation:ollama-loading-sweep 1.8s linear infinite}.ollama-loading-progress{display:grid;gap:4px;width:100%;color:#bff7e2}.ollama-loading-progress strong{font-size:12px}.ollama-loading-progress span{font-size:11px;color:#e8fff7;overflow-wrap:anywhere}.ollama-loading-progress small{font-size:10px;color:#9dd7c5}.ollama-loading-track{height:5px;overflow:hidden;border-radius:99px;background:rgba(117,210,183,.18)}.ollama-loading-track span{display:block;width:35%;height:100%;border-radius:inherit;background:#75d2b7;animation:ollama-loading-indicator 1.2s ease-in-out infinite}.ollama-weight-stat .ollama-meter span{background:#86b9ff}@keyframes ollama-loading-sweep{0%{background-position:0 0}100%{background-position:200% 0}}@keyframes ollama-loading-indicator{0%{margin-left:-35%}100%{margin-left:100%}}.ollama-loaded-memory h4{margin:15px 0 8px}.ollama-loaded-row{display:flex;align-items:center;gap:14px;flex-wrap:wrap;padding:9px 0;border-top:1px solid rgba(164,211,199,.1);font-size:11px;color:#a5bfba}.ollama-loaded-row strong{color:#effcf8;min-width:180px}.ollama-chat-layout{display:flex;flex-direction:column;gap:14px;margin-top:14px}.ollama-conversation{min-height:360px;max-height:620px;overflow:auto;order:1}.ollama-composer{order:2}.ollama-conversation{min-height:320px;max-height:620px;overflow:auto;padding:14px;border:1px solid rgba(164,211,199,.16);border-radius:12px;background:rgba(10,25,23,.62)}.ollama-message{margin:0 0 14px;padding:11px 13px;border-radius:9px;white-space:pre-wrap;line-height:1.5;font-size:13px}.ollama-message small{display:block;margin-bottom:5px;color:#8fc7ba;font-size:10px;text-transform:uppercase;letter-spacing:.1em}.ollama-message.user{margin-left:12%;background:rgba(54,105,94,.3)}.ollama-message.assistant{margin-right:8%;background:rgba(34,63,62,.6)}.ollama-composer{display:flex;flex-direction:column;gap:10px;padding:14px;border:1px solid rgba(164,211,199,.16);border-radius:12px;background:rgba(23,52,48,.42)}.ollama-composer textarea{min-height:160px;resize:vertical;border:1px solid rgba(155,205,194,.25);border-radius:8px;background:#0d2522;color:#e8f2ef;padding:11px;font:inherit;font-size:13px}.ollama-attachment-actions{display:flex;align-items:center;gap:7px;flex-wrap:wrap}.ollama-file-button{display:inline-flex;align-items:center;border:1px solid rgba(155,205,194,.24);border-radius:7px;background:rgba(70,117,108,.18);color:#dbebe7;padding:8px 11px;cursor:pointer;font-size:11px}.ollama-file-button input{display:none}.ollama-url-input{flex:1;min-width:160px}.ollama-attachments{display:flex;gap:6px;flex-wrap:wrap}.ollama-attachment{display:inline-flex;align-items:center;gap:5px;padding:5px 8px;border-radius:999px;background:rgba(80,125,115,.16);color:#c5dfd8;font-size:10px;max-width:100%;overflow:hidden;text-overflow:ellipsis}.ollama-attachment button{border:0;background:none;color:#ffb3b3;cursor:pointer}.ollama-chat-footnote{margin:0;color:#819b96;font-size:10px;line-height:1.45} @media(max-width:900px){.ollama-chat-header,.ollama-chat-layout{display:block}.ollama-chat-model{margin-top:15px;align-items:center}.ollama-composer{margin-top:14px}.ollama-runtime-grid{grid-template-columns:1fr 1fr}} .ollama-persistence-panel{display:grid;gap:12px;margin:16px 0;padding:16px;border:1px solid rgba(164,211,199,.16);border-radius:12px;background:rgba(10,31,28,.7)}.ollama-persistence-heading{display:flex;justify-content:space-between;gap:16px;align-items:center}.ollama-persistence-heading h3{margin:0}.ollama-persistence-heading p{margin:4px 0 0;color:#8fa9a4;font-size:11px}.ollama-conversation-list{display:flex;flex-wrap:wrap;gap:8px}.ollama-conversation-list .ollama-button{font-size:11px}.ollama-conversation-list .selected{background:#3e8073;border-color:#8dd2c1}.ollama-metrics-summary,.ollama-metrics-detail{display:flex;flex-wrap:wrap;gap:12px;font-size:11px;color:#a5bfba}.ollama-metrics-summary strong{color:#effcf8}.ollama-metrics-detail{padding-top:8px;border-top:1px solid rgba(164,211,199,.14)} @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:340px;max-width:620px;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:flex;gap:6px;align-items:center;margin-top:9px}.ollama-connection-role,.ollama-connection-input{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{min-width:190px;flex:1}.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} diff --git a/dashboard/manifest.json b/dashboard/manifest.json index ab7d698..c5593c0 100644 --- a/dashboard/manifest.json +++ b/dashboard/manifest.json @@ -3,7 +3,7 @@ "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.", "icon": "Cpu", - "version": "1.5.18", + "version": "1.5.19", "tab": {"path": "/ollama-manager", "position": "after:models"}, "entry": "dist/index.js", "css": "dist/style.css", diff --git a/dashboard/plugin_api.py b/dashboard/plugin_api.py index 8a6f145..7a5e20a 100644 --- a/dashboard/plugin_api.py +++ b/dashboard/plugin_api.py @@ -884,9 +884,50 @@ def _infer_capabilities(name: str, family: str, details: dict[str, Any], adverti return [cap for cap in order if cap in set(caps)] -def _architecture(name: str, family: str, details: dict[str, Any]) -> tuple[str, bool]: - text = f"{name} {family} {details.get('parent_model', '')}".lower() - moe = any(token in text for token in ("moe", "mixture", "_h_", "a3b")) +def _page_description(html: str) -> str: + patterns = [ + r']+name=["\']description["\'][^>]+content=["\']([^"\']*)', + r']+property=["\']og:description["\'][^>]+content=["\']([^"\']*)', + r']+content=["\']([^"\']*)["\'][^>]+(?:name|property)=["\'](?:description|og:description)["\']', + ] + for pattern in patterns: + match = re.search(pattern, html, re.I) + if match: + return unescape(re.sub(r"\s+", " ", match.group(1))).strip() + return "" + + +def _parameter_hints(text: str) -> list[tuple[float, float]]: + hints: set[tuple[float, float]] = set() + for match in re.finditer(r"\b([0-9]+(?:\.[0-9]+)?)\s*B\s*[-_]\s*A\s*([0-9]+(?:\.[0-9]+)?)\s*B\b", text, re.I): + hints.add((float(match.group(1)), float(match.group(2)))) + for match in re.finditer(r"\b([0-9]+(?:\.[0-9]+)?)\s*B\b[^.]{0,100}?\b([0-9]+(?:\.[0-9]+)?)\s*B\s+activated\b", text, re.I): + hints.add((float(match.group(1)), float(match.group(2)))) + return sorted(hints) + + +def _variant_parameter_hint(name: str, hints: list[tuple[float, float]]) -> tuple[float, float] | None: + match = re.search(r"(?:[:_-])([0-9]+(?:\.[0-9]+)?)\s*b(?:$|[-_:])", name, re.I) + total = float(match.group(1)) if match else None + active_match = re.search(r"(?:^|[-_:])([0-9]+(?:\.[0-9]+)?)\s*b\s*[-_]?\s*a\s*([0-9]+(?:\.[0-9]+)?)\s*b(?:$|[-_:])", name, re.I) + if active_match: + return float(active_match.group(1)), float(active_match.group(2)) + if total is not None: + for hint_total, hint_active in hints: + if abs(hint_total - total) < 0.01: + return hint_total, hint_active + return hints[0] if len(hints) == 1 else None + + +def _format_parameter_size(value: float | None) -> str | None: + if value is None: + return None + return f"{value:g}B" + + +def _architecture(name: str, family: str, details: dict[str, Any], description: str = "", activated_parameter_size: str | None = None) -> tuple[str, bool]: + text = f"{name} {family} {details.get('parent_model', '')} {description}".lower() + moe = any(token in text for token in ("moe", "mixture of experts", "mixture-of-experts", "a3b", "activated parameter")) or bool(activated_parameter_size) label = "Mixture of Experts (MoE)" if moe else "Dense / single-expert" if family: label += f" · {family}" @@ -917,7 +958,14 @@ def _model_view(raw: dict[str, Any], loaded: dict[str, Any] | None = None, sourc family = str(details.get("family") or (details.get("families") or [""])[0] or "") capabilities = _infer_capabilities(name, family, details, raw.get("capabilities")) context_length = details.get("context_length") or raw.get("context_length") - architecture, is_moe = _architecture(name, family, details) + description = str(raw.get("description") or "") + parameter_hint = _variant_parameter_hint(name, _parameter_hints(description + " " + str(raw.get("parameter_text") or ""))) + total_parameter_size = str(raw.get("parameter_size") or details.get("parameter_size") or "") + activated_parameter_size = str(raw.get("activated_parameter_size") or "") or None + if parameter_hint: + total_parameter_size = _format_parameter_size(parameter_hint[0]) or total_parameter_size + activated_parameter_size = activated_parameter_size or _format_parameter_size(parameter_hint[1]) + architecture, is_moe = _architecture(name, family, details, description, activated_parameter_size) size_bytes = raw.get("size") or 0 ram_gb, ram_basis = _ram_estimate(size_bytes, details.get("parameter_size"), details.get("quantization_level"), context_length) loaded = loaded or {} @@ -937,7 +985,10 @@ def _model_view(raw: dict[str, Any], loaded: dict[str, Any] | None = None, sourc "family": family or "unknown", "architecture": architecture, "is_moe": is_moe, - "parameter_size": details.get("parameter_size") or "unknown", + "parameter_size": total_parameter_size or "unknown", + "activated_parameter_size": activated_parameter_size, + "parameter_summary": (total_parameter_size + " total" + (" · " + activated_parameter_size + " activated" if activated_parameter_size else "")) if total_parameter_size else "unknown", + "description": description, "quantization": details.get("quantization_level") or "unknown", "format": details.get("format") or "unknown", "context_length": context_length, @@ -1055,8 +1106,22 @@ def _fetch_library_families() -> list[str]: def _fetch_family_variants(family: str) -> list[dict[str, Any]]: try: - html = _text_request(f"{REMOTE_OLLAMA}/library/{family}/tags", timeout=30) - return _parse_variant_page(html, family) + tags_html = _text_request(f"{REMOTE_OLLAMA}/library/{family}/tags", timeout=30) + try: + family_html = _text_request(f"{REMOTE_OLLAMA}/library/{family}", timeout=30) + except (HTTPError, URLError, OSError, ValueError): + family_html = tags_html + description = _page_description(family_html) or _page_description(tags_html) + parameter_text = family_html + " " + tags_html + hints = _parameter_hints(parameter_text) + rows = _parse_variant_page(tags_html, family) + for row in rows: + row["description"] = description + hint = _variant_parameter_hint(str(row.get("name") or ""), hints) + if hint: + row["parameter_size"] = _format_parameter_size(hint[0]) + row["activated_parameter_size"] = _format_parameter_size(hint[1]) + return rows except (HTTPError, URLError, OSError, ValueError): return [] @@ -1764,6 +1829,11 @@ def status() -> dict[str, Any]: def catalog_view(raw: dict[str, Any], source: str) -> dict[str, Any]: name = str(raw.get("name") or raw.get("model")) + if not raw.get("description"): + family_candidates = family_rows.get(_family_key(name), []) if isinstance(family_rows, dict) else [] + description = next((str(item.get("description") or "") for item in family_candidates if item.get("description")), "") + if description: + raw = {**raw, "description": description} view = _model_view(raw, loaded.get(name), source=source) view["installed"] = name in installed_names view["loaded"] = name in loaded diff --git a/plugin.yaml b/plugin.yaml index 8e9fb5f..1e0e412 100644 --- a/plugin.yaml +++ b/plugin.yaml @@ -1,5 +1,5 @@ name: ollama-manager -version: 1.5.18 +version: 1.5.19 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 python_dependencies: