Compare commits

...
Sign in to create a new pull request.

40 commits

Author SHA1 Message Date
pre-commit-ci[bot]
eabeff5aea [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-07-27 12:30:29 +00:00
Unsloth
571a0ef35f Merge main: rewire deep research send to the on-device auto-load
The deep research path from main called the removed autoLoadSmallestModel;
it now uses autoLoadOnDeviceModel with the same no-download guarantee and
duplicate-toast suppression as the plain send path. Test conflict resolved
by keeping both the autoload contract section and main's Vulkan picker
test.
2026-07-27 05:29:43 -07:00
Unsloth
3026ff9e79 Preflight local-only loads against the resolved path and keep GGUF rows on GGUF snapshots
The load route's sidecar tier probe reached _remote_lora_base's raw HTTP
request and Hub config reads, and the training guard's memory estimation
can call hf model_info; neither honors offline mode, so both now run
against the resolved snapshot path for local-only loads (a local path
resolves from config.json on disk and skips the remote adapter probe).
GGUF cached rows select a GGUF-bearing snapshot (top level or one folder
deep) instead of reusing the safetensors-preferring model helper, so a
mixed repo's safetensors revision cannot become the GGUF row's load target
while the UI reports a quant.
2026-07-27 05:24:22 -07:00
pre-commit-ci[bot]
10bd745d1f [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-07-27 07:53:23 +00:00
Unsloth
ed52464558 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.
2026-07-27 00:52:38 -07:00
pre-commit-ci[bot]
b107b01a4a [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-07-27 07:25:10 +00:00
Unsloth
00566662c6 Bound scan gates, keep generation fallbacks local, and align inactive-cache load targets
Inactive-cache rows now emit the newest snapshot holding config.json plus
safetensors weights as their load_id, since the load consumes that path
directly and a metadata-only newest revision would fail an eligible model.
Ordinary offline guards no longer join a forced window (their block runs
correctly under either env state), so concurrent online work sees the
narrowest possible override; forced guards still share windows. The
generation-time native template reload honors the load's local_files_only
flag and resolved path, so a tool-calling turn on an auto-loaded model
cannot download tokenizer files mid-Send. Every GGUF variant scan behind
the model-kind gate now runs with an abortable 30s timeout (matching the
inventory calls' own bound) so one hung request cannot gate Send forever.
An HF cache snapshot registered as a custom scan folder dedupes against its
cached row by expanding snapshot paths to their cache root, so shared files
consume one load attempt.
2026-07-27 00:24:24 -07:00
Unsloth
224b90eb02 Make forced offline hub-specific and close the parent preflight gap
A forced guard now only no-ops when HF_HUB_OFFLINE itself is truthy:
huggingface_hub ignores TRANSFORMERS_OFFLINE, so with only that flag set the
GGUF cache-size check could still call get_paths_info. Snapshot selection
prefers revisions holding the inventoried safetensors weights, so a newest
metadata-only revision no longer shadows a complete older one and fails a
valid candidate. The orchestrator's parent-side preflight (transformers tier
probe and GPU sizing, which calls hf model_info) runs under the local-only
guard, closed before the worker spawn so the child does not inherit the
offline env for its whole lifetime; ordinary loads keep their current
unguarded behavior.
2026-07-26 23:41:35 -07:00
Unsloth
efa72431cc Make the SSM kernel signature assert format agnostic 2026-07-26 23:21:21 -07:00
pre-commit-ci[bot]
35cd77e6d8 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-07-27 06:19:47 +00:00
Unsloth
db64cd2ab4 Align background picks with inventory selection and drop remaining install/network paths
Cached checkpoint repos (pickle weights) are excluded from background picks
like local checkpoint rows: forced-offline validation cannot consult the Hub
security scan and pickles can execute code on load. The worker keeps its
entire bootstrap offline under local-only (base resolution, transformers
activation, security gates, kernel probes and the initial load) and never
pip-installs SSM kernels for a background load; a missing fatal kernel fails
into candidate failover. The offline guard is refcounted so overlapping
local-only requests share one env override restored only when the last
exits, closing the race where one request finishing re-enabled network for
another still running. Snapshot resolution now prefers the newest snapshot
dir, the same selection the inventory scanner surfaces, before consulting
refs/main. MLX loads read config.path so the live-cache rewrite is honored.
Cached non-GGUF rows carry snapshot_size_bytes (the newest snapshot's weight
bytes) and the cascade orders on it instead of the all-revisions blob total,
so a small current revision no longer sinks behind larger candidates.
2026-07-26 23:19:03 -07:00
pre-commit-ci[bot]
1dce5373aa [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-07-27 05:53:32 +00:00
Unsloth
d68330080b Close remaining network paths in local-only loads and validation
The Vulkan-ordinal GGUF preflight downloads before the Phase 2 block, so it
now takes local_files_only and the forced-offline wrap; both download blocks
force offline so the cache-size verification (get_paths_info) stays off the
network. The offline helper now overrides an explicitly falsy
HF_HUB_OFFLINE=0 under force and restores prior values on exit. The validate
route keeps its whole metadata and security preflight offline, not just the
identifier probe. The python load path survives the process boundary: the
orchestrator forwards the flag and the route-resolved snapshot path into the
worker, which re-applies the path rewrite after rebuilding its ModelConfig,
runs the entire load under a scoped offline env, and passes the flag to the
vision processor fallback so a Hub base_model resolves from cache or fails
over instead of downloading.
2026-07-26 22:51:45 -07:00
pre-commit-ci[bot]
370195c702 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-07-27 05:00:15 +00:00
Unsloth
aa959faea5 Harden background load gates: checkpoint rows, audio codecs, offline validation, refless caches
Local checkpoint rows carry pickle weights with no Hub security scan, so
they are excluded from background picks like adapters. Non-GGUF audio
models are refused under local_files_only in both /load and /validate
because their codec runtimes (SNAC, DAC, BiCodec) download auxiliaries at
load time. The validate probe now sends local_files_only and both routes
force the offline env wrap for it, so candidate metadata resolution cannot
reach the Hub. Snapshot resolution falls back to the newest revision dir
holding a config.json when refs/main is pruned, matching the layouts the
inventory scanner accepts.
2026-07-26 21:59:20 -07:00
pre-commit-ci[bot]
37d11a967e [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-07-27 03:51:58 +00:00
Unsloth
90f9e97302 Studio autoload: extend local-only enforcement to GGUF companions, live cache dir, processor fallback
Background GGUF loads pass local_files_only through the llama.cpp path:
the main quant resolves from the cache alone and a miss fails closed
instead of downloading, while the optional mmproj and MTP drafter
companions resolve cached-or-skipped, including on the crash replay. The
safetensors snapshot rewrite resolves against the live Studio-managed
hub cache rather than huggingface_hub's import-time default, which goes
stale when the cache location changes at runtime. The vision processor
fallback loads from config.path instead of config.identifier: identical
for ordinary loads, but a local-only rewrite must keep the fallback on
the local snapshot instead of refetching by repo id.
2026-07-26 20:49:19 -07:00
pre-commit-ci[bot]
16b67a2dc3 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-07-27 02:17:51 +00:00
Unsloth
d57ca52b83 Studio autoload: local-only cached loads, bounded platform fetch, file-scoped BE marker
Background loads now send local_files_only and the load route rewrites a
cached repo id to its locally resolved snapshot directory before the
weight load, keeping the registry identity intact. A cache populated
outside Studio passes the partial check while missing shards because a
missing download manifest reads as non-partial, and from_pretrained on a
repo id would download the gaps; resolution never touches the network
and an uncached repo returns 409, so incomplete caches fail over to the
next candidate instead of fetching on Send. The platform probe behind
the picker's format gates joins the bounded best-effort prefetch (its
raw fetch has no timeout), falling back to the boot-detected platform
past the grace. The name-based big-endian marker now applies only to
direct .gguf files: a directory named foo-be says nothing about the
files inside it, which the per-file variant filter already covers.
2026-07-26 19:17:08 -07:00
Unsloth
184075a383 Studio autoload: bound the matcher fetch, let only the first attempt leapfrog scans
The dynamic hidden-model matcher fetch has no timeout of its own, unlike
the 30 second bounded inventory calls, so it now runs alongside them and
is awaited through a short grace instead of serially: a stalled
/api/hub/hidden-models request can no longer hang the send path, and the
static needles filter alone past the bound. The scan-leapfrog window
narrows to the first attempt only: it is the latency-critical one and
almost always succeeds, and once any budget is spent the remaining
attempts wait for resolution to settle, so failures can no longer
exhaust the cap on larger candidates while smaller ones are still
resolving.
2026-07-26 18:08:46 -07:00
Unsloth
b7b37e165c Merge origin/main into studio-autoload-on-device-only
Takes main's wording for the GgufVariantDetail.partial doc comment,
which both sides added.
2026-07-26 17:18:16 -07:00
pre-commit-ci[bot]
d594c2389a [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-07-26 11:42:12 +00:00
Unsloth
1d582a8781 Studio autoload: hold final attempt for pending scans, stop workers on exit, picker platform gate
The last load attempt is not spent while folder scans are still pending,
since a smaller candidate can still enter the pool; earlier attempts
keep flowing incrementally. Resolution workers stop claiming scans once
autoload reaches a terminal result, so a successful early load no longer
leaves background scans contending with inference. Local rows now apply
the picker's chat-only format gate (GGUF on any host, MLX on Mac) and
the cascade skips cached non-GGUF repos on chat-only installs, matching
what the picker offers; the remembered path stays ungated since a
recorded load proves the model runs on this install.
2026-07-26 04:41:07 -07:00
Unsloth
43a1edc2ea Studio autoload: seed no-scan rows upfront, interleave scans, gate safetensors on pending GGUF scans
Local rows that resolve without a backend folder scan seed the pool
before the workers start, so they cannot queue behind slow cached scans.
Cached and local folder scans interleave across the bounded workers so a
run of slow scans from one source cannot monopolize them. The consumer
holds model-kind candidates while scan jobs are pending, since every
pending scan can still yield a GGUF that outranks safetensors in the
documented order; resolved GGUF entries keep flowing immediately.
2026-07-26 04:15:25 -07:00
Unsloth
4791969bc9 Studio autoload: quant-size ordering for cached repos, alias dedupe, incremental resolution
Cached GGUF repos now order by the quant that will actually load instead
of the row size_bytes, which sums every downloaded quant and pushed a
repo holding one small quant behind larger models. Cached entries apply
the same seen gate local rows use and skip keys are scoped to the
backend load target, so a cached repo and an indexed local row aliasing
the same files no longer spend two attempt slots on one failure. The
fallback resolves candidates through the bounded worker pool and
consumes them incrementally from a size-ordered pool after a short
settle grace, so one slow folder scan no longer stalls the send path
behind the transport timeout.
2026-07-26 00:30:43 -07:00
pre-commit-ci[bot]
d74e7066d2 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-07-26 07:15:09 +00:00
Unsloth
cd62c3a0ba Format source guard in last-local-model-load 2026-07-26 00:13:45 -07:00
Unsloth
71341a1046 Merge origin/main into studio-autoload-on-device-only
Resolves the chat-api import conflict: keep GenerationLengthError from
main and keep the branch's move of listCachedGguf and listCachedModels
to the unified inventory API.
2026-07-26 00:10:41 -07:00
shimmyshimmer
bc9a5f7f86 Studio autoload: case-aware keys, global-order quant requeue, bounded scans
Sixth round of review follow-ups:

- Identifier keys (seen set, skip keys, remembered matching) now use
  path-shape-aware case semantics: POSIX paths keep their case since Linux
  distinguishes /models/Foo from /models/foo, while Windows-style paths and
  Hub repo ids stay case-insensitive. Inventory ids compare exactly.
- A quant that fails /load re-enters the shared fallback queue at its
  resolved size (still ahead of the safetensors group) instead of retrying
  the same folder inline, so a folder of failing quants cannot exhaust the
  attempt cap while a smaller model in another folder goes untried.
- Local variant pre-resolution runs with bounded concurrency (4) so a large
  indexed inventory does not fan out a recursive directory scan per folder
  all at once.

Contract tests and simulations updated and extended for all three.
2026-07-26 00:03:12 -07:00
pre-commit-ci[bot]
69c614e2e8 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-07-24 05:04:43 +00:00
shimmyshimmer
08cb60c6e1 Studio autoload: retry the folder's next quant when a load itself fails
Fifth round of review follow-ups. A resolved quant that passed validation
could still fail /api/inference/load (corrupt file, llama.cpp startup
error); the cascade then abandoned the whole row because only validation
blocks recorded a skip key. The fallback now marks the failed quant
skipped and resolves the folder's next complete quant before moving on,
bounded by the existing attempt cap. Single-candidate rows resolve to
null once skipped, so the retry loop terminates.

Contract test and simulation added for the failure-then-retry path.
2026-07-23 22:03:56 -07:00
shimmyshimmer
a928900a2e 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.
2026-07-23 20:01:23 -07:00
shimmyshimmer
5aa0183706 Studio autoload: retry next folder quant after a skip; dedupe by load target only
Third round of review follow-ups:

- Quant resolution for a local GGUF folder now returns the smallest quant
  that is not already in the skipped set, so one corrupt or blocked file
  cannot sink a folder that still has other complete quants.
- The cached/local dedupe keys on actual load targets and on-disk paths
  only. A local copy that merely shares a repo model_id is a distinct set
  of files and stays available when the cached copy fails or has no
  usable quant.

Contract tests updated and extended for both.
2026-07-23 18:58:25 -07:00
pre-commit-ci[bot]
44964067ab [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-07-24 01:28:47 +00:00
shimmyshimmer
bf46d8bb60 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.
2026-07-23 18:26:06 -07:00
pre-commit-ci[bot]
db7855538a [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-07-24 00:32:38 +00:00
shimmyshimmer
c4f182a581 Studio autoload: resolve dir-GGUF variants, match remembered kind, skip adapters
Review follow-ups, all verified against the backend inventory services:

- Directory-based local GGUFs (LM Studio, models dir, custom folders) are
  flagged requires_variant by the backend, so the fallback now resolves the
  smallest complete quant through the same variants API the picker card
  uses instead of dropping every directory row.
- A folder holding both GGUF and safetensors weights yields two rows with
  the same load target; remembered-model matching now requires the row
  format to agree with the remembered kind.
- Adapter rows are chat-capable but load by resolving their base model,
  which for a Hub-id base would start an implicit remote fetch; adapters
  are excluded from background auto-load.

Contract tests extended to pin all three behaviors.
2026-07-23 17:29:57 -07:00
shimmyshimmer
5d06d64a26 Studio: export autoLoadOnDeviceModel for tests
Allows behavioral simulations to drive the real auto-load implementation
directly instead of asserting on source text only. No runtime change.
2026-07-23 16:46:38 -07:00
pre-commit-ci[bot]
939ea605ad [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-07-23 23:22:52 +00:00
shimmyshimmer
36268b8444 Studio: auto-load only on-device models on send; never download the hard-coded default
Fixes #7374. Pressing Send with no model loaded could miss local models
(models dir, LM Studio, custom scan folders, inactive HF caches) and then
download unsloth/Qwen3.5-4B-MTP-GGUF from Hugging Face without consent.

- autoLoadSmallestModel is now autoLoadOnDeviceModel: adopts the server
  active model, then the remembered on-device model, then the smallest
  complete chat-capable on-device model (GGUF first), across managed HF
  caches and the unified backend local inventory.
- Inventory failures fail closed instead of reading as an empty cache.
- The remote Qwen fallback is removed from the send path; with no valid
  candidate the user is asked to select or explicitly download a model.
- last-local-model-load records loadId/inventoryId/source with backward
  compatible v1 parsing; indexed local loads are remembered while native
  path lease picks stay excluded.
- New source-contract tests in tests/studio/test_model_picker_contracts.py.
2026-07-23 16:21:08 -07:00
20 changed files with 2899 additions and 286 deletions

View file

@ -432,6 +432,14 @@ def render_native_template(
if native_tpl is None:
# A LoRA adapter's native template lives on the base model, not the adapter id.
template_source = model_info.get("base_model") or active_model_name
# A local-only (background) load resolved its weights to a local
# snapshot; the template reload must read the SAME files. For a
# non-LoRA model prefer the stored load path over the repo id, and
# either way pass local_files_only so a cache miss fails the fallback
# instead of downloading tokenizer files mid-generation.
local_files_only = bool(model_info.get("local_files_only", False))
if local_files_only and not model_info.get("base_model"):
template_source = model_info.get("model_path") or template_source
# Re-use the load-time trust_remote_code so a custom-code tokenizer repo can
# instantiate its class (the stored flag already covers template_source).
trust_remote_code = bool(model_info.get("trust_remote_code", False))
@ -441,6 +449,7 @@ def render_native_template(
template_source,
token = hf_token if hf_token and hf_token.strip() else None,
trust_remote_code = trust_remote_code,
local_files_only = local_files_only,
)
native_tpl = nt.chat_template or False
except Exception as exc:

View file

@ -322,6 +322,7 @@ class InferenceBackend:
hf_token: Optional[str] = None,
trust_remote_code: bool = False,
gpu_ids: Optional[list[int]] = None,
local_files_only: bool = False,
) -> bool:
"""Load any model: base, LoRA adapter, text, or vision."""
# Keep the token so the native-template fallback can fetch a
@ -364,6 +365,9 @@ class InferenceBackend:
"trust_remote_code": trust_remote_code,
"is_vision": config.is_vision,
"is_lora": config.is_lora,
# Local-only loads: generation-time repo fallbacks (native
# template reload) must also resolve from cache, not the Hub.
"local_files_only": local_files_only,
"is_audio": config.is_audio,
"audio_type": config.audio_type,
"has_audio_input": config.has_audio_input,
@ -562,7 +566,11 @@ class InferenceBackend:
isinstance(processor, ProcessorMixin) or hasattr(processor, "image_processor")
):
# LoRA adapters: use base model. Local merged exports: read base from export_metadata.json.
processor_source = config.base_model if config.is_lora else config.identifier
# Non-LoRA: config.path, not config.identifier. They are the
# same for ordinary loads, but a local-only load rewrites
# path to the cached snapshot and this fallback must stay
# on those local files instead of refetching by repo id.
processor_source = config.base_model if config.is_lora else config.path
if not config.is_lora and config.is_local:
_meta_path = Path(config.path) / "export_metadata.json"
try:
@ -578,10 +586,14 @@ class InferenceBackend:
)
from transformers import AutoProcessor
# Local-only loads: a LoRA base or export_metadata base_model
# is a Hub repo id; resolve it from cache or fail the load
# (candidate failover) instead of downloading the processor.
processor = AutoProcessor.from_pretrained(
processor_source,
token = hf_token if hf_token and hf_token.strip() else None,
trust_remote_code = trust_remote_code,
local_files_only = local_files_only,
)
logger.info(f"Loaded {type(processor).__name__} from {processor_source}")

View file

@ -515,29 +515,84 @@ def _hf_env_offline() -> bool:
return os.environ.get("HF_HUB_OFFLINE", "").strip().lower() in {"1", "true", "yes", "on"}
def _hub_offline_env_truthy() -> bool:
"""HF_HUB_OFFLINE specifically is truthy. huggingface_hub does not honor
TRANSFORMERS_OFFLINE, so a forced local-only guard may only no-op when the
Hub flag itself is already set."""
return os.environ.get("HF_HUB_OFFLINE", "").strip().lower() in {"1", "true", "yes", "on"}
# Overlapping offline guards share one env override: the count tracks active
# guards and the saved values are restored only when the LAST guard exits, so
# a request finishing early cannot re-enable network for one still running.
_OFFLINE_GUARD_LOCK = threading.Lock()
_OFFLINE_GUARD_STATE: dict = {"count": 0, "hub_prev": None, "transformers_prev": None}
@contextlib.contextmanager
def _hf_offline_if_dns_dead():
def _hf_offline_if_dns_dead(force: bool = False):
"""Set HF_HUB_OFFLINE for this block only when DNS to huggingface.co fails;
restores env on exit so a transient hiccup can't quarantine the process.
No-op if the user already set it."""
if "HF_HUB_OFFLINE" in os.environ:
No-op when the user already set it to a truthy value. ``force`` skips the
DNS probe and goes offline unconditionally (local-only background loads
resolve metadata from the cache without any network), overriding even an
explicitly falsy HF_HUB_OFFLINE=0 for the block and restoring it after.
Guards are refcounted so overlapping loads/validations each keep offline
until the last one exits."""
entered = False # this guard joined or created the env override
owner = False # this guard created it (first in)
with _OFFLINE_GUARD_LOCK:
if _OFFLINE_GUARD_STATE["count"] > 0:
if force:
# Join the active override so the env survives until every
# local-only guard exits, whichever request finishes first.
_OFFLINE_GUARD_STATE["count"] += 1
entered = True
# An ordinary guard never joins: its block tolerates either env
# state (it would have run online), so extending the forced
# window would only widen the exposure of concurrent online
# work to the process-global override. It no-ops instead.
elif _hf_env_offline() and (not force or _hub_offline_env_truthy()):
# User-set truthy env (count is 0): already offline, nothing to
# arrange or restore. A forced guard only trusts HF_HUB_OFFLINE
# itself: TRANSFORMERS_OFFLINE=1 alone leaves hub API calls (e.g.
# get_paths_info) online, so force still installs both flags.
pass
elif not force and "HF_HUB_OFFLINE" in os.environ:
# A user-pinned falsy value stays authoritative for ordinary loads.
pass
elif force or _probe_dns_dead():
_OFFLINE_GUARD_STATE["hub_prev"] = os.environ.get("HF_HUB_OFFLINE")
_OFFLINE_GUARD_STATE["transformers_prev"] = os.environ.get("TRANSFORMERS_OFFLINE")
os.environ["HF_HUB_OFFLINE"] = "1"
os.environ["TRANSFORMERS_OFFLINE"] = "1"
_OFFLINE_GUARD_STATE["count"] = 1
entered = True
owner = True
if not entered:
yield False
return
if not _probe_dns_dead():
yield False
return
transformers_was_set = "TRANSFORMERS_OFFLINE" in os.environ
os.environ["HF_HUB_OFFLINE"] = "1"
if not transformers_was_set:
os.environ["TRANSFORMERS_OFFLINE"] = "1"
logger.warning("huggingface.co unreachable; using local HF cache for this load.")
if owner:
if force:
logger.info("Local-only load: forcing HF offline for this block.")
else:
logger.warning("huggingface.co unreachable; using local HF cache for this load.")
try:
yield True
finally:
os.environ.pop("HF_HUB_OFFLINE", None)
if not transformers_was_set:
os.environ.pop("TRANSFORMERS_OFFLINE", None)
with _OFFLINE_GUARD_LOCK:
_OFFLINE_GUARD_STATE["count"] -= 1
if _OFFLINE_GUARD_STATE["count"] == 0:
for key, prev in (
("HF_HUB_OFFLINE", _OFFLINE_GUARD_STATE["hub_prev"]),
("TRANSFORMERS_OFFLINE", _OFFLINE_GUARD_STATE["transformers_prev"]),
):
if prev is None:
os.environ.pop(key, None)
else:
os.environ[key] = prev
_OFFLINE_GUARD_STATE["hub_prev"] = None
_OFFLINE_GUARD_STATE["transformers_prev"] = None
try:
@ -5273,6 +5328,7 @@ class LlamaCppBackend:
force: bool = False,
allow_smaller_fallback: bool = True,
cancel_event: Optional[threading.Event] = None,
local_files_only: bool = False,
) -> str:
"""Download GGUF file(s) from HuggingFace. Returns local path.
@ -5310,16 +5366,19 @@ class LlamaCppBackend:
gguf_filename = None
gguf_extra_shards: list[str] = []
if hf_variant:
try:
from huggingface_hub import list_repo_files
# Local-only loads resolve from the cache alone: the live listing
# is both network and unnecessary for files already on disk.
if not local_files_only:
try:
from huggingface_hub import list_repo_files
files = list_repo_files(hf_repo, token = hf_token)
gguf_files = _gguf_files_for_variant(files, hf_variant)
if gguf_files:
gguf_filename = gguf_files[0]
gguf_extra_shards = _gguf_extra_shards(gguf_files, gguf_filename)
except Exception as e:
logger.warning(f"Could not list repo files: {e}")
files = list_repo_files(hf_repo, token = hf_token)
gguf_files = _gguf_files_for_variant(files, hf_variant)
if gguf_files:
gguf_filename = gguf_files[0]
gguf_extra_shards = _gguf_extra_shards(gguf_files, gguf_filename)
except Exception as e:
logger.warning(f"Could not list repo files: {e}")
# Fall back to the local cache when the repo listing is unavailable.
if not gguf_filename:
@ -5343,10 +5402,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:
@ -5354,13 +5419,24 @@ 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:
logger.info(f"Reusing cached GGUF: {cached_main}")
return cached_main
# A local-only load reaching this point has no complete cached copy;
# fail closed instead of downloading (background loads never fetch).
if local_files_only:
raise RuntimeError(
f"GGUF '{hf_repo}' ({hf_variant or 'default'}) is not fully "
"available in the local cache; select it explicitly to download it."
)
# Check disk space; fall back to a smaller variant if needed
all_gguf_files = [gguf_filename] + gguf_extra_shards
try:
@ -5523,6 +5599,7 @@ class LlamaCppBackend:
label: str,
cancel_event: Optional[threading.Event] = None,
near_path: Optional[str] = None,
local_files_only: bool = False,
) -> Optional[str]:
"""Resolve and fetch a companion GGUF (mmproj / MTP drafter) by name.
@ -5544,6 +5621,12 @@ class LlamaCppBackend:
logger.info("Reusing cached %s: %s", label, cached)
return cached
# Background loads never download: without a cached copy the model
# runs without the optional companion instead of fetching it.
if local_files_only:
logger.info("Skipping %s fetch (local-only load)", label)
return None
from utils.hf_cache_settings import get_hf_cache_paths
companion_cache_dir = _hub_cache_dir_for_snapshot_path(near_path) or str(
@ -5632,6 +5715,7 @@ class LlamaCppBackend:
hf_token: Optional[str] = None,
cancel_event: Optional[threading.Event] = None,
near_path: Optional[str] = None,
local_files_only: bool = False,
) -> Optional[str]:
"""Download the mmproj (vision projection) file from a GGUF repo.
@ -5648,6 +5732,7 @@ class LlamaCppBackend:
label = "mmproj",
cancel_event = cancel_event,
near_path = near_path,
local_files_only = local_files_only,
)
def _cached_repo_mtp_drafter(
@ -5689,6 +5774,7 @@ class LlamaCppBackend:
hf_repo: str,
hf_token: Optional[str] = None,
near_path: Optional[str] = None,
local_files_only: bool = False,
) -> Optional[str]:
"""Download the separate MTP drafter (speculative head) from a GGUF repo.
@ -5722,7 +5808,7 @@ class LlamaCppBackend:
# fetched). Online, _download_companion_gguf/hf_hub_download reuse the
# current cached file and refetch a changed one, so skip the probe here
# rather than pair new weights with a stale draft.
if _hf_env_offline():
if local_files_only or _hf_env_offline():
cached = self._cached_repo_mtp_drafter(
hf_repo,
cache_dir = _hub_cache_dir_for_snapshot_path(near_path),
@ -5737,6 +5823,7 @@ class LlamaCppBackend:
pick = _pick_mtp,
label = "MTP drafter",
near_path = near_path,
local_files_only = local_files_only,
)
def _resolve_launch_mmproj_path(
@ -6433,6 +6520,9 @@ class LlamaCppBackend:
hf_repo: Optional[str] = None,
hf_variant: Optional[str] = None,
hf_token: Optional[str] = None,
# Background auto-loads: resolve every file (main quant, mmproj, MTP
# drafter) from the local cache only and never download.
local_files_only: bool = False,
# Common
model_identifier: str,
is_vision: bool = False,
@ -6470,6 +6560,7 @@ class LlamaCppBackend:
"gguf_path": gguf_path,
"mmproj_path": mmproj_path,
"mtp_draft_path": mtp_draft_path,
"local_files_only": local_files_only,
"hf_repo": hf_repo,
"hf_variant": hf_variant,
"hf_token": hf_token,
@ -6578,11 +6669,15 @@ class LlamaCppBackend:
hf_repo,
)
hf_repo = _resolved_repo
with _hf_offline_if_dns_dead():
# Same local-only policy as the Phase 2 download below: this
# preflight runs FIRST, so without the flag it would be the
# download path a background load slips through.
with _hf_offline_if_dns_dead(force = local_files_only):
_preflight_model_path = self._download_gguf(
hf_repo = hf_repo,
hf_variant = hf_variant,
hf_token = hf_token,
local_files_only = local_files_only,
)
self._reject_vulkan_diffusion_gpu_ids_before_teardown(
_preflight_model_path,
@ -6620,11 +6715,14 @@ class LlamaCppBackend:
hf_repo,
)
hf_repo = _resolved_repo
with _hf_offline_if_dns_dead():
# Forced offline under local-only so even the cache-size
# verification (get_paths_info) stays off the network.
with _hf_offline_if_dns_dead(force = local_files_only):
model_path = _preflight_model_path or self._download_gguf(
hf_repo = hf_repo,
hf_variant = hf_variant,
hf_token = hf_token,
local_files_only = local_files_only,
)
# Auto-download mmproj for vision models unless opted out.
if is_vision and not mmproj_path and not extra_args_disable_mmproj(extra_args):
@ -6632,6 +6730,7 @@ class LlamaCppBackend:
hf_repo = hf_repo,
hf_token = hf_token,
near_path = model_path,
local_files_only = local_files_only,
)
# Auto-download the separate MTP drafter (e.g. Gemma) when
# the requested spec mode can use it. Repos with the head
@ -6650,6 +6749,7 @@ class LlamaCppBackend:
hf_repo = hf_repo,
hf_token = hf_token,
near_path = model_path,
local_files_only = local_files_only,
)
elif gguf_path:
if not Path(gguf_path).is_file():

View file

@ -524,6 +524,9 @@ class MLXInferenceBackend:
dtype = None,
parallel_mode = None,
distributed_group = None,
# Accepted for worker parity; the load runs under the worker's forced
# offline env when set, which is what enforces local-only here.
local_files_only: bool = False,
) -> bool:
import mlx.core as mx
@ -603,8 +606,13 @@ class MLXInferenceBackend:
else:
load_kwargs["tensor_group"] = distributed_group
# Registry identity stays the repo id (model_name); the LOAD source
# honors config.path so a route-resolved local snapshot (local-only
# loads against a moved live cache) is read instead of re-resolving
# the id through the import-time cache location.
load_source = getattr(config, "path", None) or model_name
model, tokenizer_or_processor = FastMLXModel.from_pretrained(
model_name,
load_source,
**load_kwargs,
)
@ -627,6 +635,10 @@ class MLXInferenceBackend:
"hf_token": hf_token,
# Per-model trust_remote_code reused by the native-template reload (matches transformers).
"trust_remote_code": trust_remote_code,
# Local-only loads: generation-time repo fallbacks (native
# template reload) must also resolve from cache, not the Hub.
"local_files_only": local_files_only,
"model_path": load_source,
"model": self._model,
"tokenizer": self._tokenizer,
"processor": self._processor,

View file

@ -973,6 +973,7 @@ class InferenceOrchestrator:
subject: Optional[str] = None,
tensor_parallel: bool = False,
mlx_distributed: bool = False,
local_files_only: bool = False,
) -> bool:
"""Load a model for inference.
@ -985,7 +986,24 @@ class InferenceOrchestrator:
self.loading_models.add(model_name)
try:
needed_major = "5" if needs_transformers_5(model_name) else "4"
# Parent-side metadata preflight stays offline for local-only
# loads: the tier probe reads model configs and GPU sizing calls
# hf model_info. The guard closes BEFORE the spawn below, so the
# worker does not inherit HF_HUB_OFFLINE for its whole lifetime
# (its own bootstrap guard scopes the load and restores for
# generation-time fetches).
import contextlib
from core.inference.llama_cpp import _hf_offline_if_dns_dead
def _preflight_offline():
# Ordinary loads keep their current (unguarded) behavior.
if local_files_only:
return _hf_offline_if_dns_dead(force = True)
return contextlib.nullcontext()
with _preflight_offline():
needed_major = "5" if needs_transformers_5(model_name) else "4"
# Build config dict for subprocess
sub_config = {
@ -1000,16 +1018,25 @@ class InferenceOrchestrator:
"gpu_ids": gpu_ids,
"tensor_parallel": bool(tensor_parallel),
"mlx_distributed": bool(mlx_distributed),
"local_files_only": bool(local_files_only),
# Route-resolved local snapshot: the worker rebuilds its
# ModelConfig from model_name, which would lose the rewrite.
"local_snapshot_path": (
getattr(config, "path", None)
if local_files_only and getattr(config, "path", None) != model_name
else None
),
"mlx_parallel_mode": ("tensor" if tensor_parallel else "pipeline")
if mlx_distributed
else None,
}
resolved_gpu_ids, gpu_selection = prepare_gpu_selection(
gpu_ids,
model_name = model_name,
hf_token = hf_token,
load_in_4bit = load_in_4bit,
)
with _preflight_offline():
resolved_gpu_ids, gpu_selection = prepare_gpu_selection(
gpu_ids,
model_name = model_name,
hf_token = hf_token,
load_in_4bit = load_in_4bit,
)
sub_config["resolved_gpu_ids"] = resolved_gpu_ids
sub_config["gpu_selection"] = gpu_selection
# Parent-detected backend for the worker's apply_gpu_ids().

View file

@ -13,6 +13,7 @@ mp.Queue, and exits on shutdown or unload. Pattern follows core/training/worker.
from __future__ import annotations
import base64
import contextlib
import json
from loggers import get_logger
import os
@ -95,6 +96,28 @@ def _clean_token(value: str | None) -> str | None:
return value if value and value.strip() else None
@contextlib.contextmanager
def _local_only_offline_env(enabled: bool):
"""Force HF offline for a local-only background load. The worker is a
fresh per-load process, so scoping env here covers config resolution,
security gates, and every from_pretrained in the load, then restores it so
generation-time fetches (e.g. the chat template fallback) still work."""
if not enabled:
yield
return
prev = {key: os.environ.get(key) for key in ("HF_HUB_OFFLINE", "TRANSFORMERS_OFFLINE")}
os.environ["HF_HUB_OFFLINE"] = "1"
os.environ["TRANSFORMERS_OFFLINE"] = "1"
try:
yield
finally:
for key, value in prev.items():
if value is None:
os.environ.pop(key, None)
else:
os.environ[key] = value
def _build_model_config(config: dict):
"""Build a ModelConfig from the config dict."""
from utils.models import ModelConfig
@ -107,6 +130,12 @@ def _build_model_config(config: dict):
)
if not mc:
raise ValueError(f"Invalid model identifier: {model_name}")
# The route resolves a cached repo id to its local snapshot for local-only
# loads; from_identifier here rebuilds path as the repo id, losing that
# rewrite across the process boundary, so re-apply it.
snapshot_override = config.get("local_snapshot_path")
if snapshot_override:
mc.path = snapshot_override
return mc
@ -175,19 +204,48 @@ def _resolve_lora_4bit(mc, load_in_4bit: bool) -> bool:
return load_in_4bit
def _ensure_ssm_kernels(targets: list, resp_queue: Any) -> bool:
def _ensure_ssm_kernels(
targets: list,
resp_queue: Any,
local_files_only: bool = False,
) -> bool:
"""Install the SSM kernels the given model(s) lazy-import in from_pretrained; no-op for
non-SSM models, idempotent. Returns True on success; on a fatal mamba-ssm failure sends a
'loaded' failure response and returns False. Call BEFORE importing transformers, which
snapshots its optional-backend gates at import (a later install may not be picked up).
Under ``local_files_only`` nothing is ever installed: kernels already present are used,
a missing fatal kernel fails the load into candidate failover, and the optional
causal-conv1d fast path is skipped (its torch fallback covers it).
"""
try:
from utils.ssm_runtime import ensure_ssm_runtime
from utils.ssm_runtime import ensure_ssm_runtime, model_is_ssm
except Exception as exc:
logger.debug("ssm_runtime unavailable (%s); skipping SSM kernel pre-install", exc)
return True
_ssm_status = lambda m: _send_response(resp_queue, {"type": "status", "message": m})
if local_files_only:
import importlib.util
for ssm_target in dict.fromkeys(t for t in targets if t):
try:
needs_mamba = model_is_ssm(ssm_target)
except Exception:
needs_mamba = False
if needs_mamba and importlib.util.find_spec("mamba_ssm") is None:
_send_response(
resp_queue,
{
"type": "loaded",
"success": False,
"message": (
"This model needs the mamba-ssm kernel, which is not "
"installed; select the model explicitly to install it."
),
"error_kind": "ssm_runtime_install_failed",
},
)
return False
return True
try:
for ssm_target in dict.fromkeys(t for t in targets if t):
ensure_ssm_runtime(ssm_target, status_cb = _ssm_status)
@ -285,7 +343,13 @@ def _run_security_gates(
def _handle_load(backend, config: dict, resp_queue: Any) -> None:
"""Handle a load command: load a model into the backend."""
_offline_guard = contextlib.ExitStack()
try:
# Local-only loads run the WHOLE load offline: config resolution,
# security gates, and from_pretrained can otherwise all reach the Hub.
_offline_guard.enter_context(
_local_only_offline_env(bool(config.get("local_files_only", False)))
)
mc = _build_model_config(config)
hf_token = _clean_token(config.get("hf_token"))
@ -335,7 +399,11 @@ def _handle_load(backend, config: dict, resp_queue: Any) -> None:
str(mc.base_model) if (mc.is_lora and getattr(mc, "base_model", None)) else None
)
ssm_targets = [ssm_probe_identifier(config["model_name"], _ssm_base)]
if not _ensure_ssm_kernels(ssm_targets, resp_queue):
if not _ensure_ssm_kernels(
ssm_targets,
resp_queue,
local_files_only = bool(config.get("local_files_only", False)),
):
return
# Heartbeat keeps the orchestrator's inactivity deadline alive during slow
@ -363,6 +431,7 @@ def _handle_load(backend, config: dict, resp_queue: Any) -> None:
"hf_token": hf_token,
"trust_remote_code": trust_remote_code,
"gpu_ids": config.get("resolved_gpu_ids"),
"local_files_only": bool(config.get("local_files_only", False)),
}
if getattr(backend, "device", None) == "mlx":
load_kwargs["parallel_mode"] = config.get("mlx_parallel_mode")
@ -435,6 +504,8 @@ def _handle_load(backend, config: dict, resp_queue: Any) -> None:
"stack": traceback.format_exc(limit = 20),
},
)
finally:
_offline_guard.close()
def _drain_skip_generate(cmd: dict, resp_queue: Any, drain_event) -> bool:
@ -796,6 +867,16 @@ def run_inference_process(
apply_gpu_ids(config.get("resolved_gpu_ids"), backend = config.get("device_backend"))
# Local-only background loads keep the ENTIRE bootstrap offline: base
# resolution, transformers activation, security gates, kernel probes and
# the initial load can all reach the Hub otherwise. Closed before the
# command loop so generation-time fetches (e.g. the chat template
# fallback) still work; error-path returns end the process anyway.
_bootstrap_offline = contextlib.ExitStack()
_bootstrap_offline.enter_context(
_local_only_offline_env(bool(config.get("local_files_only", False)))
)
model_name = config["model_name"]
# ── 0. MLX fast-path — skip torch/transformers ──
@ -857,6 +938,7 @@ def run_inference_process(
return
# Enter the same command loop as the GPU path.
_bootstrap_offline.close()
logger.info("MLX inference subprocess ready, entering command loop")
while True:
try:
@ -1015,7 +1097,11 @@ def run_inference_process(
from utils.ssm_runtime import ssm_probe_identifier
_ssm_targets = [ssm_probe_identifier(model_name, _base)]
if not _ensure_ssm_kernels(_ssm_targets, resp_queue):
if not _ensure_ssm_kernels(
_ssm_targets,
resp_queue,
local_files_only = bool(config.get("local_files_only", False)),
):
return
# ── 2. Import ML libraries (fresh in this clean process) ──
@ -1077,6 +1163,8 @@ def run_inference_process(
)
return
_bootstrap_offline.close()
# ── 4. Command loop — process commands until shutdown ──
# cancel_event is an mp.Event the parent can set anytime to cancel
# generation instantly (no queue polling needed).

View file

@ -188,6 +188,13 @@ class CachedModelRepo(CachedRepoBase):
pipeline_tag: Optional[str] = None
library_name: Optional[str] = None
tags: Optional[List[str]] = None
snapshot_size_bytes: Optional[int] = Field(
None,
description = (
"Weight bytes of the newest cached snapshot only (what a load "
"resolves); size_bytes sums blobs across every cached revision."
),
)
class CachedModelsResponse(BaseModel):

View file

@ -327,7 +327,7 @@ def _scan_cached_gguf() -> list[dict]:
continue
repo_id = repo_info.repo_id
repo_path = Path(repo_info.repo_path)
snapshot_path = _cached_model_snapshot_path(repo_path)
snapshot_path = _cached_gguf_repo_snapshot_path(repo_path)
total_size = _repo_gguf_size_bytes(repo_info)
has_variant_state, variant_state_size = _gguf_variant_state_summary(
repo_id,
@ -415,16 +415,35 @@ async def list_cached_gguf_response(hf_token: Optional[str] = None):
class _CachedNonGgufPayload(NamedTuple):
size_bytes: int
snapshot_size_bytes: int
has_runnable_weights: bool
model_format: ModelFormat
last_modified: float
def _snapshot_dir_mtime(revision) -> float:
"""mtime of a revision's snapshot dir; the same signal latest_snapshot_dir
(and therefore the load-side snapshot resolution) selects by."""
snapshot_path = getattr(revision, "snapshot_path", None)
if not snapshot_path:
return 0.0
try:
return float(Path(snapshot_path).stat().st_mtime)
except OSError:
return 0.0
def _repo_non_gguf_model_payload(repo_info) -> _CachedNonGgufPayload:
all_weight_blobs: dict[str, tuple[int, float]] = {}
adapter_blobs: dict[str, tuple[int, float]] = {}
safetensors_blobs: dict[str, tuple[int, float]] = {}
checkpoint_blobs: dict[str, tuple[int, float]] = {}
# Per-revision selected-format byte sums, so the row can also report the
# size of the ONE snapshot a load resolves (newest by snapshot-dir mtime)
# 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
@ -433,7 +452,7 @@ def _repo_non_gguf_model_payload(repo_info) -> _CachedNonGgufPayload:
has_checkpoint = False
def _record_blob(
target: dict[str, tuple[int, float]], file_obj, rev_id: str, file_name: str
target: dict[str, tuple[int, float]], file_obj, rev_id: str, file_name: str, category: str
) -> None:
blob_path = getattr(file_obj, "blob_path", None)
size = int(file_obj.size_on_disk or 0)
@ -441,9 +460,13 @@ def _repo_non_gguf_model_payload(repo_info) -> _CachedNonGgufPayload:
value = (size, _blob_mtime(file_obj))
target[key] = value
all_weight_blobs[key] = value
per_rev = rev_category_sizes.setdefault(rev_id, {})
per_rev[category] = per_rev.get(category, 0) + size
per_rev["all"] = per_rev.get("all", 0) + size
for revision in repo_info.revisions:
rev_id = getattr(revision, "commit_hash", None) or str(id(revision))
rev_snapshot_mtimes[rev_id] = _snapshot_dir_mtime(revision)
for f in revision.files:
file_name = str(f.file_name)
lower = file_name.lower()
@ -452,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
@ -461,15 +485,15 @@ def _repo_non_gguf_model_payload(repo_info) -> _CachedNonGgufPayload:
is_checkpoint = _is_checkpoint_weight_name(name)
if is_adapter:
has_adapter_weights = True
_record_blob(adapter_blobs, f, rev_id, file_name)
_record_blob(adapter_blobs, f, rev_id, file_name, "adapter")
if is_safetensors:
has_safetensors = True
if _is_transformers_safetensors_weight_name(name):
has_transformers_safetensors = True
_record_blob(safetensors_blobs, f, rev_id, file_name)
_record_blob(safetensors_blobs, f, rev_id, file_name, "safetensors")
if is_checkpoint:
has_checkpoint = True
_record_blob(checkpoint_blobs, f, rev_id, file_name)
_record_blob(checkpoint_blobs, f, rev_id, file_name, "checkpoint")
model_format = (
_classify_non_gguf_model_format(
@ -485,22 +509,134 @@ def _repo_non_gguf_model_payload(repo_info) -> _CachedNonGgufPayload:
)
if model_format == "adapter":
selected_blobs = adapter_blobs
selected_category = "adapter"
elif model_format == "safetensors":
selected_blobs = safetensors_blobs
selected_category = "safetensors"
elif model_format == "checkpoint":
selected_blobs = checkpoint_blobs
selected_category = "checkpoint"
else:
selected_blobs = all_weight_blobs
selected_category = "all"
size_bytes = sum(size for size, _mtime in selected_blobs.values())
# The one snapshot a load resolves (newest snapshot-dir mtime) among the
# 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)
else:
snapshot_size_bytes = size_bytes
return _CachedNonGgufPayload(
size_bytes = sum(size for size, _mtime in selected_blobs.values()),
size_bytes = size_bytes,
snapshot_size_bytes = snapshot_size_bytes,
has_runnable_weights = model_format != "unknown",
model_format = model_format,
last_modified = max((mtime for _size, mtime in selected_blobs.values()), default = 0.0),
)
def _newest_snapshot_where(repo_path: Path, loadable) -> Optional[Path]:
"""Newest snapshots/* dir (by mtime) whose entry names satisfy *loadable*.
Inactive-cache rows emit the selected snapshot path as their load_id,
which the load consumes directly (it bypasses the repo-id resolver), so
the snapshot must actually hold the format the row was classified from.
"""
snapshots = repo_path / "snapshots"
try:
revisions = [entry for entry in snapshots.iterdir() if entry.is_dir()]
except OSError:
return None
def _ok(rev: Path) -> bool:
try:
names = [entry.name for entry in rev.iterdir()]
except OSError:
return False
return loadable(names)
candidates = [rev for rev in revisions if _ok(rev)]
if not candidates:
return None
try:
return max(candidates, key = lambda rev: rev.stat().st_mtime).resolve()
except OSError:
return None
def _weightful_snapshot_path(repo_path: Path) -> Optional[Path]:
"""Newest snapshot holding config.json plus a safetensors weight (the
non-GGUF resolvers' complete-revision predicate)."""
return _newest_snapshot_where(
repo_path,
lambda names: "config.json" in names and any(n.endswith(".safetensors") for n in names),
)
def _gguf_snapshot_path(repo_path: Path) -> Optional[Path]:
"""Newest snapshot holding a GGUF file (top level or one folder deep,
where multi-quant repos keep per-quant subfolders)."""
direct = _newest_snapshot_where(
repo_path,
lambda names: any(_is_gguf_filename(n.lower()) for n in names),
)
if direct is not None:
return direct
snapshots = repo_path / "snapshots"
try:
revisions = [entry for entry in snapshots.iterdir() if entry.is_dir()]
except OSError:
return None
def _has_nested_gguf(rev: Path) -> bool:
try:
return any(True for _ in rev.glob("*/*.gguf"))
except OSError:
return False
candidates = [rev for rev in revisions if _has_nested_gguf(rev)]
if not candidates:
return None
try:
return max(candidates, key = lambda rev: rev.stat().st_mtime).resolve()
except OSError:
return None
def _cached_model_snapshot_path(repo_path: Path) -> Optional[Path]:
weightful = _weightful_snapshot_path(repo_path)
if weightful is not None:
return weightful
resolved = hf_cache_scan.resolve_hf_cache_realpath(repo_path)
if not resolved:
return None
path = Path(resolved)
return path if path.is_dir() else None
def _cached_gguf_repo_snapshot_path(repo_path: Path) -> Optional[Path]:
"""Snapshot path for a GGUF row: a safetensors-bearing revision of a
mixed repo must not become the GGUF row's load target."""
gguf_snapshot = _gguf_snapshot_path(repo_path)
if gguf_snapshot is not None:
return gguf_snapshot
resolved = hf_cache_scan.resolve_hf_cache_realpath(repo_path)
if not resolved:
return None
@ -636,6 +772,7 @@ def _scan_cached_models() -> list[dict]:
row = {
"repo_id": repo_id,
"size_bytes": payload.size_bytes,
"snapshot_size_bytes": payload.snapshot_size_bytes,
"cache_path": str(repo_info.repo_path),
"partial": snapshot_partial,
"partial_transport": (

View file

@ -0,0 +1,87 @@
# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
"""Local-only snapshot resolution for background model loads."""
import os
from typing import Optional
def _snapshot_has_weights(rev: str) -> bool:
"""Whether a snapshot dir holds at least one safetensors weight file.
Background loads only target safetensors-format rows (adapters and
pickle checkpoints are excluded upstream), so this is the runnable-weight
signal that made the inventory row eligible.
"""
try:
return any(name.endswith(".safetensors") for name in os.listdir(rev))
except OSError:
return False
def _snapshot_dir_fallback(repo_id: str, cache_dir: Optional[str]) -> Optional[str]:
"""Newest snapshots/* dir (by mtime) holding a config.json, preferring
revisions that also hold weights.
This mirrors the inventory scanner's selection (newest revision that
actually carries the inventoried weights), so the load targets the
snapshot that made the row eligible: a newest metadata-only revision must
not shadow an older complete one. It also covers revision-only layouts
(refs/ missing or pruned) that snapshot_download(local_files_only = True)
cannot map through refs/main.
"""
if cache_dir is None:
try:
from huggingface_hub.constants import HF_HUB_CACHE
cache_dir = HF_HUB_CACHE
except Exception:
return None
folder = os.path.join(cache_dir, "models--" + repo_id.replace("/", "--"), "snapshots")
try:
revisions = [
os.path.join(folder, name)
for name in os.listdir(folder)
if os.path.isdir(os.path.join(folder, name))
]
except OSError:
return None
candidates = [rev for rev in revisions if os.path.isfile(os.path.join(rev, "config.json"))]
if not candidates:
return None
weightful = [rev for rev in candidates if _snapshot_has_weights(rev)]
return max(weightful or candidates, key = os.path.getmtime)
def resolve_local_snapshot_path(
repo_id: str,
hf_token: Optional[str] = None,
cache_dir: Optional[str] = None,
) -> Optional[str]:
"""Resolve a Hub repo id to its snapshot directory in the local HF cache
without any network access; None when the repo is not cached.
The newest snapshot dir (by mtime) holding a config.json is preferred:
that is the same selection the inventory scanner surfaces, so the load
targets the revision that made the row eligible. refs/main can lag it
when a non-main commit was downloaded later, so consulting refs first
could load an older revision or fail on its missing files.
``snapshot_download(local_files_only = True)`` stays as the fallback for
layouts the directory scan cannot interpret. Either way resolution never
touches the network, and a missing-file snapshot still resolves so the
subsequent weight load fails instead of downloading the gaps, which is
the fail-closed behavior background loads need.
"""
resolved = _snapshot_dir_fallback(repo_id, cache_dir)
if resolved is not None:
return resolved
try:
from huggingface_hub import snapshot_download
return snapshot_download(
repo_id,
local_files_only = True,
token = hf_token or None,
cache_dir = cache_dir or None,
)
except Exception:
return None

View file

@ -40,6 +40,13 @@ class LoadRequest(BaseModel):
gguf_variant: Optional[str] = Field(
None, description = "GGUF quantization variant (e.g. 'Q4_K_M')"
)
local_files_only: bool = Field(
False,
description = (
"Resolve a cached repo id against the local snapshot only and "
"never download missing files (background auto-loads)."
),
)
trust_remote_code: bool = Field(
False,
description = "Allow loading models with custom code (e.g. NVIDIA Nemotron). Only enable for repos you trust.",
@ -249,6 +256,14 @@ class ValidateModelRequest(BaseModel):
"delegate fitting to llama.cpp, while explicit layers are user-owned."
),
)
local_files_only: bool = Field(
False,
description = (
"Background auto-loads: resolve model metadata from the local "
"cache only and reject candidates whose runtime would pull "
"remote auxiliaries (e.g. audio codec models)."
),
)
include_context_length: bool = Field(
False,
description = "Also read the native context length from the local GGUF header. "

View file

@ -5045,8 +5045,9 @@ async def _load_model_impl(
# is_lora auto-detected from adapter_config.json on disk/HF.
# DNS-probe wrap so offline loads skip 30-60s of soft-failed network
# checks before the worker starts.
with _hf_offline_if_dns_dead():
# checks before the worker starts. Local-only loads force offline so
# metadata resolution itself cannot reach the Hub.
with _hf_offline_if_dns_dead(force = request.local_files_only):
config = ModelConfig.from_identifier(
model_id = model_identifier,
hf_token = request.hf_token,
@ -5059,6 +5060,51 @@ async def _load_model_impl(
detail = f"Invalid model identifier: {model_log_label}",
)
# A background auto-load promises on-device-only resolution, but a
# cache populated outside Studio can pass the partial check while
# missing shards, and from_pretrained on a repo id would download the
# gaps. Rewriting the load path to the locally resolved snapshot keeps
# the registry identity (config.identifier) intact while forcing the
# weight load to the files actually on disk, so an incomplete cache
# fails over to the next candidate instead of fetching.
from utils.paths import is_local_path
# Python audio runtimes fetch codec auxiliaries (SNAC/DAC/BiCodec repos)
# at load time regardless of where the weights live, so a local-only
# load of a non-GGUF audio model still hits the network. Refuse it.
if request.local_files_only and not config.is_gguf and getattr(config, "is_audio", False):
raise HTTPException(
status_code = 409,
detail = (
f"Model '{model_log_label}' needs audio codec downloads; "
"select it explicitly to load it."
),
)
if request.local_files_only and not config.is_gguf and not is_local_path(config.path):
from hub.utils.local_snapshot import resolve_local_snapshot_path
from utils.hf_cache_settings import get_hf_cache_paths
# Resolve against the LIVE Studio-managed hub cache: Studio can
# move the cache at runtime without rewriting Hugging Face's
# import-time env constants, so the helper's default would search
# the stale location and 409 models the inventory just found.
local_snapshot = await asyncio.to_thread(
resolve_local_snapshot_path,
config.path,
request.hf_token,
str(get_hf_cache_paths().hub_cache),
)
if local_snapshot is None:
raise HTTPException(
status_code = 409,
detail = (
f"Model '{model_log_label}' is not available on device; "
"select it explicitly to download it."
),
)
config.path = local_snapshot
# Normalize gpu_ids: empty list means auto-selection, same as None
effective_gpu_ids = request.gpu_ids if request.gpu_ids else None
@ -5089,7 +5135,13 @@ async def _load_model_impl(
# to match. Off-loop: tier resolution reads configs.
if effective_load_in_4bit and not config.is_gguf:
from utils.transformers_version import latest_tier_active_for
if await asyncio.to_thread(latest_tier_active_for, config.identifier, request.hf_token):
# Local-only loads probe the tier against the resolved snapshot:
# a repo id would reach _remote_lora_base's raw HTTP request and
# Hub config reads, while a local path resolves from config.json
# on disk (non-canonical ids skip the remote adapter probe).
_tier_target = config.path if request.local_files_only else config.identifier
if await asyncio.to_thread(latest_tier_active_for, _tier_target, request.hf_token):
effective_load_in_4bit = False
logger.info(
f"Latest-transformers sidecar active for '{model_log_label}' - "
@ -5111,10 +5163,17 @@ async def _load_model_impl(
# Apply the training coexistence policy before the unload step below
# frees the resident model. Off-loop: the default-mode guard does sync work.
# Local-only non-GGUF loads size against the resolved snapshot so the
# guard's memory estimation reads local files instead of hf model_info
# (which performs no offline-mode check). GGUF sizing already reads
# the cached file under its own local-only handling.
_guard_identifier = (
config.path if request.local_files_only and not config.is_gguf else model_identifier
)
await asyncio.to_thread(
_guard_chat_load_against_training,
config,
model_identifier = model_identifier,
model_identifier = _guard_identifier,
hf_token = request.hf_token,
load_in_4bit = effective_load_in_4bit,
max_seq_length = request.max_seq_length,
@ -5201,6 +5260,10 @@ async def _load_model_impl(
hf_repo = config.gguf_hf_repo,
hf_variant = config.gguf_variant,
hf_token = request.hf_token,
# Background auto-loads never download: main quant resolves
# cache-only and the optional mmproj/MTP companions are
# cached-or-skipped instead of fetched.
local_files_only = request.local_files_only,
)
else:
# Local mode: llama-server loads via -m <path>
@ -5414,6 +5477,9 @@ async def _load_model_impl(
approved_remote_code_fingerprint = request.approved_remote_code_fingerprint,
gpu_ids = effective_gpu_ids,
subject = current_subject,
# Threads the rewritten snapshot path and forced-offline load into
# the worker subprocess, which rebuilds its own ModelConfig.
local_files_only = request.local_files_only,
)
if not success:
@ -5652,10 +5718,19 @@ async def validate_model(
native_grant_backed = False
model_log_label = request.model_path
# Local-only validation (background auto-loads) covers the WHOLE metadata
# and security preflight with a forced-offline env, not just the identifier
# probe: the upgrade, remote-code, and file-security helpers below all read
# Hub configs and would otherwise refetch them for a cache-only candidate.
import contextlib
_local_only_offline = contextlib.ExitStack()
try:
model_identifier, model_log_label, native_grant_backed = (
_resolve_model_identifier_for_request(request, operation = "validate-model")
)
if request.local_files_only:
_local_only_offline.enter_context(_hf_offline_if_dns_dead(force = True))
config = ModelConfig.from_identifier(
model_id = model_identifier,
hf_token = request.hf_token,
@ -5668,6 +5743,22 @@ async def validate_model(
detail = f"Invalid model identifier: {model_log_label}",
)
# Same audio gate as /load: non-GGUF audio models pull codec repos at
# load time, so a local-only candidate is rejected here before the
# frontend burns a load attempt on it.
if (
request.local_files_only
and not getattr(config, "is_gguf", False)
and getattr(config, "is_audio", False)
):
raise HTTPException(
status_code = 409,
detail = (
f"Model '{model_log_label}' needs audio codec downloads; "
"select it explicitly to load it."
),
)
# Apply the same training coexistence policy as /load before the frontend
# unloads the current model.
effective_gpu_ids = request.gpu_ids if request.gpu_ids else None
@ -5903,6 +5994,8 @@ async def validate_model(
status_code = 400,
detail = "Invalid model",
)
finally:
_local_only_offline.close()
# studio_router only: admin action, kept off the OpenAI-compatible /v1 mount.

View file

@ -0,0 +1,252 @@
# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
"""Local-only snapshot resolution for background auto-loads.
A cache populated outside Studio (no download manifest) passes the partial
check while missing shard files, and ``from_pretrained`` on a repo id would
download the gaps. Background loads therefore rewrite the load path to the
LOCALLY resolved snapshot: resolution never touches the network, an uncached
repo resolves to None (409 upstream), and an incomplete snapshot still
resolves so the weight load fails on the missing files instead of fetching
them. No GPU or network required.
"""
from __future__ import annotations
import sys
from pathlib import Path
import pytest
_BACKEND_DIR = str(Path(__file__).resolve().parent.parent)
if _BACKEND_DIR not in sys.path:
sys.path.insert(0, _BACKEND_DIR)
pytest.importorskip("huggingface_hub")
from hub.utils.local_snapshot import resolve_local_snapshot_path
_REV = "0123456789abcdef0123456789abcdef01234567"
def _build_cached_repo(
cache_dir: Path,
repo_id: str,
files: dict[str, str],
with_refs: bool = True,
rev: str = _REV,
) -> Path:
"""Lay out a minimal HF hub cache entry the way huggingface_hub expects:
``models--org--name/refs/main`` pointing at a snapshot directory.
``with_refs = False`` builds the revision-only layout (pruned or foreign
caches) the inventory scanner accepts."""
repo_dir = cache_dir / f"models--{repo_id.replace('/', '--')}"
snapshot = repo_dir / "snapshots" / rev
snapshot.mkdir(parents = True)
if with_refs:
(repo_dir / "refs").mkdir(exist_ok = True)
(repo_dir / "refs" / "main").write_text(rev)
for name, content in files.items():
(snapshot / name).write_text(content)
return snapshot
def test_cached_repo_resolves_to_its_snapshot_dir(tmp_path):
snapshot = _build_cached_repo(
tmp_path,
"org/tiny-model",
{"config.json": "{}", "model.safetensors": "weights"},
)
resolved = resolve_local_snapshot_path("org/tiny-model", cache_dir = str(tmp_path))
assert resolved is not None
assert Path(resolved).resolve() == snapshot.resolve()
def test_incomplete_snapshot_still_resolves_locally(tmp_path):
"""Missing shards must not block resolution: the local path is what makes
the subsequent weight load fail closed instead of downloading."""
snapshot = _build_cached_repo(
tmp_path,
"org/half-downloaded",
{
"config.json": "{}",
"model-00001-of-00002.safetensors": "first shard only",
},
)
resolved = resolve_local_snapshot_path("org/half-downloaded", cache_dir = str(tmp_path))
assert resolved is not None
assert Path(resolved).resolve() == snapshot.resolve()
def test_uncached_repo_resolves_to_none(tmp_path):
assert resolve_local_snapshot_path("org/never-downloaded", cache_dir = str(tmp_path)) is None
def test_newest_snapshot_preferred_over_refs_main(tmp_path):
"""A newer snapshot downloaded at an explicit revision outranks the older
refs/main target: the inventory surfaces the newest snapshot by mtime, so
the load must resolve the same one instead of an older (possibly
incomplete) main revision."""
import os
import time
old_main = _build_cached_repo(
tmp_path,
"org/newer-rev",
{"config.json": "{}"},
rev = "a" * 40,
)
stale = time.time() - 1000
os.utime(old_main, (stale, stale))
newer = _build_cached_repo(
tmp_path,
"org/newer-rev",
{"config.json": "{}", "model.safetensors": "weights"},
with_refs = False,
rev = "b" * 40,
)
resolved = resolve_local_snapshot_path("org/newer-rev", cache_dir = str(tmp_path))
assert resolved is not None
assert Path(resolved).resolve() == newer.resolve()
def test_revision_only_snapshot_resolves_without_refs(tmp_path):
"""snapshot_download(local_files_only = True) needs refs/main, but the
inventory scanner accepts revision-only layouts (pruned refs), so the
resolver must fall back to the snapshot directory itself."""
snapshot = _build_cached_repo(
tmp_path,
"org/no-refs",
{"config.json": "{}", "model.safetensors": "weights"},
with_refs = False,
)
resolved = resolve_local_snapshot_path("org/no-refs", cache_dir = str(tmp_path))
assert resolved is not None
assert Path(resolved).resolve() == snapshot.resolve()
def test_refless_fallback_picks_newest_snapshot_with_config(tmp_path):
"""With several revision dirs, the fallback must pick the newest one that
actually holds a config.json, skipping empty or partial revisions."""
import os
import time
old = _build_cached_repo(
tmp_path,
"org/multi-rev",
{"config.json": "{}"},
with_refs = False,
rev = "a" * 40,
)
stale = time.time() - 1000
os.utime(old, (stale, stale))
new = _build_cached_repo(
tmp_path,
"org/multi-rev",
{"config.json": "{}"},
with_refs = False,
rev = "b" * 40,
)
configless = _build_cached_repo(
tmp_path,
"org/multi-rev",
{"tokenizer.json": "{}"},
with_refs = False,
rev = "c" * 40,
)
assert configless.exists()
resolved = resolve_local_snapshot_path("org/multi-rev", cache_dir = str(tmp_path))
assert resolved is not None
assert Path(resolved).resolve() == new.resolve()
def test_gguf_rows_select_gguf_bearing_snapshot_in_mixed_repos(tmp_path):
"""A mixed repo caching a newer safetensors revision beside an older GGUF
revision: the GGUF row's snapshot selection must return the GGUF-bearing
revision, not the safetensors one the model row prefers."""
import os
import sys
import time
backend_dir = str(Path(__file__).resolve().parent.parent)
if backend_dir not in sys.path:
sys.path.insert(0, backend_dir)
from hub.services.models.cache_inventory import (
_cached_gguf_repo_snapshot_path,
_cached_model_snapshot_path,
)
repo_dir = tmp_path / "models--org--mixed"
gguf_rev = repo_dir / "snapshots" / ("a" * 40)
gguf_rev.mkdir(parents = True)
(gguf_rev / "mixed-Q4_K_M.gguf").write_text("gguf-bytes")
stale = time.time() - 1000
os.utime(gguf_rev, (stale, stale))
st_rev = repo_dir / "snapshots" / ("b" * 40)
st_rev.mkdir(parents = True)
(st_rev / "config.json").write_text("{}")
(st_rev / "model.safetensors").write_text("weights")
gguf_pick = _cached_gguf_repo_snapshot_path(repo_dir)
assert gguf_pick is not None
assert Path(gguf_pick).resolve() == gguf_rev.resolve()
model_pick = _cached_model_snapshot_path(repo_dir)
assert model_pick is not None
assert Path(model_pick).resolve() == st_rev.resolve()
def test_weightless_newest_snapshot_does_not_shadow_complete_older_one(tmp_path):
"""A newest metadata-only revision (config.json, no weights) must not win
over an older revision holding the inventoried safetensors weights: the
inventory made the row eligible from the weightful revision, so the load
must resolve that one instead of failing on the weightless dir."""
import os
import time
complete = _build_cached_repo(
tmp_path,
"org/meta-newest",
{"config.json": "{}", "model.safetensors": "weights"},
with_refs = False,
rev = "a" * 40,
)
stale = time.time() - 1000
os.utime(complete, (stale, stale))
_build_cached_repo(
tmp_path,
"org/meta-newest",
{"config.json": "{}"},
with_refs = False,
rev = "b" * 40,
)
resolved = resolve_local_snapshot_path("org/meta-newest", cache_dir = str(tmp_path))
assert resolved is not None
assert Path(resolved).resolve() == complete.resolve()
def test_refless_fallback_without_config_resolves_to_none(tmp_path):
"""A snapshots dir with no config.json anywhere is not a loadable text
model cache; resolution must stay None (409 upstream), not guess."""
_build_cached_repo(
tmp_path,
"org/no-config",
{"tokenizer.json": "{}"},
with_refs = False,
)
assert resolve_local_snapshot_path("org/no-config", cache_dir = str(tmp_path)) is None
def test_resolution_never_uses_the_network(tmp_path, monkeypatch):
"""local_files_only resolution must not open any connection even when the
repo is absent (the tempting fallback would be a Hub metadata call)."""
import socket
def _no_network(*_args, **_kwargs):
raise AssertionError("network access attempted during local resolution")
monkeypatch.setattr(socket.socket, "connect", _no_network)
_build_cached_repo(tmp_path, "org/offline-ok", {"config.json": "{}"})
assert resolve_local_snapshot_path("org/offline-ok", cache_dir = str(tmp_path))
assert resolve_local_snapshot_path("org/absent", cache_dir = str(tmp_path)) is None

View file

@ -0,0 +1,146 @@
# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
"""Refcounted HF offline guard semantics.
Overlapping local-only validations/loads share one process-global env
override. The refcount means a request finishing first cannot restore the
environment while another local-only request still runs (which would let its
remaining metadata checks reach the Hub), and forced mode overrides an
explicitly falsy HF_HUB_OFFLINE=0 then restores it. The guard is extracted
from source and exercised with stubbed logging/DNS so no ML dependencies are
needed.
"""
from __future__ import annotations
import contextlib
import os
import threading
from pathlib import Path
import pytest
_LLAMA_CPP = Path(__file__).resolve().parent.parent / "core" / "inference" / "llama_cpp.py"
class _NullLogger:
def info(self, *args, **kwargs):
pass
def warning(self, *args, **kwargs):
pass
def _load_guard(dns_dead: bool = False):
src = _LLAMA_CPP.read_text()
start = src.index("def _hub_offline_env_truthy")
end = src.index("_SLOT_SAVE_MAX_BYTES")
end = src.rindex("try:", start, end)
block = src[start:end]
ns = {
"threading": threading,
"contextlib": contextlib,
"os": os,
"logger": _NullLogger(),
"_hf_env_offline": lambda: os.environ.get("HF_HUB_OFFLINE", "").strip().lower()
in {"1", "true", "yes", "on"},
"_probe_dns_dead": lambda: dns_dead,
}
exec(block, ns)
return ns["_hf_offline_if_dns_dead"]
@pytest.fixture
def clean_env(monkeypatch):
monkeypatch.delenv("HF_HUB_OFFLINE", raising = False)
monkeypatch.delenv("TRANSFORMERS_OFFLINE", raising = False)
def test_overlapping_guards_restore_only_after_last_exit(clean_env):
guard = _load_guard()
a = guard(force = True)
b = guard(force = True)
assert a.__enter__() is True
assert os.environ.get("HF_HUB_OFFLINE") == "1"
assert b.__enter__() is True
a.__exit__(None, None, None)
assert (
os.environ.get("HF_HUB_OFFLINE") == "1"
), "first exit must not restore while another guard is active"
b.__exit__(None, None, None)
assert "HF_HUB_OFFLINE" not in os.environ
def test_force_overrides_and_restores_falsy_env(clean_env):
guard = _load_guard()
os.environ["HF_HUB_OFFLINE"] = "0"
g = guard(force = True)
assert g.__enter__() is True
assert os.environ["HF_HUB_OFFLINE"] == "1"
g.__exit__(None, None, None)
assert os.environ["HF_HUB_OFFLINE"] == "0"
def test_falsy_env_stays_authoritative_for_ordinary_loads(clean_env):
guard = _load_guard(dns_dead = True)
os.environ["HF_HUB_OFFLINE"] = "0"
g = guard(force = False)
assert g.__enter__() is False
assert os.environ["HF_HUB_OFFLINE"] == "0"
g.__exit__(None, None, None)
def test_truthy_user_env_is_a_noop(clean_env):
guard = _load_guard()
os.environ["HF_HUB_OFFLINE"] = "1"
g = guard(force = True)
assert g.__enter__() is False
g.__exit__(None, None, None)
assert os.environ["HF_HUB_OFFLINE"] == "1"
def test_transformers_only_env_does_not_satisfy_forced_guard(clean_env):
"""TRANSFORMERS_OFFLINE=1 alone is not hub-offline: huggingface_hub
ignores it, so a forced guard must still install HF_HUB_OFFLINE for the
block and restore the prior state after."""
guard = _load_guard()
os.environ["TRANSFORMERS_OFFLINE"] = "1"
g = guard(force = True)
assert g.__enter__() is True
assert os.environ.get("HF_HUB_OFFLINE") == "1"
g.__exit__(None, None, None)
assert "HF_HUB_OFFLINE" not in os.environ
assert os.environ.get("TRANSFORMERS_OFFLINE") == "1"
def test_nonforce_never_extends_forced_window(clean_env):
"""An ordinary (non-force) guard entering while a forced override is
active must no-op, not join: its block tolerates either env state, and
joining would only widen the exposure of concurrent online work to the
process-global override."""
guard = _load_guard()
forced = guard(force = True)
plain = guard(force = False)
assert forced.__enter__() is True
assert plain.__enter__() is False
forced.__exit__(None, None, None)
assert (
"HF_HUB_OFFLINE" not in os.environ
), "the forced owner's exit restores; the ordinary no-op guard holds nothing"
plain.__exit__(None, None, None)
assert "HF_HUB_OFFLINE" not in os.environ
def test_forced_guards_share_windows_with_dns_dead_owners(clean_env):
"""A forced guard joining a DNS-dead override keeps the env until the
forced guard itself exits, even when the owner exits first."""
guard = _load_guard(dns_dead = True)
plain = guard(force = False)
forced = guard(force = True)
assert plain.__enter__() is True
assert forced.__enter__() is True
plain.__exit__(None, None, None)
assert os.environ.get("HF_HUB_OFFLINE") == "1"
forced.__exit__(None, None, None)
assert "HF_HUB_OFFLINE" not in os.environ

File diff suppressed because it is too large Load diff

View file

@ -169,6 +169,9 @@ export async function validateModel(
// --fit, while a pinned layer count is owned by the user. Tell validate
// so it applies the same training-guard policy as /load.
gpu_memory_mode: payload.gpu_memory_mode,
// Background auto-loads: resolve metadata from the local cache only
// and reject candidates whose runtime pulls remote auxiliaries.
local_files_only: payload.local_files_only ?? false,
}),
});
return parseJsonOrThrow<ValidateModelResponse>(response);
@ -380,6 +383,9 @@ export interface CachedModelRepo {
repo_id: string;
load_id?: string | null;
size_bytes: number;
/** Weight bytes of the newest cached snapshot only (what a load resolves);
* size_bytes sums selected-format blobs across every cached revision. */
snapshot_size_bytes?: number | null;
/** Epoch seconds of the newest downloaded weight file; sorts Downloaded
* newest-first. Optional for older-backend compatibility. */
last_modified?: number;
@ -946,6 +952,9 @@ export async function listGgufVariants(
options?: {
preferLocalCache?: boolean;
localPath?: string | null;
/** Aborts the underlying fetch; background scans pass a timeout signal so
* one hung request cannot gate Send forever. */
signal?: AbortSignal;
},
): Promise<GgufVariantsResponse> {
const params = new URLSearchParams({ repo_id: repoId });
@ -958,6 +967,7 @@ export async function listGgufVariants(
}
const response = await authFetch(`/api/models/gguf-variants?${params}`, {
headers: hubTokenHeader(hfToken),
signal: options?.signal,
});
return parseJsonOrThrow<GgufVariantsResponse>(response);
}

View file

@ -1080,21 +1080,39 @@ export function useChatModelRuntime() {
}
}
await refresh({ signal: abortCtrl.signal });
// Native-picked files are never remembered: their access depends
// on a signed, expiring path lease that a raw remembered path
// would bypass. Backend-indexed local rows (models dir, LM
// Studio, custom scan folders; selection source "local") are
// remembered and re-resolved through inventory on auto-load.
// Other arbitrary paths keep the isLocalModelPath protection.
const indexedLocalSelection =
typeof selection !== "string" && selection.source === "local";
if (
!isLora &&
!(loadResponse.is_lora ?? false) &&
!nativePathToken &&
!isLocalModelPath(modelId) &&
!isExternalModelId(modelId)
!isExternalModelId(modelId) &&
(indexedLocalSelection || !isLocalModelPath(modelId))
) {
if (loadResponse.is_gguf || isGguf || ggufVariant) {
const kind =
loadResponse.is_gguf || isGguf || ggufVariant
? ("gguf" as const)
: ("model" as const);
if (isLocalModelPath(modelId)) {
recordLastLocalModelLoad({
id: modelId,
kind: "gguf",
kind,
ggufVariant: ggufVariant ?? null,
loadId: modelId,
source: "local",
});
} else {
recordLastLocalModelLoad({ id: modelId, kind: "model" });
recordLastLocalModelLoad({
id: modelId,
kind,
ggufVariant: ggufVariant ?? null,
});
}
}
} catch (error) {

View file

@ -41,6 +41,9 @@ export interface LoadModelRequest {
load_in_4bit: boolean;
is_lora: boolean;
gguf_variant?: string | null;
/** Resolve a cached repo id against the local snapshot only and never
* download missing files (background auto-loads). */
local_files_only?: boolean;
/** Allow loading models with custom code (e.g. NVIDIA Nemotron). Only enable for repos you trust. */
trust_remote_code?: boolean;
/** sha256 fingerprint pinning user approval of this exact custom-code version. */

View file

@ -3,13 +3,46 @@
export type LastLocalModelKind = "gguf" | "model";
/**
* Where the remembered model came from:
* - "hf_cache": a managed Hugging Face cache repo (active or inactive cache),
* resolved through the cached-gguf / cached-models inventory.
* - "models_dir" / "lmstudio" / "custom": a backend-indexed local inventory
* row, resolved through the /api/hub/local inventory.
* - "local": an indexed local row whose exact scan source was not known at
* record time (interactive picker loads); resolved like the other local
* sources.
*
* Native file-picker selections are never recorded here: their access depends
* on a signed, expiring native path lease that must not be bypassed by a raw
* remembered path (the caller skips recording when a lease token is present).
*/
export type LastLocalModelSource =
| "hf_cache"
| "models_dir"
| "lmstudio"
| "custom"
| "local";
export type LastLocalModelLoad = {
/** Display / repository identity (HF repo id, or a local path for indexed rows). */
id: string;
kind: LastLocalModelKind;
/** Managed-cache GGUF quant. Null is valid when the load target itself
* identifies the actual GGUF (a local file or directory). */
ggufVariant: string | null;
/** Backend-provided load target when it differs from `id` (e.g. an
* inactive-cache row's load_id or an indexed local path). */
loadId: string | null;
/** Stable backend inventory row identity, when available. */
inventoryId: string | null;
source: LastLocalModelSource;
loadedAt: number;
};
// Kept at v1: new fields are parsed backward-compatibly, so existing records
// (which predate loadId/inventoryId/source) keep resolving as managed-cache
// entries without a migration pass.
const STORAGE_KEY = "unsloth.last-local-model-load.v1";
function storage(): Storage | null {
@ -24,6 +57,26 @@ function isLastLocalModelKind(value: unknown): value is LastLocalModelKind {
return value === "gguf" || value === "model";
}
const LOCAL_MODEL_SOURCES: readonly LastLocalModelSource[] = [
"hf_cache",
"models_dir",
"lmstudio",
"custom",
"local",
];
function isLastLocalModelSource(value: unknown): value is LastLocalModelSource {
return LOCAL_MODEL_SOURCES.includes(value as LastLocalModelSource);
}
export function isManagedCacheSource(source: LastLocalModelSource): boolean {
return source === "hf_cache";
}
function normalizeOptionalString(value: unknown): string | null {
return typeof value === "string" && value.trim() ? value : null;
}
export function readLastLocalModelLoad(): LastLocalModelLoad | null {
try {
const raw = storage()?.getItem(STORAGE_KEY);
@ -39,17 +92,25 @@ export function readLastLocalModelLoad(): LastLocalModelLoad | null {
) {
return null;
}
if (
parsed.kind === "gguf" &&
(typeof parsed.ggufVariant !== "string" || !parsed.ggufVariant.trim())
) {
// Legacy v1 records carry no source; they were only ever written for
// managed-cache repos.
const source = isLastLocalModelSource(parsed.source)
? parsed.source
: "hf_cache";
const ggufVariant = normalizeOptionalString(parsed.ggufVariant);
// A managed-cache GGUF loads by repo + quant, so the quant is required.
// An indexed local GGUF's load target identifies the file itself, so a
// null variant stays valid.
if (parsed.kind === "gguf" && source === "hf_cache" && !ggufVariant) {
return null;
}
return {
id: parsed.id,
kind: parsed.kind,
ggufVariant:
typeof parsed.ggufVariant === "string" ? parsed.ggufVariant : null,
ggufVariant,
loadId: normalizeOptionalString(parsed.loadId),
inventoryId: normalizeOptionalString(parsed.inventoryId),
source,
loadedAt: parsed.loadedAt,
};
} catch {
@ -61,22 +122,31 @@ export function recordLastLocalModelLoad(input: {
id: string;
kind: LastLocalModelKind;
ggufVariant?: string | null;
loadId?: string | null;
inventoryId?: string | null;
source?: LastLocalModelSource;
}): void {
const id = input.id.trim();
if (!id) {
return;
}
const source = input.source ?? "hf_cache";
const ggufVariant = input.ggufVariant?.trim() || null;
if (input.kind === "gguf" && !ggufVariant) {
if (input.kind === "gguf" && source === "hf_cache" && !ggufVariant) {
return;
}
try {
// Only inventory identity is stored: never tokens, native path leases, or
// security approvals.
storage()?.setItem(
STORAGE_KEY,
JSON.stringify({
id,
kind: input.kind,
ggufVariant: input.kind === "gguf" ? ggufVariant : null,
loadId: input.loadId?.trim() || null,
inventoryId: input.inventoryId?.trim() || null,
source,
loadedAt: Date.now(),
} satisfies LastLocalModelLoad),
);

View file

@ -65,6 +65,9 @@ export interface CachedModelRepo {
format_variant?: string | null;
capabilities?: BackendModelCapabilities | null;
size_bytes: number;
/** Weight bytes of the newest cached snapshot only (what a load resolves);
* size_bytes sums selected-format blobs across every cached revision. */
snapshot_size_bytes?: number | null;
cache_path?: string;
last_modified?: number | null;
partial?: boolean;

View file

@ -296,9 +296,14 @@ def test_chat_autoload_scopes_variant_lookup_to_cached_repo_path():
"""Autoload must probe the exact cache row it will load, including rows
retained from a previously selected Hugging Face cache."""
src = _read("features/chat/api/chat-adapter.ts")
auto_load = src.split("async function autoLoadSmallestModel", 1)[1]
assert auto_load.count("preferLocalCache: true") >= 2
assert auto_load.count("localPath: repo.cache_path") >= 2
auto_load = src.split("async function autoLoadOnDeviceModel", 1)[1]
# 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]
@ -580,6 +585,802 @@ def test_legacy_migration_is_idempotent_and_non_destructive():
assert "if (isDefaultConfig(migrated) || Object.hasOwn(map, key)) {" in src
# ---------------------------------------------------------------------------
# Send-with-no-model auto-load (issue #7374): on-device discovery must cover
# every picker inventory source, the remembered model must survive local
# (non-cache) loads, and the send path must never start a remote download.
# ---------------------------------------------------------------------------
def _autoload_section() -> str:
src = _read("features/chat/api/chat-adapter.ts")
return src.split("async function autoLoadOnDeviceModel", 1)[1]
def test_send_path_cannot_reach_hardcoded_default_download():
"""Pressing Send with no model loaded must never fetch the hard-coded
default repo from Hugging Face (the unconsented download in the bug
report). Any recommended download must stay an explicit user action."""
src = _read("features/chat/api/chat-adapter.ts")
assert "Qwen3.5-4B-MTP-GGUF" not in src
assert "Downloading a small model" not in src
assert "No downloaded models found" not in src
# The old entry point must not linger anywhere.
assert "autoLoadSmallestModel" not in src
# The renamed entry point runs exactly once per send, so the submitted
# prompt executes exactly once after a successful load.
assert src.count("await autoLoadOnDeviceModel())") == 1
def test_autoload_no_model_error_is_actionable():
"""With no valid on-device candidate the user is told to select or
explicitly download a model instead of getting a silent remote load."""
src = _read("features/chat/api/chat-adapter.ts")
assert "Select a model in the top bar, or download one from the Hub, then retry." in src
def test_autoload_inventory_failure_is_not_empty_inventory():
"""A failed cached/local inventory request must stop the automatic
selection path, not be swallowed into an empty list that used to fall
through to the remote default download."""
src = _read("features/chat/api/chat-adapter.ts")
assert ".catch(() => [])" not in src
auto_load = _autoload_section()
assert "inventoryErrorSurfaced: true" in auto_load
# All three inventory sources are queried together and fail closed.
for needle in (
"listCachedGguf(hfToken)",
"listCachedModels(hfToken)",
"listLocalModels()",
):
assert needle in auto_load, needle
def test_autoload_uses_unified_backend_inventory():
"""Auto-load must consume the same non-React backend inventory the
unified picker uses (no second frontend filesystem scanner), covering
the models dir, LM Studio dirs, and custom scan folders."""
src = _read("features/chat/api/chat-adapter.ts")
assert re.search(
r'import \{[^}]*listLocalModels[^}]*\} from "@/features/hub/inventory/api"',
src,
re.S,
)
sources = re.search(r"const AUTO_LOAD_LOCAL_SOURCES[^;]*;", src, re.S)
assert sources, "AUTO_LOAD_LOCAL_SOURCES not found"
for source in ('"models_dir"', '"lmstudio"', '"custom"'):
assert source in sources.group(0), source
def test_autoload_filters_match_picker_policy():
"""Only complete, chat-capable, non-hidden rows may auto-load: partial
downloads, weightless/non-chat folders, and infrastructure models are
excluded with the same policy the picker applies."""
src = _read("features/chat/api/chat-adapter.ts")
local_fn = src.split("function isAutoLoadableLocalRow", 1)[1]
local_fn = local_fn.split("\nfunction ", 1)[0]
assert "row.capabilities?.can_chat !== true" in local_fn
assert "row.partial" in local_fn
# Adapters resolve their base model on load; a Hub-id base would start
# the implicit remote fetch a background auto-load must never trigger.
assert 'row.model_format === "adapter"' in local_fn
assert "isHiddenModelId(row.model_id, row.id, row.path)" in local_fn
# The name-based marker only applies to direct .gguf files; a directory
# named e.g. /models/foo-be says nothing about the files inside it, which
# are filtered per-file during variant resolution.
assert 'row.path.toLowerCase().endsWith(".gguf") &&' in local_fn
assert "hasBigEndianGgufMarker(row.path, row.format_variant)" in local_fn
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
def test_autoload_local_rows_load_via_backend_target():
"""Indexed local rows (models dir, LM Studio, custom scan folders) must
load through the backend-provided target and record their stable
inventory identity, never a reconstructed path or synthetic variant."""
src = _read("features/chat/api/chat-adapter.ts")
assert "function localRowLoadTarget" in src
assert "row.load_id || row.id" in src
candidate_fn = src.split("function localRowToCandidate", 1)[1]
candidate_fn = candidate_fn.split("\n/**", 1)[0]
assert "loadId: localRowLoadTarget(row)" in candidate_fn
assert "inventoryId: row.inventory_id ?? null" in candidate_fn
# Inactive-cache rows keep loading by their backend load_id.
auto_load = _autoload_section()
assert auto_load.count("loadId: repo.load_id") >= 3
def test_autoload_remembers_last_model_across_all_sources():
"""The remembered model resolves against managed caches AND the indexed
local inventory; a stale entry only falls through to other on-device
candidates (there is no remote branch left to reach)."""
auto_load = _autoload_section()
assert "isManagedCacheSource(lastLoaded.source)" in auto_load
assert "matchesRememberedLocalRow(candidateRow, lastLoaded)" in auto_load
assert "await resolveLocalRowCandidate(" in auto_load
assert "lastLoaded.ggufVariant," in auto_load
# Managed-cache candidates record their provenance for later resolution.
assert auto_load.count('source: "hf_cache"') >= 4
def test_autoload_deduplicates_cached_and_local_candidates():
"""Candidates resolving to the same load target (e.g. a custom scan
folder pointing into an HF cache) must not be tried twice, but the
dedupe must key on actual load targets/paths only: a local copy that
merely shares a repo model_id is a distinct set of files and must stay
available when the cached copy fails or has no usable quant."""
auto_load = _autoload_section()
assert "const seenLoadTargets = new Set<string>()" 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}:${normalizeLoadTargetKey(alias)}`)" 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
def test_local_quant_resolution_skips_failed_quants():
"""When a folder's smallest quant already failed or was blocked, the
resolver must return the next complete quant instead of abandoning the
whole folder (which made Send falsely report no 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 "for (const entry of downloaded)" in resolve_fn
assert "if (isSkippedCandidate?.(candidate)) continue;" in resolve_fn
# The fallback loop feeds the skip set into resolution.
auto_load = _autoload_section()
assert "isSkippedAutoLoadCandidate," in auto_load
def test_autoload_trust_guard_still_blocks_background_loads():
"""A model needing custom-code approval or a security review is never
silently auto-loaded, and a blocked candidate can only cascade to other
on-device candidates."""
auto_load = _autoload_section()
assert "validation.requires_trust_remote_code" in auto_load
assert "validation.requires_security_review" in auto_load
assert "MAX_AUTO_LOAD_ATTEMPTS" in auto_load
def test_remembered_model_record_supports_local_sources():
"""last-local-model-load must represent managed-cache models AND
backend-indexed local models: a local GGUF is valid with a null variant,
legacy v1 records keep resolving as managed-cache entries, and no
secrets (tokens/leases) are ever persisted."""
src = _read("features/chat/utils/last-local-model-load.ts")
# Same storage key: v1 records parse backward-compatibly, no migration.
assert 'const STORAGE_KEY = "unsloth.last-local-model-load.v1";' in src
# Legacy records carry no source and default to the managed cache.
assert "isLastLocalModelSource(parsed.source)" in src
assert ': "hf_cache";' in src
# The GGUF-variant requirement is scoped to managed-cache records; a
# local GGUF's load target identifies the file, so null stays valid.
assert src.count('source === "hf_cache" && !ggufVariant') == 2
# Indexed local scan sources are representable.
for source in ('"models_dir"', '"lmstudio"', '"custom"'):
assert source in src, source
# Identity only: never tokens, native path leases, or approvals.
assert "nativePath" not in src
assert "hfToken" not in src and "hf_token" not in src
assert "fingerprint" not in src
def test_interactive_local_loads_are_remembered_without_lease_bypass():
"""A successful interactive load of a backend-indexed local model
(picker source "local") must be remembered so auto-load can reuse it,
while native-picked files (signed, expiring path lease) and other
arbitrary paths must never be recorded."""
src = _read("features/chat/hooks/use-chat-model-runtime.ts")
record_block = src.split("const indexedLocalSelection", 1)[1]
record_block = record_block.split("} catch (error) {", 1)[0]
assert 'selection.source === "local"' in src
assert "!nativePathToken &&" in record_block
assert "(indexedLocalSelection || !isLocalModelPath(modelId))" in record_block
assert 'source: "local",' in record_block
def test_remembered_local_row_match_requires_kind_agreement():
"""A folder holding both GGUF and safetensors weights yields two inventory
rows with the same path/load target, so the remembered kind must gate the
identifier match or a remembered safetensors load can resolve to the GGUF
row (and vice versa)."""
src = _read("features/chat/api/chat-adapter.ts")
match_fn = src.split("function matchesRememberedLocalRow", 1)[1]
match_fn = match_fn.split("\nasync function ", 1)[0].split("\nfunction ", 1)[0]
assert '(row.model_format === "gguf") !== (remembered.kind === "gguf")' in match_fn
def test_directory_gguf_rows_resolve_variant_like_picker():
"""Directory-based local GGUFs (LM Studio, models dir, custom folders) are
flagged requires_variant by the backend, so the fallback must resolve a
quant through the variants API (as the picker card does) instead of
silently dropping every directory row; non-GGUF variant-requiring rows
have no background resolution and stay excluded."""
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 "row.capabilities?.requires_variant === true" in resolve_fn
assert "if (!isGguf) return null;" 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 "listGgufVariantsBounded(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(" in auto_load
def test_remembered_local_failure_does_not_block_folder_fallback():
"""A failed remembered model must exclude only that exact candidate key,
not mark the whole row or repo as seen; otherwise a folder or cache repo
with another complete quant can never fall back and Send falsely reports
no model. Applies to local rows and managed-cache repos alike."""
src = _read("features/chat/api/chat-adapter.ts")
auto_load = src.split("async function autoLoadOnDeviceModel", 1)[1]
remembered_block = auto_load.split("if (lastLoaded) {", 1)[1]
remembered_block = remembered_block.split("// On-device fallback", 1)[0]
assert (
"markSeen(" not in remembered_block
), "remembered paths must not pre-mark their row/repo as deduped"
assert "autoLoadSkipKey(rememberedCandidate)" in remembered_block
def test_fallback_orders_by_resolved_quant_size():
"""A GGUF folder or cache repo row's size_bytes sums every quant in it, so
the smallest-first fallback must order both local rows and cached repos by
the resolved quant's own size; otherwise a repo holding one 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 rows order on the resolved quant size.
assert "sizeBytes: resolved.sizeBytes" in auto_load
# Cached GGUF repos order on the resolved quant size too.
assert "const resolveCachedGgufEntry" in auto_load
assert "sizeBytes: sizeOrUnknownBytes(variant.size_bytes)" in auto_load
# Non-GGUF cached repos order on the SELECTED snapshot's size (falling
# back to the all-revisions row sum for older backends).
seed_block = auto_load.split("for (const repo of platform.chatOnly ? [] : modelRepos)", 1)[1]
seed_block = seed_block.split("const resolveCachedGgufEntry", 1)[0]
assert "repo.snapshot_size_bytes ?? repo.size_bytes" in seed_block
def test_cascade_retries_next_quant_after_load_failure():
"""A failed /api/inference/load (not just a blocked validation) must mark
that quant skipped and re-enter the folder's or repo's next complete quant
into the GLOBAL size order (still ahead of the safetensors group) instead
of retrying inline, so one folder of failing quants cannot starve a
smaller model elsewhere; single-candidate rows resolve to null once
skipped, and the attempt cap bounds total loads."""
src = _read("features/chat/api/chat-adapter.ts")
auto_load = src.split("async function autoLoadOnDeviceModel", 1)[1]
# No inline retry loop: retries re-enter the shared ordered pool.
assert "while (localCandidate" not in auto_load
assert "retry: true," in auto_load
assert "insertReady({ ...next, retry: true })" in auto_load
# Ordered insertion respects the GGUF-before-safetensors group boundary
# and the ascending size order among the remaining candidates.
assert "!isModelKindEntry(readyPool[at])" in auto_load
assert "readyPool[at].sizeBytes <= entry.sizeBytes" in auto_load
# Requeued entries bypass the seen gate; fresh rows still dedupe.
assert "if (!candidate.retry) {" in auto_load
# The cascade catch records the failed quant before requeueing.
assert "skippedAutoLoadCandidates.add(skipKey)" in auto_load
assert "skippedAutoLoadCandidates.add(autoLoadSkipKey(localCandidate))" in auto_load
# Termination guard: a skipped single candidate resolves to null.
resolve_fn = src.split("async function resolveLocalRowCandidate", 1)[1]
resolve_fn = resolve_fn.split("\nfunction ", 1)[0]
assert "if (isSkippedCandidate?.(candidate)) return null;" in resolve_fn
def test_cached_rows_deduped_against_local_aliases():
"""A cached repo and an indexed local row can alias the same files (e.g.
a scan folder pointing into an HF cache). The fallback must not spend a
second load attempt re-trying files already visited or failed through the
other row: cached branches apply the same seen gate local rows use, and
skip keys are scoped to the backend load target both rows share."""
src = _read("features/chat/api/chat-adapter.ts")
auto_load = src.split("async function autoLoadOnDeviceModel", 1)[1]
assert 'if (isSeen("gguf", repo.load_id || repo.repo_id, repo.cache_path))' in auto_load
assert 'if (isSeen("model", repo.load_id || repo.repo_id, repo.cache_path))' in auto_load
key_fn = src.split("function autoLoadSkipKey", 1)[1]
key_fn = key_fn.split("\nfunction ", 1)[0]
assert "candidate.loadId ?? candidate.id" in key_fn
def test_send_not_blocked_by_full_inventory_resolution():
"""Pressing Send must not wait for every /gguf-variants folder scan before
the first load attempt: candidates resolve through a bounded worker pool
and are consumed incrementally after a short settle grace, so one slow
folder cannot stall the send path behind the transport timeout."""
src = _read("features/chat/api/chat-adapter.ts")
assert "const AUTO_LOAD_RESOLVE_GRACE_MS" in src
auto_load = src.split("async function autoLoadOnDeviceModel", 1)[1]
# The consumer never awaits full resolution; it waits for the grace
# window (cut short when resolution finishes) and then per-completion.
assert "await resolutionDone" not in auto_load
assert "clearTimeout(graceTimer)" in auto_load
assert "await nextProgress();" in auto_load
assert "if (pendingJobs <= 0) {" in auto_load
# Rows that need no backend scan seed the pool before the workers start,
# so they can never queue behind slow folder scans.
assert "const needsVariantScan" in auto_load
assert ".filter((row) => !needsVariantScan(row))" in auto_load
assert auto_load.index(".filter((row) => !needsVariantScan(row))") < auto_load.index(
"const cachedScanJobs"
)
# Cached and local scans interleave so one slow source cannot
# monopolize every worker.
assert "resolutionJobs.push(cachedScanJobs[jobIndex])" in auto_load
assert "resolutionJobs.push(localScanJobs[jobIndex])" in auto_load
def test_pending_gguf_scans_gate_safetensors_candidates():
"""GGUF-first is the documented preference order, and incremental
consumption must not let an instantly-resolved safetensors row claim a
load slot while a pending folder scan can still yield a GGUF candidate;
resolved GGUF entries are never gated."""
src = _read("features/chat/api/chat-adapter.ts")
auto_load = src.split("async function autoLoadOnDeviceModel", 1)[1]
assert "if (isModelKindEntry(candidate) && pendingJobs > 0) {" in auto_load
def test_only_first_attempt_leapfrogs_pending_scans():
"""Fast-resolving candidates whose loads fail must not exhaust the attempt
cap while pending scans can still yield smaller loadable quants: only the
first (latency-critical) attempt may run ahead of pending scans; once any
budget is spent, the remaining attempts wait for the settled global
smallest-first order."""
src = _read("features/chat/api/chat-adapter.ts")
auto_load = src.split("async function autoLoadOnDeviceModel", 1)[1]
assert "if (pendingJobs > 0 && loadAttempts > 0) {" in auto_load
def test_hidden_matcher_fetch_never_blocks_send_unbounded():
"""ensureHiddenModelMatchers has no timeout of its own (unlike the
30s-bounded inventory calls), so the send path must not await it serially:
it runs alongside inventory discovery and is only awaited through a short
grace, after which the static needles filter alone."""
src = _read("features/chat/api/chat-adapter.ts")
auto_load = src.split("async function autoLoadOnDeviceModel", 1)[1]
assert "await ensureHiddenModelMatchers()" not in auto_load
assert "const hiddenMatchersReady = ensureHiddenModelMatchers().catch(" in auto_load
# The platform probe backing the picker's format gates is unbounded too
# (raw fetch, no signal), so it joins the same bounded prefetch wait.
assert "await fetchDeviceType();" not in auto_load
assert "const platformReady: Promise<void> = fetchDeviceType().then(" in auto_load
assert "const BEST_EFFORT_PREFETCH_GRACE_MS" in src
assert "setTimeout(resolve, BEST_EFFORT_PREFETCH_GRACE_MS)" in auto_load
assert "clearTimeout(matcherTimer)" in auto_load
def test_resolution_workers_stop_on_terminal_result():
"""A successful early load (or any other terminal outcome) must stop the
workers from claiming further folder scans, so autoload cannot leave
background scans contending with inference for the backend and disk."""
src = _read("features/chat/api/chat-adapter.ts")
auto_load = src.split("async function autoLoadOnDeviceModel", 1)[1]
assert "let resolutionStopped = false;" in auto_load
assert "while (!resolutionStopped && nextJob < resolutionJobs.length)" in auto_load
# The flag is set in a finally so every exit path (return, break, throw)
# stops the workers.
assert "resolutionStopped = true;" in auto_load
assert auto_load.index("} finally {") < auto_load.index("resolutionStopped = true;")
def test_local_rows_apply_picker_platform_gate():
"""Chat-only installs run GGUF (any host) and MLX (Mac only); the picker
hides other local formats and all cached non-GGUF rows there, so the
background cascade must not load a row the user could not have picked.
The remembered path stays ungated: a recorded load is user precedent."""
src = _read("features/chat/api/chat-adapter.ts")
local_fn = src.split("function isAutoLoadableLocalRow", 1)[1]
local_fn = local_fn.split("\nfunction ", 1)[0]
assert "platform.chatOnly" in local_fn
assert "localRowIsGgufLike(row)" in local_fn
assert "platform.isMac && localRowIsMlxNamed(row)" in local_fn
auto_load = src.split("async function autoLoadOnDeviceModel", 1)[1]
# The platform snapshot hydrates through the bounded best-effort prefetch
# (see test_hidden_matcher_fetch_never_blocks_send_unbounded).
assert "fetchDeviceType().then(" in auto_load
# Cascade seeding of cached non-GGUF repos mirrors the picker's
# chat-only exclusion; the remembered lookup above it stays unfiltered.
assert "platform.chatOnly ? [] : modelRepos" in auto_load
def test_autoload_keys_preserve_posix_path_case():
"""Linux filesystems distinguish /models/Foo from /models/foo, so seen
keys and remembered-model matching must not fold case on POSIX paths;
Windows-style paths and Hub repo ids keep case-insensitive matching."""
src = _read("features/chat/api/chat-adapter.ts")
norm_fn = src.split("function normalizeLoadTargetKey", 1)[1]
norm_fn = norm_fn.split("\nfunction ", 1)[0].split("\nconst ", 1)[0]
assert "looksWindowsPath" in norm_fn
assert 'value.startsWith("/") || value.startsWith("~")' in norm_fn
assert "return value;" in norm_fn
assert "return value.toLowerCase();" in norm_fn
match_fn = src.split("function matchesRememberedLocalRow", 1)[1]
match_fn = match_fn.split("\nfunction ", 1)[0]
assert "normalizeLoadTargetKey" in match_fn
# Inventory ids compare exactly (same backend generator on both sides).
assert "row.inventory_id === remembered.inventoryId" in match_fn
assert "row.inventory_id.toLowerCase()" not in match_fn
# Skip keys use the same semantics: a failure recorded for /models/Foo
# must not also skip /models/foo.
key_fn = src.split("function autoLoadCandidateKey", 1)[1]
key_fn = key_fn.split("\nfunction ", 1)[0]
assert "normalizeLoadTargetKey(id)" in key_fn
assert "id.toLowerCase()" not in key_fn
def test_local_variant_scans_bounded_concurrency():
"""Each /gguf-variants call triggers a recursive backend directory scan,
so pre-resolution must not fan out unbounded over every indexed folder
at once."""
src = _read("features/chat/api/chat-adapter.ts")
assert "const AUTO_LOAD_VARIANT_SCAN_CONCURRENCY" in src
auto_load = src.split("async function autoLoadOnDeviceModel", 1)[1]
assert "Math.min(AUTO_LOAD_VARIANT_SCAN_CONCURRENCY, resolutionJobs.length)" in auto_load
assert "await Promise.all(\n cascadeLocalRows.map(" not in auto_load
BACKEND = WORKDIR / "studio" / "backend"
def _read_backend(rel: str) -> str:
path = BACKEND / rel
assert path.exists(), f"missing backend source file: {path}"
return path.read_text()
def test_background_loads_resolve_local_files_only():
"""A cache populated outside Studio can pass the partial check while
missing shard files, and from_pretrained on a repo id downloads the gaps.
Background auto-loads therefore send local_files_only and the load route
rewrites the path to the locally resolved snapshot (identity intact), so
an incomplete cache fails over to the next candidate instead of
downloading on Send."""
src = _read("features/chat/api/chat-adapter.ts")
load_fn = src.split("async function loadAutoLoadCandidate", 1)[1]
load_fn = load_fn.split("loadAttempts += 1;", 1)[1]
assert "local_files_only: true," in load_fn
types = _read("features/chat/types/api.ts")
assert "local_files_only?: boolean;" in types
request_model = _read_backend("models/inference.py")
assert "local_files_only: bool = Field(" in request_model
route = _read_backend("routes/inference.py")
assert "request.local_files_only" in route
assert "resolve_local_snapshot_path" in route
assert "config.path = local_snapshot" in route
# Uncached repos fail closed with a conflict, never a download.
rewrite = route.split("request.local_files_only", 1)[1]
rewrite = rewrite.split("config.path = local_snapshot", 1)[0]
assert "status_code = 409" in rewrite
helper = _read_backend("hub/utils/local_snapshot.py")
assert "local_files_only = True" in helper
# The rewrite resolves against the LIVE Studio-managed cache location,
# not huggingface_hub's import-time default.
assert "str(get_hf_cache_paths().hub_cache)," in route
def test_background_candidate_filters_have_no_side_effects():
"""Round-13 gates. Local checkpoint rows carry pickle weights with no Hub
security scan, so they are never background-picked. The canAutoLoad probe
validates with the same local-only policy /load enforces, and the validate
route runs it offline so the metadata probe cannot reach the Hub. Non-GGUF
audio models are refused under local-only in both routes because their
codec runtimes download auxiliaries at load time. Revision-only caches
(refs pruned) still resolve through the snapshot-dir fallback."""
adapter = _read("features/chat/api/chat-adapter.ts")
assert 'if (row.model_format === "checkpoint") {' in adapter
can_fn = adapter.split("async function canAutoLoad", 1)[1]
can_fn = can_fn.split("async function", 1)[0]
assert "local_files_only: true," in can_fn
chat_api = _read("features/chat/api/chat-api.ts")
assert "local_files_only: payload.local_files_only ?? false," in chat_api
request_model = _read_backend("models/inference.py")
validate_schema = request_model.split("class ValidateModelRequest", 1)[1]
assert "local_files_only: bool = Field(" in validate_schema
route = _read_backend("routes/inference.py")
# /load forces offline resolution under local-only; /validate covers its
# whole preflight via the ExitStack wrap (asserted separately below).
assert route.count("with _hf_offline_if_dns_dead(force = request.local_files_only):") == 1
# Audio gate present in both routes, GGUF exempt (llama.cpp path already
# resolves companions cached-or-skipped under the flag).
assert route.count("needs audio codec downloads") == 2
for gate in route.split("needs audio codec downloads")[:-1]:
tail = gate.rsplit("if request.local_files_only", 1)[1]
assert "is_gguf" in tail and "is_audio" in tail
llama = _read_backend("core/inference/llama_cpp.py")
assert "def _hf_offline_if_dns_dead(force: bool = False):" in llama
# force must also override an explicitly falsy HF_HUB_OFFLINE=0 (only a
# TRUTHY env value short-circuits), and overlapping guards are refcounted
# so the env is restored only when the LAST one exits; behavior is
# exercised directly in test_offline_guard_refcount.py.
assert "elif _hf_env_offline() and (not force or _hub_offline_env_truthy()):" in llama
assert "_OFFLINE_GUARD_LOCK" in llama
assert '_OFFLINE_GUARD_STATE["count"] += 1' in llama
helper = _read_backend("hub/utils/local_snapshot.py")
assert "def _snapshot_dir_fallback(" in helper
assert "config.json" in helper
def test_local_only_covers_every_load_and_validate_network_path():
"""Round-14 gates. The Vulkan-ordinal GGUF preflight downloads FIRST, so it
takes the same local-only flag and forced-offline wrap as the Phase 2
download (whose cache-size check would otherwise call get_paths_info).
The validate route keeps its whole metadata/security preflight offline, not
just the identifier probe. The python load path survives the process
boundary: the orchestrator forwards the flag and the route-resolved
snapshot path into the worker, which runs the entire load under a scoped
offline env and passes the flag to the vision processor fallback."""
llama = _read_backend("core/inference/llama_cpp.py")
# Both GGUF download blocks in load_model force offline under local-only.
assert llama.count("with _hf_offline_if_dns_dead(force = local_files_only):") == 2
preflight = llama.split("_preflight_model_path = self._download_gguf(", 1)[1]
preflight = preflight.split(")", 1)[0]
assert "local_files_only = local_files_only," in preflight
route = _read_backend("routes/inference.py")
validate_src = route.split('operation = "validate-model"', 1)[1]
assert (
"_local_only_offline.enter_context(_hf_offline_if_dns_dead(force = True))" in validate_src
)
assert "_local_only_offline.close()" in validate_src
# The non-GGUF load threads the flag into the subprocess backend.
load_call = route.split("backend.load_model,\n config = config,", 1)[1]
load_call = load_call.split(")", 1)[0]
assert "local_files_only = request.local_files_only," in load_call
orchestrator = _read_backend("core/inference/orchestrator.py")
assert '"local_files_only": bool(local_files_only),' in orchestrator
assert '"local_snapshot_path"' in orchestrator
worker = _read_backend("core/inference/worker.py")
assert "def _local_only_offline_env(" in worker
assert "_offline_guard.enter_context(" in worker
assert "_offline_guard.close()" in worker
# The route's snapshot rewrite is re-applied after the worker rebuilds
# its ModelConfig from the identifier.
assert 'snapshot_override = config.get("local_snapshot_path")' in worker
assert "mc.path = snapshot_override" in worker
assert '"local_files_only": bool(config.get("local_files_only", False)),' in worker
inference = _read_backend("core/inference/inference.py")
processor_call = inference.split("processor = AutoProcessor.from_pretrained(", 1)[1]
processor_call = processor_call.split("logger.info", 1)[0]
assert "local_files_only = local_files_only," in processor_call
def test_background_picks_mirror_inventory_and_skip_installers():
"""Round-15 gates. Cached checkpoint repos (pickle weights) are excluded
from background picks like local checkpoint rows. The worker keeps its
ENTIRE bootstrap offline under local-only and never pip-installs SSM
kernels (missing fatal kernels fail into candidate failover). Snapshot
resolution prefers the newest snapshot dir, matching the inventory
scanner's latest_snapshot_dir selection, before consulting refs/main.
MLX loads read config.path so the live-cache rewrite is honored. Cached
non-GGUF ordering uses the selected snapshot's size, not the
all-revisions blob total."""
adapter = _read("features/chat/api/chat-adapter.ts")
cached_filter = adapter.split("function isAutoLoadableCachedRepo", 1)[1]
cached_filter = cached_filter.split("AUTO_LOAD_LOCAL_SOURCES", 1)[0]
assert 'if (repo.model_format === "checkpoint") {' in cached_filter
assert "repo.snapshot_size_bytes ?? repo.size_bytes" in adapter
worker = _read_backend("core/inference/worker.py")
assert "_bootstrap_offline = contextlib.ExitStack()" in worker
# Entered before base resolution / gates / kernels, closed before BOTH
# command loops (MLX and GPU paths).
bootstrap = worker.split("_bootstrap_offline = contextlib.ExitStack()", 1)[1]
assert bootstrap.count("_bootstrap_offline.close()") == 2
ssm_sig = worker.split("def _ensure_ssm_kernels(", 1)[1].split(") -> bool:", 1)[0]
assert "local_files_only: bool = False" in ssm_sig
ssm = worker.split("def _ensure_ssm_kernels", 1)[1]
ssm = ssm.split("def _run_security_gates", 1)[0]
assert "if local_files_only:" in ssm
assert 'importlib.util.find_spec("mamba_ssm") is None' in ssm
helper = _read_backend("hub/utils/local_snapshot.py")
resolve = helper.split("def resolve_local_snapshot_path", 1)[1]
# Newest-snapshot scan runs BEFORE the refs/main-based resolver (compare
# the actual calls, not docstring mentions).
assert resolve.index("resolved = _snapshot_dir_fallback(") < resolve.index(
"return snapshot_download("
)
mlx = _read_backend("core/inference/mlx_inference.py")
assert 'load_source = getattr(config, "path", None) or model_name' in mlx
inventory = _read_backend("hub/services/models/cache_inventory.py")
assert "snapshot_size_bytes" in inventory
assert "def _snapshot_dir_mtime(" in inventory
schema = _read_backend("hub/schemas/inventory.py")
assert "snapshot_size_bytes" in schema
def test_forced_offline_is_hub_specific_and_covers_parent_preflight():
"""Round-16 gates. A forced guard may only no-op when HF_HUB_OFFLINE
itself is truthy (huggingface_hub ignores TRANSFORMERS_OFFLINE), snapshot
selection prefers revisions that hold the inventoried safetensors weights
so a metadata-only newest revision cannot shadow a complete older one,
and the orchestrator's parent-side preflight (transformers tier probe,
GPU sizing via hf model_info) runs under the local-only guard, closed
before the worker spawn so the child does not inherit the env."""
llama = _read_backend("core/inference/llama_cpp.py")
assert "def _hub_offline_env_truthy(" in llama
assert "not force or _hub_offline_env_truthy()" in llama
helper = _read_backend("hub/utils/local_snapshot.py")
assert "def _snapshot_has_weights(" in helper
assert "max(weightful or candidates, key = os.path.getmtime)" in helper
orchestrator = _read_backend("core/inference/orchestrator.py")
preflight = orchestrator.split("def _preflight_offline():", 1)[1]
assert "_hf_offline_if_dns_dead(force = True)" in preflight
# Both metadata call sites are guarded, and the guard closes before spawn.
assert preflight.count("with _preflight_offline():") == 2
tier_probe = preflight.split("with _preflight_offline():", 1)[1]
assert "needs_transformers_5" in tier_probe.split("with _preflight_offline():", 1)[0]
assert "prepare_gpu_selection(" in tier_probe.split("_spawn_subprocess", 1)[0]
def test_generation_and_scan_paths_stay_bounded_and_local():
"""Round-17 gates. Inactive-cache rows emit a weight-bearing snapshot as
their load_id (the load consumes it directly, bypassing the repo-id
resolver). Ordinary offline guards never extend a forced window. The
generation-time native template reload honors the load's local-only flag
and resolved path instead of refetching the repo id online. Every GGUF
variant scan behind the model-kind gate is bounded by an abortable
timeout, and an HF cache snapshot registered as a custom folder dedupes
against its cached row through the shared cache root."""
inventory = _read_backend("hub/services/models/cache_inventory.py")
assert "def _weightful_snapshot_path(" in inventory
resolver = inventory.split("def _cached_model_snapshot_path(", 1)[1]
resolver = resolver.split("def ", 1)[0]
assert "_weightful_snapshot_path(repo_path)" in resolver
llama = _read_backend("core/inference/llama_cpp.py")
join_branch = llama.split('if _OFFLINE_GUARD_STATE["count"] > 0:', 1)[1]
join_branch = join_branch.split("elif", 1)[0]
assert "if force:" in join_branch
helpers = _read_backend("core/inference/chat_template_helpers.py")
reload_block = helpers.split("native_chat_template", 1)[1]
reload_block = reload_block.split("model_info[", 1)[0]
assert 'local_files_only = bool(model_info.get("local_files_only", False))' in reload_block
assert 'template_source = model_info.get("model_path") or template_source' in reload_block
assert "local_files_only = local_files_only," in reload_block
inference = _read_backend("core/inference/inference.py")
assert '"local_files_only": local_files_only,' in inference
mlx = _read_backend("core/inference/mlx_inference.py")
assert '"local_files_only": local_files_only,' in mlx
assert '"model_path": load_source,' in mlx
adapter = _read("features/chat/api/chat-adapter.ts")
assert "AUTO_LOAD_VARIANT_SCAN_TIMEOUT_MS" in adapter
assert "async function listGgufVariantsBounded(" in adapter
assert "controller.abort()" in adapter
# No unbounded scan calls remain in the adapter: every call site routes
# through the bounded wrapper (the wrapper itself holds the one direct
# call, with the timeout signal attached).
assert adapter.count("await listGgufVariants(") == 1
chat_api = _read("features/chat/api/chat-api.ts")
assert "signal: options?.signal," in chat_api
assert "function expandSeenValues(" in adapter
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_route_preflights_and_gguf_rows_stay_format_true():
"""Round-19 gates. The /load route's sidecar tier probe and training
guard size local-only candidates against the resolved snapshot path (a
repo id would reach _remote_lora_base's raw HTTP request and hf
model_info, neither of which honors offline mode). GGUF cached rows
select a GGUF-bearing snapshot, so a mixed repo's safetensors revision
cannot become the GGUF row's load target."""
route = _read_backend("routes/inference.py")
assert "_tier_target = config.path if request.local_files_only else config.identifier" in route
guard_block = route.split("_guard_identifier = (", 1)[1]
guard_block = guard_block.split("await asyncio.to_thread(", 1)[0]
assert "config.path" in guard_block
assert "request.local_files_only and not config.is_gguf" in guard_block
assert "model_identifier = _guard_identifier," in route
inventory = _read_backend("hub/services/models/cache_inventory.py")
assert "def _newest_snapshot_where(" in inventory
assert "def _gguf_snapshot_path(" in inventory
assert "def _cached_gguf_repo_snapshot_path(" in inventory
gguf_scan = inventory.split("def _scan_cached_gguf(", 1)[1]
gguf_scan = gguf_scan.split("def ", 1)[0]
assert "_cached_gguf_repo_snapshot_path(repo_path)" in gguf_scan
assert "_cached_model_snapshot_path(repo_path)" not in gguf_scan
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
loads pass local_files_only into the llama.cpp path: companions resolve
cached-or-skipped, and a main-quant cache miss raises instead of
downloading."""
route = _read_backend("routes/inference.py")
gguf_source = route.split("if config.gguf_hf_repo:", 1)[1]
gguf_source = gguf_source.split("else:", 1)[0]
assert "local_files_only = request.local_files_only," in gguf_source
llama = _read_backend("core/inference/llama_cpp.py")
# The flag flows to the main quant and both companion helpers, and the
# crash-replay kwargs keep it so a reload stays local-only.
assert llama.count("local_files_only = local_files_only,") >= 3
assert '"local_files_only": local_files_only,' in llama
# Companions: cached-or-skipped, never fetched.
assert 'logger.info("Skipping %s fetch (local-only load)", label)' in llama
assert "if local_files_only or _hf_env_offline():" in llama
# Main quant: a cache miss fails closed.
download = llama.split("def _download_gguf", 1)[1]
download = download.split("def _download_companion_gguf", 1)[0]
assert "if local_files_only:" in download
assert "select it explicitly to download it." in download
inference = _read_backend("core/inference/inference.py")
# The vision processor fallback stays on the (possibly rewritten local)
# load path instead of refetching by repo id.
assert "config.base_model if config.is_lora else config.path" in inference
assert "config.base_model if config.is_lora else config.identifier" not in inference
def test_vulkan_inference_devices_are_the_pickable_set():
"""GGUF loads run through llama-server, so on a Vulkan build the picker must
offer the inference inventory (ggml ordinals, the space `--device Vulkan<i>`