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.
This commit is contained in:
danielhanchen 2026-07-29 08:57:14 +00:00
commit 0174461632
2 changed files with 31 additions and 1 deletions

View file

@ -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([

View file

@ -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."""