feat: include Hugging Face model search
This commit is contained in:
@@ -83,6 +83,8 @@ 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.
|
||||
|
||||
Model search also queries the public Hugging Face Hub API when a search 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. They are intentionally not sent to Ollama's `/api/pull`: repository formats and runtime requirements vary, so a Transformers/safetensors/FP8/GGUF repository must be inspected before installation. For example, searching `Qwen3.8-Flash-Next` returns `Qwen/Qwen3.8-Flash-Next` and compatible community repositories when Hugging Face has indexed them.
|
||||
|
||||
|
||||
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.
|
||||
|
||||
|
||||
Vendored
+27
-4
@@ -129,6 +129,7 @@
|
||||
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 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) badges.push(h(Badge, { key: "installed", tone: "installed" }, "installed"));
|
||||
if (!installed) badges.push(h(Badge, { key: "available", tone: "download" }, "available"));
|
||||
@@ -141,7 +142,8 @@
|
||||
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, className: "ollama-button danger", onClick: function () { action("delete", model.name); } }, busy === model.name + ":delete" ? "Removing…" : "Remove"),
|
||||
!installed && h(Button, { disabled: !!busy, onClick: function () { action("pull", model.name); } }, busy === model.name + ":pull" ? "Downloading…" : "Download")
|
||||
!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 && 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" },
|
||||
@@ -150,7 +152,7 @@
|
||||
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", { 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-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-strengths" }, h("strong", null, "Excels at: "), (model.strengths || []).join(" · ")),
|
||||
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"),
|
||||
@@ -159,6 +161,7 @@
|
||||
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("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, "Digest: ", h("strong", null, model.digest ? model.digest.slice(0, 16) + "…" : "unknown")),
|
||||
h("span", null, "Embedding: ", h("strong", null, model.embedding_length || "unknown")),
|
||||
@@ -508,12 +511,27 @@
|
||||
var noticeState = React.useState(null), notice = noticeState[0], setNotice = noticeState[1];
|
||||
var targetDialogState = React.useState(null), targetDialog = targetDialogState[0], setTargetDialog = targetDialogState[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 searchSequence = React.useRef(0);
|
||||
function load() {
|
||||
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); } });
|
||||
}
|
||||
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) { setExternalSearch([]); setExternalSearchBusy(false); return; }
|
||||
setExternalSearchBusy(true);
|
||||
var timer = setTimeout(function () {
|
||||
fetchJSON(API + "/catalog/search?q=" + encodeURIComponent(term) + "&limit=30").then(function (value) {
|
||||
if (sequence === searchSequence.current) setExternalSearch(value.results || []);
|
||||
}).catch(function () { if (sequence === searchSequence.current) setExternalSearch([]); }).finally(function () { if (sequence === searchSequence.current) setExternalSearchBusy(false); });
|
||||
}, 250);
|
||||
return function () { clearTimeout(timer); };
|
||||
}, [query]);
|
||||
function action(kind, name, selectedTarget) {
|
||||
if (kind === "delete" && !window.confirm("Remove " + name + " from Ollama?")) return;
|
||||
if ((kind === "pull" || kind === "redownload") && !selectedTarget) {
|
||||
@@ -543,6 +561,10 @@
|
||||
if (catalogSort === "name") return String(a.name).localeCompare(String(b.name));
|
||||
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 catalogCapabilities = data && data.catalog_filter_options ? data.catalog_filter_options.capabilities || [] : [];
|
||||
@@ -568,7 +590,8 @@
|
||||
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" },
|
||||
h("input", { className: "ollama-search", value: query, placeholder: "Search models, capabilities, or strengths…", onChange: function (event) { setQuery(event.target.value); } }),
|
||||
h("input", { className: "ollama-search", value: query, placeholder: "Search Ollama + Hugging Face models…", onChange: function (event) { setQuery(event.target.value); } }),
|
||||
externalSearchBusy && h("small", { className: "ollama-search-status" }, "Searching Hugging Face…"),
|
||||
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 })),
|
||||
@@ -578,7 +601,7 @@
|
||||
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 === "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." : "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 }); }))
|
||||
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 }); }))
|
||||
);
|
||||
}
|
||||
registry.register("ollama-manager", Page);
|
||||
|
||||
@@ -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.7.2",
|
||||
"version": "1.7.3",
|
||||
"tab": {"path": "/ollama-manager", "position": "after:models"},
|
||||
"entry": "dist/index.js",
|
||||
"css": "dist/style.css",
|
||||
|
||||
@@ -70,6 +70,7 @@ def _discover_ollama_endpoint() -> None:
|
||||
|
||||
LOCAL_OLLAMA = _ollama_base_url()
|
||||
REMOTE_OLLAMA = "https://ollama.com"
|
||||
HUGGINGFACE_API = "https://huggingface.co/api"
|
||||
CATALOG_FILE = "catalog.json"
|
||||
MODEL_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:/-]{0,190}$")
|
||||
MELBOURNE = ZoneInfo("Australia/Melbourne")
|
||||
@@ -747,6 +748,14 @@ def _json_request(url: str, method: str = "GET", payload: Any = None, timeout: i
|
||||
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.3"})
|
||||
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:
|
||||
name = str(name or "").strip()
|
||||
if not MODEL_RE.fullmatch(name):
|
||||
@@ -1361,6 +1370,98 @@ 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 _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):
|
||||
"""Extract the public Ollama tag rows without depending on third-party HTML packages."""
|
||||
|
||||
@@ -2679,6 +2780,23 @@ 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")}
|
||||
|
||||
|
||||
@router.get("/catalog/search")
|
||||
def catalog_search(q: str = "", limit: int = 30) -> dict[str, Any]:
|
||||
query = str(q or "").strip()[:120]
|
||||
if len(query) < 2:
|
||||
return {"query": query, "results": [], "sources": ["ollama", "huggingface"]}
|
||||
ollama = _search_ollama_catalog(query, limit=limit)
|
||||
huggingface = _search_huggingface(query, limit=limit)
|
||||
combined = ollama + huggingface
|
||||
return {
|
||||
"query": query,
|
||||
"results": combined,
|
||||
"ollama_results": ollama,
|
||||
"huggingface_results": huggingface,
|
||||
"sources": ["ollama", "huggingface"],
|
||||
}
|
||||
|
||||
|
||||
@router.post("/pull")
|
||||
def pull_model(body: ModelRequest) -> dict[str, Any]:
|
||||
name = _valid_name(body.name)
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
name: ollama-manager
|
||||
version: 1.7.2
|
||||
version: 1.7.3
|
||||
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:
|
||||
|
||||
@@ -89,6 +89,44 @@ class ValidationHarnessTests(unittest.TestCase):
|
||||
self.assertEqual(response, {"jobs": active})
|
||||
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)
|
||||
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_primary_draft_validators_and_primary_compilation_produce_one_answer(self):
|
||||
body = api.ChatRequest(
|
||||
primary_model="primary",
|
||||
|
||||
Reference in New Issue
Block a user