From 017446163227e5a006ee79c1d4307c9a972a1c81 Mon Sep 17 00:00:00 2001 From: danielhanchen Date: Wed, 29 Jul 2026 08:57:14 +0000 Subject: [PATCH] Resolve a local quant folder's variants by its path A local row carries a repo id only inside the HF cache: getLocalHubId returns null unless the source is hf_cache. A plain folder of quants, under the models dir, a custom folder or LM Studio, therefore has none, while the backend still marks it as needing one, since requires_variant is scan_path.is_dir(), and leaves format_variant null because a directory is not the single-file case. settingsGgufVariantForRow then returns null, the guard is entered, the lookup is skipped for want of an id, and the toast is all that is left. The row menu offers Settings on every non-dataset row, so for these folders it could never do anything. The listing takes a path in that position and scans it, before the repo-id validation that would otherwise reject one, and that is already the request the on-device card makes: its fetch state is keyed on the model id, which for a local row without a hub id is the load path. So both surfaces now choose from the same quant set, and the settings key, the row's load id, does not move. --- studio/frontend/src/features/hub/hub-page.tsx | 9 +++++++- tests/studio/test_model_picker_contracts.py | 23 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/studio/frontend/src/features/hub/hub-page.tsx b/studio/frontend/src/features/hub/hub-page.tsx index 2dd4569d37..a67c6cab50 100644 --- a/studio/frontend/src/features/hub/hub-page.tsx +++ b/studio/frontend/src/features/hub/hub-page.tsx @@ -1343,7 +1343,14 @@ export function ModelsPage() { // the server mirror is wrong too. Resolve it as the on-device card does. let ggufVariant = settingsGgufVariantForRow(row); if (!ggufVariant && row.isGguf && row.capabilities.requiresVariant) { - const repoId = row.kind === "cache" ? row.repoId : (row.repoId ?? null); + // A local row only carries a repo id when it sits in the HF cache, so a + // plain folder of quants (the models dir, a custom folder, LM Studio) has + // none while still being marked as needing one. The listing takes a path + // in the same position and scans it, which is exactly what the on-device + // card already does for these rows, so without the fallback this menu + // entry could only ever reach the "couldn't determine which quant" toast. + const repoId = + row.kind === "cache" ? row.repoId : (row.repoId ?? row.path ?? null); if (repoId) { try { const [res] = await Promise.all([ diff --git a/tests/studio/test_model_picker_contracts.py b/tests/studio/test_model_picker_contracts.py index d8739c269a..bbb782f5c2 100644 --- a/tests/studio/test_model_picker_contracts.py +++ b/tests/studio/test_model_picker_contracts.py @@ -1389,6 +1389,29 @@ def test_settings_open_reads_status_before_resolving_the_quant(): assert "refreshResidentModelStatus(), ]);" in page +def test_a_local_quant_folder_resolves_its_variants_by_path(): + """A local row carries a repo id only inside the HF cache, so a plain folder of + quants has none while still being marked as needing one, and the row menu's + Settings could then only reach the error toast.""" + hub = " ".join(_read("features/hub/hub-page.tsx").split()) + assert ( + 'const repoId = row.kind === "cache" ? row.repoId : (row.repoId ?? row.path ?? null);' + in hub + ) + # The on-device card already lists by path for the same rows; this is the + # request it makes, so both surfaces choose from one set of quants. + card = " ".join(_read("features/hub/catalog/local-on-device-card.tsx").split()) + assert "repoId: modelId, hfToken, preferLocalCache: true, localPath: localGgufPath," in card + # The backend takes a path in the repo_id position and scans it, before the + # repo-id validation that would otherwise 400 on a path. + variants = ( + WORKDIR / "studio" / "backend" / "hub" / "services" / "models" / "gguf_variants.py" + ).read_text(encoding = "utf-8") + scan = variants.split("if is_local_path(repo_id):", 1) + assert len(scan) == 2, "the local-path branch this leans on" + assert "_is_valid_repo_id(repo_id)" in scan[1], "the branch has to come first" + + def test_cached_repo_settings_key_follows_the_row_not_the_view(): """A repo in an inactive HF cache loads by snapshot path while its settings are keyed by repo id."""