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.
This commit is contained in:
parent
db7855538a
commit
bf46d8bb60
2 changed files with 45 additions and 7 deletions
|
|
@ -55,6 +55,7 @@ import {
|
|||
type PendingImageEditReference,
|
||||
type RagAutoInject,
|
||||
GPU_LAYERS_AUTO,
|
||||
isLocalModelPath,
|
||||
loadedGpuMemoryFields,
|
||||
reconcilePersistedGpuIds,
|
||||
resolveLoadedSpeculativeSettings,
|
||||
|
|
@ -1439,14 +1440,18 @@ function findCachedRepo<T extends { repo_id: string }>(
|
|||
|
||||
/**
|
||||
* Managed-cache rows eligible for background auto-load: complete, not
|
||||
* hidden infrastructure, and not declared non-chat by the backend.
|
||||
* hidden infrastructure, not declared non-chat by the backend, and not an
|
||||
* adapter (loading an adapter resolves its base model, which for an
|
||||
* uncached Hub base would start an implicit remote fetch).
|
||||
*/
|
||||
function isAutoLoadableCachedRepo(repo: {
|
||||
repo_id: string;
|
||||
partial?: boolean;
|
||||
model_format?: string | null;
|
||||
capabilities?: { can_chat?: boolean } | null;
|
||||
}): boolean {
|
||||
if (repo.partial) return false;
|
||||
if (repo.model_format === "adapter") return false;
|
||||
if (repo.capabilities?.can_chat === false) return false;
|
||||
return !isHiddenModelId(repo.repo_id);
|
||||
}
|
||||
|
|
@ -1524,7 +1529,12 @@ async function resolveLocalRowCandidate(
|
|||
// Only GGUF folders have an automatic quant resolution path.
|
||||
if (!isGguf) return null;
|
||||
if (!rememberedVariant) {
|
||||
const variants = await listGgufVariants(row.model_id || row.id, undefined, {
|
||||
// Scan the folder the row will actually load from: the cache-first
|
||||
// prefer_local_cache flow could return a quant present in the HF
|
||||
// cache but missing at row's own path. A local-path repo id routes
|
||||
// the backend straight to the filesystem scan of that folder.
|
||||
const variantScanTarget = isLocalModelPath(row.id) ? row.id : row.path;
|
||||
const variants = await listGgufVariants(variantScanTarget, undefined, {
|
||||
preferLocalCache: true,
|
||||
localPath: row.path,
|
||||
});
|
||||
|
|
@ -1952,9 +1962,13 @@ export async function autoLoadOnDeviceModel(): Promise<{
|
|||
matchesRememberedLocalRow(candidateRow, lastLoaded),
|
||||
);
|
||||
if (row) {
|
||||
markSeen(row.load_id, row.id, row.path, row.model_id);
|
||||
// Not marked seen here: if this exact quant fails, the fallback
|
||||
// loop may still pick another complete quant from the same folder
|
||||
// (only the failed candidate key below is excluded, mirroring the
|
||||
// managed-cache remembered path).
|
||||
let rememberedCandidate: AutoLoadCandidate | null = null;
|
||||
try {
|
||||
const rememberedCandidate = await resolveLocalRowCandidate(
|
||||
rememberedCandidate = await resolveLocalRowCandidate(
|
||||
row,
|
||||
lastLoaded.ggufVariant,
|
||||
);
|
||||
|
|
@ -1972,9 +1986,10 @@ export async function autoLoadOnDeviceModel(): Promise<{
|
|||
hadNonTrustFailure = true;
|
||||
skippedAutoLoadCandidates.add(
|
||||
autoLoadCandidateKey(
|
||||
row.model_format === "gguf" ? "gguf" : "model",
|
||||
rememberedCandidate?.kind ??
|
||||
(row.model_format === "gguf" ? "gguf" : "model"),
|
||||
row.id,
|
||||
lastLoaded.ggufVariant,
|
||||
rememberedCandidate?.ggufVariant ?? lastLoaded.ggufVariant,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -528,6 +528,9 @@ def test_autoload_filters_match_picker_policy():
|
|||
cached_fn = src.split("function isAutoLoadableCachedRepo", 1)[1]
|
||||
cached_fn = cached_fn.split("\nconst ", 1)[0]
|
||||
assert "repo.partial" in cached_fn
|
||||
# Cached adapter repos are chat-capable too and resolve a base model on
|
||||
# load, so they must be excluded exactly like local adapter rows.
|
||||
assert 'repo.model_format === "adapter"' in cached_fn
|
||||
assert "repo.capabilities?.can_chat === false" in cached_fn
|
||||
assert "isHiddenModelId(repo.repo_id)" in cached_fn
|
||||
|
||||
|
|
@ -640,10 +643,30 @@ def test_directory_gguf_rows_resolve_variant_like_picker():
|
|||
resolve_fn = resolve_fn.split("\nfunction ", 1)[0]
|
||||
assert "row.capabilities?.requires_variant === true" in resolve_fn
|
||||
assert "if (!isGguf) return null;" in resolve_fn
|
||||
assert "listGgufVariants(row.model_id || row.id" in resolve_fn
|
||||
# Quants must be resolved from the folder the row will load from, not
|
||||
# from a same-id HF cache repo whose quants may be absent locally.
|
||||
assert (
|
||||
"const variantScanTarget = isLocalModelPath(row.id) ? row.id : row.path;"
|
||||
in resolve_fn
|
||||
)
|
||||
assert "listGgufVariants(variantScanTarget" in resolve_fn
|
||||
assert "localPath: row.path" in resolve_fn
|
||||
assert "entry.downloaded && !entry.partial && isAutoLoadableGgufVariant(entry)" in resolve_fn
|
||||
# The cascade must keep directory GGUF rows as candidates.
|
||||
auto_load = src.split("async function autoLoadOnDeviceModel", 1)[1]
|
||||
assert 'row.model_format === "gguf" ||' in auto_load
|
||||
assert "await resolveLocalRowCandidate(row)" in auto_load
|
||||
|
||||
|
||||
def test_remembered_local_failure_does_not_block_folder_fallback():
|
||||
"""A failed remembered local quant must exclude only that exact candidate
|
||||
key, not mark the whole row as seen; otherwise a folder with another
|
||||
complete quant can never fall back and Send falsely reports no model."""
|
||||
src = _read("features/chat/api/chat-adapter.ts")
|
||||
auto_load = src.split("async function autoLoadOnDeviceModel", 1)[1]
|
||||
remembered_block = auto_load.split("isManagedCacheSource(lastLoaded.source)", 1)[1]
|
||||
remembered_block = remembered_block.split('} else if (lastLoaded.kind === "gguf")', 1)[0]
|
||||
assert "markSeen(" not in remembered_block, (
|
||||
"remembered-local retry must not pre-mark the row as deduped"
|
||||
)
|
||||
assert "rememberedCandidate?.ggufVariant ?? lastLoaded.ggufVariant" in remembered_block
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue