Studio autoload: order folders by resolved quant size; kind-scoped dedupe keys

Fourth round of review follow-ups:

- Local candidates are now resolved before the fallback groups are sorted,
  and multi-quant GGUF folders order by the resolved quant's own size. The
  backend row size sums every quant in a folder, so a folder holding a
  2 GB and an 8 GB quant previously sorted after a 4 GB single-quant model
  even though its 2 GB quant was the smallest loadable artifact.
- The cached/local dedupe keys now carry the model kind. A folder emitting
  both GGUF and safetensors rows shares one path while holding two
  different models, so a failing GGUF row no longer shadows its loadable
  safetensors sibling.

Contract tests updated and extended for both.
This commit is contained in:
shimmyshimmer 2026-07-23 20:01:23 -07:00
commit a928900a2e
2 changed files with 124 additions and 65 deletions

View file

@ -1520,11 +1520,24 @@ function localRowToCandidate(
* the same variants API the picker card uses and the smallest complete,
* auto-loadable quant is chosen. Returns null when no quant can be resolved.
*/
// Unknown sizes (0) sort last so a sizeless row can't shadow a real one.
function sizeOrUnknownBytes(bytes?: number | null): number {
return bytes && bytes > 0 ? bytes : Number.MAX_SAFE_INTEGER;
}
type ResolvedLocalCandidate = {
candidate: AutoLoadCandidate;
/** Size of what would actually load: the resolved quant's own size for a
* multi-quant folder (whose row size_bytes SUMS every quant), else the
* row size. Orders the smallest-first cascade. */
sizeBytes: number;
};
async function resolveLocalRowCandidate(
row: LocalModelInfo,
rememberedVariant: string | null = null,
isSkippedCandidate?: (candidate: AutoLoadCandidate) => boolean,
): Promise<AutoLoadCandidate | null> {
): Promise<ResolvedLocalCandidate | null> {
const isGguf = row.model_format === "gguf";
if (row.capabilities?.requires_variant === true) {
// Only GGUF folders have an automatic quant resolution path.
@ -1550,12 +1563,15 @@ async function resolveLocalRowCandidate(
for (const entry of downloaded) {
const candidate = localRowToCandidate(row, entry.quant);
if (isSkippedCandidate?.(candidate)) continue;
return candidate;
return { candidate, sizeBytes: sizeOrUnknownBytes(entry.size_bytes) };
}
return null;
}
}
return localRowToCandidate(row, isGguf ? rememberedVariant : null);
return {
candidate: localRowToCandidate(row, isGguf ? rememberedVariant : null),
sizeBytes: sizeOrUnknownBytes(row.size_bytes),
};
}
/** Resolve a remembered local model against current backend inventory. */
@ -1952,17 +1968,27 @@ export async function autoLoadOnDeviceModel(): Promise<{
const modelRepos = allModelRepos.filter(isAutoLoadableCachedRepo);
const localRows = allLocalRows.filter(isAutoLoadableLocalRow);
// Dedupe candidates that resolve to the SAME load target (e.g. a custom
// scan folder pointing into an HF cache). Keyed on load targets and
// on-disk paths only: a shared model_id does not mean the same files, and
// a distinct local copy must stay available when the cached copy fails.
// scan folder pointing into an HF cache). Keyed on kind + load target /
// on-disk path: a shared model_id does not mean the same files (a distinct
// local copy stays available when the cached copy fails), and a folder
// emitting both GGUF and safetensors rows shares a path while holding two
// different models, so the format is part of the key.
const seenLoadTargets = new Set<string>();
const markSeen = (...values: (string | null | undefined)[]): void => {
const markSeen = (
kind: LastLocalModelKind,
...values: (string | null | undefined)[]
): void => {
for (const value of values) {
if (value) seenLoadTargets.add(value.toLowerCase());
if (value) seenLoadTargets.add(`${kind}:${value.toLowerCase()}`);
}
};
const isSeen = (...values: (string | null | undefined)[]): boolean =>
values.some((value) => !!value && seenLoadTargets.has(value.toLowerCase()));
const isSeen = (
kind: LastLocalModelKind,
...values: (string | null | undefined)[]
): boolean =>
values.some(
(value) => !!value && seenLoadTargets.has(`${kind}:${value.toLowerCase()}`),
);
try {
if (lastLoaded) {
@ -1977,10 +2003,9 @@ export async function autoLoadOnDeviceModel(): Promise<{
// managed-cache remembered path).
let rememberedCandidate: AutoLoadCandidate | null = null;
try {
rememberedCandidate = await resolveLocalRowCandidate(
row,
lastLoaded.ggufVariant,
);
rememberedCandidate =
(await resolveLocalRowCandidate(row, lastLoaded.ggufVariant))
?.candidate ?? null;
if (rememberedCandidate) {
toast("Loading last used model…", {
id: toastId,
@ -2006,7 +2031,7 @@ export async function autoLoadOnDeviceModel(): Promise<{
} else if (lastLoaded.kind === "gguf") {
const repo = findCachedRepo(ggufRepos, lastLoaded.id);
if (repo && lastLoaded.ggufVariant) {
markSeen(repo.load_id || repo.repo_id, repo.cache_path);
markSeen("gguf", repo.load_id || repo.repo_id, repo.cache_path);
try {
const variants = await listGgufVariants(repo.repo_id, undefined, {
preferLocalCache: true,
@ -2051,7 +2076,7 @@ export async function autoLoadOnDeviceModel(): Promise<{
} else {
const repo = findCachedRepo(modelRepos, lastLoaded.id);
if (repo) {
markSeen(repo.load_id || repo.repo_id, repo.cache_path);
markSeen("model", repo.load_id || repo.repo_id, repo.cache_path);
try {
toast("Loading last used model…", {
id: toastId,
@ -2093,53 +2118,80 @@ export async function autoLoadOnDeviceModel(): Promise<{
type FallbackCandidate =
| { type: "cached-gguf"; repo: CachedGgufRepo; sizeBytes: number }
| { type: "cached-model"; repo: CachedModelRepo; sizeBytes: number }
| { type: "local"; row: LocalModelInfo; sizeBytes: number };
// Unknown sizes (0) sort last so a sizeless row can't shadow a real one.
const sizeOrUnknown = (bytes?: number | null): number =>
bytes && bytes > 0 ? bytes : Number.MAX_SAFE_INTEGER;
| {
type: "local";
row: LocalModelInfo;
candidate: AutoLoadCandidate;
sizeBytes: number;
};
const bySizeAsc = (a: FallbackCandidate, b: FallbackCandidate): number =>
a.sizeBytes - b.sizeBytes;
// Directory-based GGUF rows resolve a quant automatically below; only
// non-GGUF variant-requiring rows have no background resolution path.
const isSkippedAutoLoadCandidate = (c: AutoLoadCandidate): boolean =>
skippedAutoLoadCandidates.has(
autoLoadCandidateKey(c.kind, c.id, c.ggufVariant),
);
// Directory-based GGUF rows resolve a quant automatically; only non-GGUF
// variant-requiring rows have no background resolution path.
const cascadeLocalRows = localRows.filter(
(row) =>
row.model_format === "gguf" ||
row.capabilities?.requires_variant !== true,
);
// Resolve local candidates BEFORE ordering: a multi-quant folder's row
// size_bytes sums every quant in it, so the cascade must order on the
// resolved quant's own size or a folder with a small quant would lose to
// a larger single-quant model.
const localEntries = (
await Promise.all(
cascadeLocalRows.map(
async (row): Promise<FallbackCandidate | null> => {
try {
const resolved = await resolveLocalRowCandidate(
row,
null,
isSkippedAutoLoadCandidate,
);
if (!resolved) return null;
return {
type: "local" as const,
row,
candidate: resolved.candidate,
sizeBytes: resolved.sizeBytes,
};
} catch {
hadNonTrustFailure = true;
return null;
}
},
),
)
).filter((entry): entry is FallbackCandidate => entry !== null);
const ggufGroup: FallbackCandidate[] = [
...ggufRepos.map((repo) => ({
type: "cached-gguf" as const,
repo,
sizeBytes: sizeOrUnknown(repo.size_bytes),
sizeBytes: sizeOrUnknownBytes(repo.size_bytes),
})),
...cascadeLocalRows
.filter((row) => row.model_format === "gguf")
.map((row) => ({
type: "local" as const,
row,
sizeBytes: sizeOrUnknown(row.size_bytes),
})),
...localEntries.filter(
(entry) => entry.type === "local" && entry.candidate.kind === "gguf",
),
].sort(bySizeAsc);
const modelGroup: FallbackCandidate[] = [
...modelRepos.map((repo) => ({
type: "cached-model" as const,
repo,
sizeBytes: sizeOrUnknown(repo.size_bytes),
sizeBytes: sizeOrUnknownBytes(repo.size_bytes),
})),
...cascadeLocalRows
.filter((row) => row.model_format !== "gguf")
.map((row) => ({
type: "local" as const,
row,
sizeBytes: sizeOrUnknown(row.size_bytes),
})),
...localEntries.filter(
(entry) => entry.type === "local" && entry.candidate.kind === "model",
),
].sort(bySizeAsc);
for (const candidate of [...ggufGroup, ...modelGroup]) {
if (loadAttempts >= MAX_AUTO_LOAD_ATTEMPTS) break;
if (candidate.type === "cached-gguf") {
const repo = candidate.repo;
markSeen(repo.load_id || repo.repo_id, repo.cache_path);
markSeen("gguf", repo.load_id || repo.repo_id, repo.cache_path);
try {
const variants = await listGgufVariants(repo.repo_id, undefined, {
preferLocalCache: true,
@ -2181,7 +2233,7 @@ export async function autoLoadOnDeviceModel(): Promise<{
}
if (candidate.type === "cached-model") {
const repo = candidate.repo;
markSeen(repo.load_id || repo.repo_id, repo.cache_path);
markSeen("model", repo.load_id || repo.repo_id, repo.cache_path);
if (
skippedAutoLoadCandidates.has(
autoLoadCandidateKey("model", repo.repo_id),
@ -2210,30 +2262,15 @@ export async function autoLoadOnDeviceModel(): Promise<{
continue;
}
const row = candidate.row;
if (isSeen(row.load_id, row.id, row.path)) {
const localCandidate = candidate.candidate;
if (isSeen(localCandidate.kind, row.load_id, row.id, row.path)) {
continue;
}
markSeen(localCandidate.kind, row.load_id, row.id, row.path);
if (isSkippedAutoLoadCandidate(localCandidate)) {
continue;
}
markSeen(row.load_id, row.id, row.path);
try {
const localCandidate = await resolveLocalRowCandidate(row, null, (c) =>
skippedAutoLoadCandidates.has(
autoLoadCandidateKey(c.kind, c.id, c.ggufVariant),
),
);
if (!localCandidate) {
continue;
}
if (
skippedAutoLoadCandidates.has(
autoLoadCandidateKey(
localCandidate.kind,
localCandidate.id,
localCandidate.ggufVariant,
),
)
) {
continue;
}
if (await loadAutoLoadCandidate(localCandidate)) {
return { loaded: true, blockedByTrustRemoteCode: false };
}

View file

@ -572,8 +572,12 @@ def test_autoload_deduplicates_cached_and_local_candidates():
available when the cached copy fails or has no usable quant."""
auto_load = _autoload_section()
assert "const seenLoadTargets = new Set<string>()" in auto_load
assert "markSeen(repo.load_id || repo.repo_id, repo.cache_path)" in auto_load
assert "isSeen(row.load_id, row.id, row.path)" in auto_load
# Keys carry the model kind: a folder emitting both GGUF and safetensors
# rows shares a path while holding two different models.
assert "seenLoadTargets.add(`${kind}:${value.toLowerCase()}`)" in auto_load
assert 'markSeen("gguf", repo.load_id || repo.repo_id, repo.cache_path)' in auto_load
assert 'markSeen("model", repo.load_id || repo.repo_id, repo.cache_path)' in auto_load
assert "isSeen(localCandidate.kind, row.load_id, row.id, row.path)" in auto_load
# The repo-id-based dedupe that shadowed distinct local copies is gone.
assert "isSeen(row.load_id, row.id, row.path, row.model_id)" not in auto_load
assert "markSeen(repo.repo_id," not in auto_load
@ -590,7 +594,7 @@ def test_local_quant_resolution_skips_failed_quants():
assert "if (isSkippedCandidate?.(candidate)) continue;" in resolve_fn
# The fallback loop feeds the skip set into resolution.
auto_load = _autoload_section()
assert "await resolveLocalRowCandidate(row, null, (c) =>" in auto_load
assert "isSkippedAutoLoadCandidate," in auto_load
def test_autoload_trust_guard_still_blocks_background_loads():
@ -671,7 +675,7 @@ def test_directory_gguf_rows_resolve_variant_like_picker():
# 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, null, (c) =>" in auto_load
assert "await resolveLocalRowCandidate(" in auto_load
def test_remembered_local_failure_does_not_block_folder_fallback():
@ -686,3 +690,21 @@ def test_remembered_local_failure_does_not_block_folder_fallback():
"markSeen(" not in remembered_block
), "remembered-local retry must not pre-mark the row as deduped"
assert "rememberedCandidate?.ggufVariant ?? lastLoaded.ggufVariant" in remembered_block
def test_local_fallback_orders_by_resolved_quant_size():
"""A GGUF folder row's size_bytes sums every quant in the folder, so the
smallest-first cascade must order local candidates by the resolved
quant's own size; otherwise a folder with a small quant loses to a
larger single-quant model."""
src = _read("features/chat/api/chat-adapter.ts")
resolve_fn = src.split("async function resolveLocalRowCandidate", 1)[1]
resolve_fn = resolve_fn.split("\nfunction ", 1)[0]
assert "sizeBytes: sizeOrUnknownBytes(entry.size_bytes)" in resolve_fn
auto_load = _autoload_section()
# Local candidates are resolved BEFORE the groups are sorted.
assert "const localEntries = (" in auto_load
assert auto_load.index("const localEntries = (") < auto_load.index(
"const ggufGroup: FallbackCandidate[]"
)
assert "sizeBytes: resolved.sizeBytes" in auto_load