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:
parent
b107b01a4a
commit
ed52464558
4 changed files with 91 additions and 18 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue