Trust per-call flags over env for hub calls and gate formats on the backend platform only

get_paths_info performs no offline-mode check and HF_HUB_OFFLINE is baked
into hub constants at import, so the env guard cannot stop it in the parent
process; local-only GGUF reuse now skips the remote size verification
per-call, and a truncated cache fails the load into candidate failover
instead. Platform format gates apply only once the backend-reported
platform is fetched: the browser fallback can describe a different machine
(Mac browser against a remote Linux backend), which would wrongly report no
model; while unknown, candidates flow ungated and a chat-only backend
rejects ineligible ones at validation without consuming an attempt.
snapshot_size_bytes now uses the resolvers' complete-revision predicate
(config plus weights in the same revision) so a weight-only newer revision
is not sized while the older complete one loads. Cached-repo variant scans
are memoized per run, so a stalled repository times out once instead of
gating Send through a second identical bounded request.
This commit is contained in:
Unsloth 2026-07-27 00:52:38 -07:00
commit ed52464558
4 changed files with 91 additions and 18 deletions

View file

@ -5350,10 +5350,16 @@ class LlamaCppBackend:
# Resolve by variant so a newer revision's filename does not hide
# the complete older copy. Size-check against that older snapshot's
# own revision when its metadata remains available.
# Local-only loads skip the remote size check outright: the
# hub API behind it (get_paths_info) performs no offline-mode
# check and HF_HUB_OFFLINE is baked into hub constants at
# import, so only a per-call skip reliably keeps this off the
# network. A truncated cache then fails the load into
# candidate failover instead of being caught up front.
cached_main = cached_gguf_for_load(
hf_repo,
hf_variant,
verify_sizes = True,
verify_sizes = not local_files_only,
hf_token = hf_token,
)
else:
@ -5361,7 +5367,10 @@ class LlamaCppBackend:
cached_main = (
candidate[0]
if candidate is not None
and _cached_candidate_matches_revision_size(hf_repo, candidate, hf_token)
and (
local_files_only
or _cached_candidate_matches_revision_size(hf_repo, candidate, hf_token)
)
else None
)
if cached_main is not None:

View file

@ -443,6 +443,7 @@ def _repo_non_gguf_model_payload(repo_info) -> _CachedNonGgufPayload:
# rather than only the all-revisions total.
rev_category_sizes: dict[str, dict[str, int]] = {}
rev_snapshot_mtimes: dict[str, float] = {}
rev_has_config: dict[str, bool] = {}
has_config = False
has_adapter_config = False
has_adapter_weights = False
@ -474,6 +475,7 @@ def _repo_non_gguf_model_payload(repo_info) -> _CachedNonGgufPayload:
continue
if name == "config.json":
has_config = True
rev_has_config[rev_id] = True
continue
if name == "adapter_config.json":
has_adapter_config = True
@ -520,13 +522,21 @@ def _repo_non_gguf_model_payload(repo_info) -> _CachedNonGgufPayload:
size_bytes = sum(size for size, _mtime in selected_blobs.values())
# The one snapshot a load resolves (newest snapshot-dir mtime) among the
# revisions actually holding selected-format weights; falls back to the
# all-revisions total when no revision reports one.
# revisions holding selected-format weights. For safetensors rows the
# snapshot resolvers additionally require config.json in the SAME
# revision, so sizing must use that predicate too: a weight-only newer
# revision would otherwise be sized while the older complete revision is
# what actually loads. Falls back to weight-only revisions, then to the
# all-revisions total.
weight_revs = [
rev_id
for rev_id, sizes in rev_category_sizes.items()
if sizes.get(selected_category, 0) > 0
]
if selected_category == "safetensors":
complete_revs = [rev_id for rev_id in weight_revs if rev_has_config.get(rev_id, False)]
if complete_revs:
weight_revs = complete_revs
if weight_revs:
newest_rev = max(weight_revs, key = lambda rev_id: rev_snapshot_mtimes.get(rev_id, 0.0))
snapshot_size_bytes = rev_category_sizes[newest_rev].get(selected_category, 0)

View file

