Skip to content

Commit bf46d8b

Browse files
shimmyshimmerUnsloth
authored andcommitted
Studio autoload: folder quant fallback, scan own path, skip cached adapters
Second round of review follow-ups, verified against the backend services: - A failed remembered local quant now excludes only that exact candidate key instead of marking the whole row seen, so another complete quant in the same folder can still load (mirrors the managed-cache remembered path). - Quant resolution for a local GGUF folder scans the folder itself via a local-path repo id; the cache-first prefer_local_cache flow could return a cache quant missing from the folder when the row also has a Hub model_id. - Cached adapter repos are chat-capable in the cached inventory and resolve a base model on load, so they are excluded from background auto-load like local adapter rows. Contract tests extended for all three.
1 parent db78555 commit bf46d8b

2 files changed

Lines changed: 45 additions & 7 deletions

File tree

studio/frontend/src/features/chat/api/chat-adapter.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import {
5555
type PendingImageEditReference,
5656
type RagAutoInject,
5757
GPU_LAYERS_AUTO,
58+
isLocalModelPath,
5859
loadedGpuMemoryFields,
5960
reconcilePersistedGpuIds,
6061
resolveLoadedSpeculativeSettings,
@@ -1439,14 +1440,18 @@ function findCachedRepo<T extends { repo_id: string }>(
14391440

14401441
/**
14411442
* Managed-cache rows eligible for background auto-load: complete, not
1442-
* hidden infrastructure, and not declared non-chat by the backend.
1443+
* hidden infrastructure, not declared non-chat by the backend, and not an
1444+
* adapter (loading an adapter resolves its base model, which for an
1445+
* uncached Hub base would start an implicit remote fetch).
14431446
*/
14441447
function isAutoLoadableCachedRepo(repo: {
14451448
repo_id: string;
14461449
partial?: boolean;
1450+
model_format?: string | null;
14471451
capabilities?: { can_chat?: boolean } | null;
14481452
}): boolean {
14491453
if (repo.partial) return false;
1454+
if (repo.model_format === "adapter") return false;
14501455
if (repo.capabilities?.can_chat === false) return false;
14511456
return !isHiddenModelId(repo.repo_id);
14521457
}
@@ -1524,7 +1529,12 @@ async function resolveLocalRowCandidate(
15241529
// Only GGUF folders have an automatic quant resolution path.
15251530
if (!isGguf) return null;
15261531
if (!rememberedVariant) {
1527-
const variants = await listGgufVariants(row.model_id || row.id, undefined, {
1532+
// Scan the folder the row will actually load from: the cache-first
1533+
// prefer_local_cache flow could return a quant present in the HF
1534+
// cache but missing at row's own path. A local-path repo id routes
1535+
// the backend straight to the filesystem scan of that folder.
1536+
const variantScanTarget = isLocalModelPath(row.id) ? row.id : row.path;
1537+
const variants = await listGgufVariants(variantScanTarget, undefined, {
15281538
preferLocalCache: true,
15291539
localPath: row.path,
15301540
});
@@ -1952,9 +1962,13 @@ export async function autoLoadOnDeviceModel(): Promise<{
19521962
matchesRememberedLocalRow(candidateRow, lastLoaded),
19531963
);
19541964
if (row) {
1955-
markSeen(row.load_id, row.id, row.path, row.model_id);
1965+
// Not marked seen here: if this exact quant fails, the fallback
1966+
// loop may still pick another complete quant from the same folder
1967+
// (only the failed candidate key below is excluded, mirroring the
1968+
// managed-cache remembered path).
1969+
let rememberedCandidate: AutoLoadCandidate | null = null;
19561970
try {
1957-
const rememberedCandidate = await resolveLocalRowCandidate(
1971+
rememberedCandidate = await resolveLocalRowCandidate(
19581972
row,
19591973
lastLoaded.ggufVariant,
19601974
);
@@ -1972,9 +1986,10 @@ export async function autoLoadOnDeviceModel(): Promise<{
19721986
hadNonTrustFailure = true;
19731987
skippedAutoLoadCandidates.add(
19741988
autoLoadCandidateKey(
1975-
row.model_format === "gguf" ? "gguf" : "model",
1989+
rememberedCandidate?.kind ??
1990+
(row.model_format === "gguf" ? "gguf" : "model"),
19761991
row.id,
1977-
lastLoaded.ggufVariant,
1992+
rememberedCandidate?.ggufVariant ?? lastLoaded.ggufVariant,
19781993
),
19791994
);
19801995
}

tests/studio/test_model_picker_contracts.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,9 @@ def test_autoload_filters_match_picker_policy():
528528
cached_fn = src.split("function isAutoLoadableCachedRepo", 1)[1]
529529
cached_fn = cached_fn.split("\nconst ", 1)[0]
530530
assert "repo.partial" in cached_fn
531+
# Cached adapter repos are chat-capable too and resolve a base model on
532+
# load, so they must be excluded exactly like local adapter rows.
533+
assert 'repo.model_format === "adapter"' in cached_fn
531534
assert "repo.capabilities?.can_chat === false" in cached_fn
532535
assert "isHiddenModelId(repo.repo_id)" in cached_fn
533536

@@ -640,10 +643,30 @@ def test_directory_gguf_rows_resolve_variant_like_picker():
640643
resolve_fn = resolve_fn.split("\nfunction ", 1)[0]
641644
assert "row.capabilities?.requires_variant === true" in resolve_fn
642645
assert "if (!isGguf) return null;" in resolve_fn
643-
assert "listGgufVariants(row.model_id || row.id" in resolve_fn
646+
# Quants must be resolved from the folder the row will load from, not
647+
# from a same-id HF cache repo whose quants may be absent locally.
648+
assert (
649+
"const variantScanTarget = isLocalModelPath(row.id) ? row.id : row.path;"
650+
in resolve_fn
651+
)
652+
assert "listGgufVariants(variantScanTarget" in resolve_fn
644653
assert "localPath: row.path" in resolve_fn
645654
assert "entry.downloaded && !entry.partial && isAutoLoadableGgufVariant(entry)" in resolve_fn
646655
# The cascade must keep directory GGUF rows as candidates.
647656
auto_load = src.split("async function autoLoadOnDeviceModel", 1)[1]
648657
assert 'row.model_format === "gguf" ||' in auto_load
649658
assert "await resolveLocalRowCandidate(row)" in auto_load
659+
660+
661+
def test_remembered_local_failure_does_not_block_folder_fallback():
662+
"""A failed remembered local quant must exclude only that exact candidate
663+
key, not mark the whole row as seen; otherwise a folder with another
664+
complete quant can never fall back and Send falsely reports no model."""
665+
src = _read("features/chat/api/chat-adapter.ts")
666+
auto_load = src.split("async function autoLoadOnDeviceModel", 1)[1]
667+
remembered_block = auto_load.split("isManagedCacheSource(lastLoaded.source)", 1)[1]
668+
remembered_block = remembered_block.split('} else if (lastLoaded.kind === "gguf")', 1)[0]
669+
assert "markSeen(" not in remembered_block, (
670+
"remembered-local retry must not pre-mark the row as deduped"
671+
)
672+
assert "rememberedCandidate?.ggufVariant ?? lastLoaded.ggufVariant" in remembered_block

0 commit comments

Comments
 (0)