@ -2140,12 +2140,16 @@ export async function autoLoadOnDeviceModel(): Promise<{
};
}
// The platform snapshot the picker's format gates key on: hydrated by the
// bounded best-effort prefetch above, else the store's boot-detected
// client-side values (the same defaults the whole UI uses pre-hydration).
// The platform snapshot the picker's format gates key on. Format gates
// only apply once the BACKEND-reported platform has been fetched: the
// boot-detected fallback describes the browser, which may differ from the
// host (a Mac browser against a remote Linux/CUDA backend would wrongly
// gate out every cached non-GGUF candidate). While the platform is
// unknown, candidates flow ungated and an actually chat-only backend
// rejects ineligible ones at validation without consuming an attempt.
const platformState = usePlatformStore.getState();
const platform: AutoLoadPlatform = {
chatOnly: platformState.isChatOnly(),
chatOnly: platformState.fetched ? platformState.isChatOnly() : false,
isMac: platformState.deviceType === "mac",
};
const ggufRepos = allGgufRepos.filter(isAutoLoadableCachedRepo);
@ -2184,6 +2188,31 @@ export async function autoLoadOnDeviceModel(): Promise<{
),
);
// One variant scan per cached repo per run: the remembered-model lookup
// and the cascade share the same result, so a stalled repository times
// out ONCE instead of gating Send through a second identical bounded
// request. Rejections are memoized too, on purpose: a repo whose scan
// already failed this run is not rescanned.
const repoVariantScans = new Map<
string,
ReturnType<typeof listGgufVariantsBounded>
>();
const scanRepoVariants = (
repoId: string,
localPath: string | null | undefined,
) => {
const key = `${repoId}|${localPath ?? ""}`;
let pending = repoVariantScans.get(key);
if (!pending) {
pending = listGgufVariantsBounded(repoId, {
preferLocalCache: true,
localPath,
});
repoVariantScans.set(key, pending);
}
return pending;
};
try {
if (lastLoaded) {
if (!isManagedCacheSource(lastLoaded.source)) {
@ -2230,10 +2259,7 @@ export async function autoLoadOnDeviceModel(): Promise<{
// may still pick another complete quant from this repo (only the
// failed candidate key below is excluded).
try {
const variants = await listGgufVariantsBounded(repo.repo_id, {
preferLocalCache: true,
localPath: repo.cache_path,
});
const variants = await scanRepoVariants(repo.repo_id, repo.cache_path);
const variant = variants.variants.find(
(entry) =>
entry.downloaded &&
@ -2388,10 +2414,7 @@ export async function autoLoadOnDeviceModel(): Promise<{
const resolveCachedGgufEntry = async (
repo: CachedGgufRepo,
): Promise<Extract<FallbackCandidate, { type: "cached-gguf" }> | null> => {
const variants = await listGgufVariantsBounded(repo.repo_id, {
preferLocalCache: true,
localPath: repo.cache_path,
});
const variants = await scanRepoVariants(repo.repo_id, repo.cache_path);
const downloaded = variants.variants
.filter(
(v) => v.downloaded && !v.partial && isAutoLoadableGgufVariant(v),

View file

@ -297,8 +297,13 @@ def test_chat_autoload_scopes_variant_lookup_to_cached_repo_path():
retained from a previously selected Hugging Face cache."""
src = _read("features/chat/api/chat-adapter.ts")
auto_load = src.split("async function autoLoadOnDeviceModel", 1)[1]
assert auto_load.count("preferLocalCache: true") >= 2
assert auto_load.count("localPath: repo.cache_path") >= 2
# Both cached-repo lookups (remembered model and cascade) route through
# the memoized scanRepoVariants, which carries the cache-scoped params.
scan_fn = auto_load.split("const scanRepoVariants = (", 1)[1]
scan_fn = scan_fn.split("return pending;", 1)[0]
assert "preferLocalCache: true" in scan_fn
assert "localPath," in scan_fn
assert auto_load.count("await scanRepoVariants(repo.repo_id, repo.cache_path)") == 2
chat_api = _read("features/chat/api/chat-api.ts")
variants_fn = chat_api.split("export async function listGgufVariants", 1)[1]
@ -1291,6 +1296,32 @@ def test_generation_and_scan_paths_stay_bounded_and_local():
assert adapter.count("expandSeenValues(value)") == 2
def test_local_only_gguf_reuse_and_platform_gates_are_authoritative():
"""Round-18 gates. get_paths_info performs no offline-mode check and
HF_HUB_OFFLINE is baked into hub constants at import, so local-only GGUF
reuse skips the remote size verification per-call instead of relying on
the env guard. Platform format gates only apply once the BACKEND-reported
platform is fetched (the browser fallback may describe a different
machine). snapshot_size_bytes uses the resolvers' complete-revision
predicate (config plus weights in the SAME revision). Cached-repo variant
scans are memoized per run so a stalled repo times out once."""
llama = _read_backend("core/inference/llama_cpp.py")
assert "verify_sizes = not local_files_only," in llama
reuse = llama.split("_cached_complete_candidate(hf_repo, gguf_filename, gguf_extra_shards)", 1)[1]
reuse = reuse.split("cached_main is not None", 1)[0]
assert "local_files_only" in reuse and "_cached_candidate_matches_revision_size" in reuse
adapter = _read("features/chat/api/chat-adapter.ts")
assert "chatOnly: platformState.fetched ? platformState.isChatOnly() : false," in adapter
assert "const repoVariantScans = new Map<" in adapter
assert "const scanRepoVariants = (" in adapter
assert adapter.count("await scanRepoVariants(repo.repo_id, repo.cache_path)") == 2
inventory = _read_backend("hub/services/models/cache_inventory.py")
assert "rev_has_config" in inventory
assert 'if selected_category == "safetensors":' in inventory
def test_gguf_background_loads_never_download_companions():
"""A cached GGUF load can still fetch from the Hub through its optional
companions (mmproj, MTP drafter) or a cache-miss main quant. Background