Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
84 commits
Select commit Hold shift + click to select a range
033d607
studio: classify embedding models from the HF cache and honor offline…
hakanbaysal Jul 3, 2026
aabf0d0
studio: judge the active cached revision, harden the cache probe, sto…
hakanbaysal Jul 17, 2026
763ef72
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 17, 2026
fb0cfae
studio: treat a missing active-ref snapshot as a cache miss, don't ca…
hakanbaysal Jul 18, 2026
abf5874
Merge branch 'main' into fix/studio-offline-embedding-model-cache
hakanbaysal Jul 18, 2026
21bef98
studio: defer online embedding detection to the Hub, re-probe offline
hakanbaysal Jul 18, 2026
e9c7da6
studio: harden offline embedding detection against empty refs, offlin…
hakanbaysal Jul 18, 2026
bfc1cff
studio: reuse the exact-match-first case resolver and preserve the de…
hakanbaysal Jul 18, 2026
3468fa9
studio: don't let a stale cache marker mask a permanent Hub error
hakanbaysal Jul 18, 2026
202b23f
studio: honor TRANSFORMERS_OFFLINE in the embedding preflight, skip c…
hakanbaysal Jul 18, 2026
cb1f538
studio: never skip the security scan on TRANSFORMERS_OFFLINE alone
hakanbaysal Jul 18, 2026
df32b90
studio: short-circuit the security preflight under either offline flag
hakanbaysal Jul 18, 2026
38b78c8
studio: scope the offline scan bypass to callers that load local-only
hakanbaysal Jul 18, 2026
164b5d8
studio: capture offline state once, and probe the ST cache root
hakanbaysal Jul 18, 2026
f94f3ec
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 18, 2026
72eb4f4
studio: probe the cache the ST loader actually uses, and require it b…
hakanbaysal Jul 18, 2026
c7b8c85
Merge branch 'main' into fix/studio-offline-embedding-model-cache
hakanbaysal Jul 18, 2026
1a26812
studio: thread the captured offline state into the module probe, fix …
hakanbaysal Jul 18, 2026
a3fb2e9
studio: accept only torch-loadable weights in the offline ST probe; f…
shimmyshimmer Jul 19, 2026
afb323d
Merge branch 'main' into fix/studio-offline-embedding-model-cache
hakanbaysal Jul 19, 2026
ae2f5c0
studio: probe the exact repo dir and revision an offline load resolves
hakanbaysal Jul 19, 2026
9e6a355
studio: record refs/main in the ONNX-only probe test
hakanbaysal Jul 19, 2026
003c982
studio: recognize base-model weight files and gate the offline positi…
shimmyshimmer Jul 19, 2026
563dd31
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 19, 2026
62e57d1
studio: require a complete weight set offline and persist embedder ve…
shimmyshimmer Jul 19, 2026
98abd64
studio: require complete weights (with shard index) and resolve defau…
shimmyshimmer Jul 19, 2026
9b2895c
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 19, 2026
9753c43
studio: require a tokenizer, case-fold verdict ids, and serialize ver…
shimmyshimmer Jul 19, 2026
8f4d3bb
studio: tighten comments in the offline embedding-model classifier
shimmyshimmer Jul 19, 2026
c2614bb
studio: drop redundant comments in the offline embedding-model classi…
shimmyshimmer Jul 19, 2026
9e34679
Merge branch 'main' into fix/studio-offline-embedding-model-cache
hakanbaysal Jul 19, 2026
b83c15d
studio: pin embedder verdicts to a revision, canonicalize default ali…
hakanbaysal Jul 19, 2026
35113e6
studio: validate the real ST load root, and pin verdicts to the Hub r…
hakanbaysal Jul 19, 2026
27f60b1
Merge branch 'main' into fix/studio-offline-embedding-model-cache
hakanbaysal Jul 19, 2026
8407e0a
studio: fail closed for an offline security scan instead of failing open
shimmyshimmer Jul 20, 2026
4d49096
studio: only suppress an offline pickle when a loadable safetensors w…
shimmyshimmer Jul 20, 2026
ba98c68
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 20, 2026
fbf9977
studio: scope the offline pickle scan to load paths; reset __all__ op…
shimmyshimmer Jul 20, 2026
c96decf
studio: recase slashless ST aliases and accept a pinned embedder afte…
shimmyshimmer Jul 20, 2026
56201d0
studio: scan modules.json-declared module roots in the offline pickle…
shimmyshimmer Jul 20, 2026
947beb1
studio: classify cached non-Transformer SentenceTransformer models of…
shimmyshimmer Jul 20, 2026
f56b712
studio: scan PEFT adapter pickle weights in the offline security gate
shimmyshimmer Jul 20, 2026
4c04b87
studio: require weights for Dense/CNN/LSTM SentenceTransformer module…
shimmyshimmer Jul 20, 2026
ac0a89c
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 20, 2026
fbfbe07
studio: scan root-index subdir pickle shards offline; handle __all__.…
shimmyshimmer Jul 20, 2026
8e9a0e4
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 20, 2026
b85a771
studio: classify StaticEmbedding offline, require WordEmbeddings toke…
shimmyshimmer Jul 20, 2026
5847763
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 20, 2026
95af81b
Resolve indexed safetensors shards relative to their index
shimmyshimmer Jul 20, 2026
67ac0ef
Restrict offline weight-completeness check to declared load roots
shimmyshimmer Jul 20, 2026
2250697
Scan SentenceTransformer Router child module weights offline
shimmyshimmer Jul 20, 2026
ae15078
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 20, 2026
9fd2b13
Do not treat an unreferenced config subdir as an offline load root
shimmyshimmer Jul 20, 2026
be9a581
Classify a root Router (Asym) model as loadable offline
shimmyshimmer Jul 20, 2026
9c77fd7
Require every declared module before accepting an offline cache
shimmyshimmer Jul 20, 2026
5623ed1
Merge origin/main into fix/studio-offline-embedding-model-cache
shimmyshimmer Jul 20, 2026
cf8ece8
Reject self-referential Router children instead of recursing forever
shimmyshimmer Jul 20, 2026
e46c7ef
Treat a destructuring __all__ assignment as opaque
shimmyshimmer Jul 20, 2026
f84c7e1
Canonicalize declared module paths before scoping the offline pickle …
shimmyshimmer Jul 20, 2026
a1323be
Close offline embedding-classification completeness gaps
shimmyshimmer Jul 20, 2026
47d4cd3
Close case-folding and online-traversal holes in the offline pickle gate
shimmyshimmer Jul 20, 2026
ecbd325
Treat a conditional __all__ mutation as opaque in the import-hoist li…
shimmyshimmer Jul 20, 2026
3db5c0c
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 20, 2026
fe756ba
Scope Router child sub-modules as load roots in the online embedding …
shimmyshimmer Jul 20, 2026
cde4da9
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 20, 2026
02d843e
Allow a recorded-clean pickle embedder to load offline
shimmyshimmer Jul 21, 2026
831da56
Harden the embedding verdict cache against review findings
shimmyshimmer Jul 21, 2026
8c91458
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 21, 2026
527396a
Tighten offline embedding classification and the pickle gate
shimmyshimmer Jul 21, 2026
0704c6e
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 21, 2026
529876b
Scope Router-child pickles to their deepest load root and gate the ST…
shimmyshimmer Jul 21, 2026
6c58540
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 21, 2026
e55a1c6
Reject snapshot-escaping shard paths and credit Transformer submodule…
shimmyshimmer Jul 21, 2026
e667419
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 21, 2026
845ba4b
Scope Router children against the snapshot and mirror the ST alias re…
shimmyshimmer Jul 21, 2026
f08dc34
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 21, 2026
1c7170d
Tighten root shard credit, module-path escapes, and weight-set probe …
shimmyshimmer Jul 21, 2026
2571e1b
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 21, 2026
b76eda1
Restore scripts/verify_import_hoist.py to main
shimmyshimmer Jul 21, 2026
cadd262
Reuse a shared HF cache skeleton in the offline classification tests
shimmyshimmer Jul 21, 2026
fd7c2de
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 21, 2026
92cc0e7
Merge main and replace with the minimal offline-embedding fix for #6817
shimmyshimmer Jul 22, 2026
ef49462
Reclassify embedding models from the cache on every offline call
shimmyshimmer Jul 22, 2026
48309dc
Tighten comments on the offline embedding path
shimmyshimmer Jul 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 53 additions & 12 deletions studio/backend/core/rag/embeddings.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

from utils.hardware.hardware import DeviceType, get_device
from utils.transformers_dtype import dtype_kwargs
from utils.utils import hf_env_offline

from . import config

Expand Down Expand Up @@ -119,46 +120,86 @@ def _st_module_subdirs(name: str, token: str | None) -> tuple[str, ...]:
return ()


def _guard_model_security(name: str) -> None:
def _guard_model_security(name: str, local_only: bool = False) -> None:
"""Refuse to load a repo HF flagged as unsafe: a poisoned pickle deserializes inside
SentenceTransformer regardless of trust_remote_code. Defense in depth behind the
/settings gate (a name can also arrive via env/default); local paths and unreachable
scans fail open inside evaluate_file_security. Never bricks the embedder on a gate error.

``local_only`` (offline) inspects the local cache; subdir probes are skipped (they'd hit the
network and hang, and the offline gate walks the whole snapshot anyway).
"""
try:
from utils.security import evaluate_file_security, security_load_subdirs

token = _ambient_hf_token()
# Union the audio-model load roots with the ST module dirs so a flagged pickle
# directly under a Transformer module dir (0_Transformer/) blocks instead of
# passing as an unreferenced nested shard.
load_subdirs = tuple(
dict.fromkeys((*security_load_subdirs(name, token), *_st_module_subdirs(name, token)))
)
blocked = evaluate_file_security(name, hf_token = token, load_subdirs = load_subdirs).blocked
if local_only:
load_subdirs = ()
else:
# Union audio-model load roots with ST module dirs so a flagged pickle under a
# Transformer module dir blocks instead of passing as an unreferenced nested shard.
load_subdirs = tuple(
dict.fromkeys(
(*security_load_subdirs(name, token), *_st_module_subdirs(name, token))
)
)
blocked = evaluate_file_security(
name, hf_token = token, load_subdirs = load_subdirs, local_only_load = local_only
).blocked
except Exception:
return
Comment on lines 149 to 150

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Fail closed when the offline security gate errors

When offline inspection itself raises, this blanket handler returns and _get() immediately constructs SentenceTransformer from the cached snapshot, so an unverified pickle can still be deserialized. This is especially problematic for malformed/unreadable cache metadata that causes evaluate_file_security(..., local_only_load=True) to raise rather than return a blocking decision; offline mode has no Hub scan to compensate. Re-raise an UnsafeEmbeddingModelError (or otherwise block) when local_only is true.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The offline gate already fails closed on every reachable case: _evaluate_local_only returns a blocking decision when the cache cannot be resolved or read, rather than raising. The only way evaluate_file_security(local_only_load=True) raises is a valid-JSON-but-wrong-shape modules.json, and that same file makes SentenceTransformer's own _load_sbert_model raise while reading modules.json, before any module weight is deserialized, so there is no reachable path where the swallow lets an unscanned pickle load. I have left the guard unchanged; glad to add the offline fail-closed invariant as defense in depth if preferred, but it is not fixing a reachable bug.

if blocked:
reason = (
"has cached pickle weights that cannot be security-scanned offline and no "
"safetensors alternative"
if local_only
else "is flagged as unsafe by Hugging Face's security scan"
)
raise UnsafeEmbeddingModelError(
f"Embedding model {name!r} is flagged as unsafe by Hugging Face's security "
"scan; refusing to load. Set a different RAG embedding model."
f"Embedding model {name!r} {reason}; refusing to load. "
"Set a different RAG embedding model."
)


def _st_accepts_local_files_only(st_cls) -> bool:
"""Whether this SentenceTransformer version accepts local_files_only; passing it to an
older constructor raises, so gate on the signature."""
try:
import inspect
return "local_files_only" in inspect.signature(st_cls.__init__).parameters
except Exception:
return False


def _get(model_name: str | None = None):
"""Cached SentenceTransformer, (re)loading on a name change. Loaded in fp16
for a ~1.5x speedup at negligible accuracy loss."""
global _model, _name
name = model_name or config.effective_embedding_model()
# Capture offline state once so the gate and the load agree (no window where the gate is
# skipped as offline but the constructor then reaches the network).
local_only = hf_env_offline()
with _lock:
if _model is None or _name != name:
_install_torchao_stub_once()
from sentence_transformers import SentenceTransformer

device = _device()
logger.info("loading embedding model %s on %s", name, device)
_guard_model_security(name)
_model = SentenceTransformer(name, device = device, model_kwargs = dtype_kwargs("float16"))
_guard_model_security(name, local_only)
st_kwargs = dict(device = device, model_kwargs = dtype_kwargs("float16"))
load_target = name
if local_only:
from utils.utils import hf_cache_snapshot_dir
snapshot = hf_cache_snapshot_dir(name)
if snapshot is not None:
# Load from the local snapshot dir: a local path never touches the Hub, so
# this is offline-safe on ANY sentence-transformers version (even ones
# predating local_files_only).
load_target = str(snapshot)
elif _st_accepts_local_files_only(SentenceTransformer):
Comment on lines +197 to +200

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep old SentenceTransformer versions offline

When only TRANSFORMERS_OFFLINE=1 is set and the installed SentenceTransformer predates local_files_only, this branch leaves load_target as the remote repo ID and passes no offline flag. SentenceTransformer then delegates to huggingface_hub, which does not honor TRANSFORMERS_OFFLINE, so an uncached model performs the network request this change is intended to avoid and can hang on DNS retries. Enforce Hub offline mode around this fallback or fail locally before calling the older constructor.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The premise is not accurate for huggingface_hub 0.36.2: constants.HF_HUB_OFFLINE = _is_true(HF_HUB_OFFLINE or TRANSFORMERS_OFFLINE) is evaluated at import, so hub DOES honor TRANSFORMERS_OFFLINE. Verified: with TRANSFORMERS_OFFLINE=1 set at launch, hf_hub_download() on an uncached repo with NO local_files_only kwarg raises LocalEntryNotFoundError in 0.00s (no network, no hang) - the download forces offline from the constant. The studio sets the offline env at launch (nothing sets it at runtime for the RAG embedder; _hf_offline_if_dns_dead wraps only llama inference), so an old-ST uncached load fails fast rather than reaching the network.

st_kwargs["local_files_only"] = True
Comment on lines +200 to +201

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Prevent legacy loaders from reaching Hub offline

When only TRANSFORMERS_OFFLINE is set and hf_cache_snapshot_dir() cannot resolve a snapshot (for example, an uncached model), SentenceTransformers versions that predate local_files_only take neither branch and receive the remote repo id unchanged. TRANSFORMERS_OFFLINE is not honored by huggingface_hub, so those versions can still attempt network requests and hang instead of failing locally, despite this code classifying the session as offline. Set HF_HUB_OFFLINE for the load or raise a cache-only error when the constructor lacks local_files_only.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the P1 above: hub 0.36.2 folds TRANSFORMERS_OFFLINE into HF_HUB_OFFLINE at import and the underlying download forces offline from that constant, so an uncached load on an ST predating local_files_only raises LocalEntryNotFoundError immediately (0.00s) instead of reaching the network. Not reachable for the launch-set offline session the studio actually runs.

Comment on lines +200 to +201

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Force cache-only loading on legacy SentenceTransformers

When only TRANSFORMERS_OFFLINE=1 is set and the embedding model is not already resolved to a snapshot, legacy SentenceTransformers versions without an explicit local_files_only constructor parameter receive neither a local path nor a cache-only flag. Those versions proceed through their Hugging Face download path, which honors HF_HUB_OFFLINE rather than TRANSFORMERS_OFFLINE, so this path can still attempt DNS/network retries—the exact offline hang this change is intended to avoid. This affects installs using an older supported SentenceTransformers release with an uncached default or force-saved embedding model.

Useful? React with 👍 / 👎.

_model = SentenceTransformer(load_target, **st_kwargs)
_name = name
return _model

Expand Down
77 changes: 55 additions & 22 deletions studio/backend/routes/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,11 @@ def update_embedding_model(
log = logger,
) from exc
hf_token = (payload.hf_token or "").strip() or None
from utils.utils import hf_env_offline

# Offline, both the Hub malware scan and the is-embedding check are unreachable and degrade
# to the local cache below; capture the state once.
local_only_load = hf_env_offline()
# The env/default model needs no verification; saving it is a no-op override.
# A local GGUF on the llama-server backend is accepted as-is: it is exactly
# what the backend loads, and HF metadata cannot verify a local path.
Expand All @@ -439,26 +444,41 @@ def update_embedding_model(
# Fall back to the loader's own token so a gated/private repo is actually scanned
# (a token-less scan fails open for exactly the repo that would still load).
scan_token = hf_token or _ambient_hf_token()
# Include the ST module dirs (0_Transformer/) so a flagged pickle directly under
# one blocks instead of passing as an unreferenced nested shard.
load_subdirs = tuple(
dict.fromkeys(
(
*security_load_subdirs(model, scan_token),
*_st_module_subdirs(model, scan_token),
# Offline: subdir probes would hit the network and hang; the offline gate walks the
# whole cached snapshot, so no load-subdir hints are needed.
if local_only_load:
load_subdirs = ()
else:
# Include ST module dirs (0_Transformer/) so a flagged pickle directly under one
# blocks instead of passing as an unreferenced nested shard.
load_subdirs = tuple(
dict.fromkeys(
(
*security_load_subdirs(model, scan_token),
*_st_module_subdirs(model, scan_token),
)
)
)
)
if evaluate_file_security(model, hf_token = scan_token, load_subdirs = load_subdirs).blocked:
if evaluate_file_security(
model,
hf_token = scan_token,
load_subdirs = load_subdirs,
local_only_load = local_only_load,
).blocked:
# 403, not 409: the client routes every 409 into the forceable "save anyway"
# flow, but this block is a hard, non-forceable security refusal.
raise HTTPException(
status_code = 403,
if local_only_load:
detail = (
f"{model!r} has cached pickle weights that cannot be security-scanned "
"offline and no safetensors alternative, so it cannot be used as the "
"embedding model. Re-download it with safetensors weights while online."
)
else:
detail = (
f"{model!r} is flagged as unsafe by Hugging Face's security scan and "
"cannot be used as the embedding model."
),
)
)
raise HTTPException(status_code = 403, detail = detail)
if model != default_embedding_model() and not payload.force and not is_local_gguf:
from core.rag import config as rag_config

Expand All @@ -468,15 +488,28 @@ def update_embedding_model(
# which would wrongly 409 a valid online GGUF embedder.
gguf_named = _llama_backend_active() and rag_config._names_gguf(model)
if not gguf_named and not is_embedding_model(model, hf_token = hf_token):
raise HTTPException(
status_code = 409,
detail = (
f"Could not verify {model!r} as an embedding model on "
"Hugging Face (it may be the wrong model type, gated, or "
"you may be offline)."
),
)
gguf_error = _local_gguf_backend_error(model) or _hf_gguf_backend_error(model, hf_token)
# Offline, is_embedding_model can only confirm the ST layout (modules.json); a
# transformers-native embedder (e.g. gte-modernbert) is unverifiable without Hub
# metadata. If already cached and loadable, accept it rather than raising a 409 that
# online would not (ST can load any cached encoder). Uncached -> 409.
from utils.utils import hf_cache_snapshot_is_loadable

# Require a genuinely loadable cache (config + weights), not just a resolved refs/main,
# so a metadata-only partial cache still gets the forceable 409.
offline_cached = local_only_load and hf_cache_snapshot_is_loadable(model)
if not offline_cached:
Comment on lines +497 to +500

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject partial SentenceTransformer caches offline

When offline, a custom cached SentenceTransformer repo containing only modules.json (and possibly config metadata) makes is_embedding_model() return true, so this entire block is skipped and the setting is saved without force. The loadability check therefore only protects transformers-native models; _get() subsequently selects that partial snapshot and fails when SentenceTransformer needs the missing weights. Apply the same hf_cache_snapshot_is_loadable() requirement to the offline positive-ST path so incomplete cached downloads return the intended forceable 409.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Offline, hub is forced into local-files-only mode (the constant above), so a partial ST snapshot (modules.json, no weights) does not hang: _get() loads the local snapshot path and SentenceTransformer raises an immediate local error on the missing weights, no network. That is a clean failure on an incomplete cache (not well-formed input), not a reachable hang or wrong result, so it is below the bar for a code change here.

raise HTTPException(
status_code = 409,
detail = (
f"Could not verify {model!r} as an embedding model on "
"Hugging Face (it may be the wrong model type, gated, or "
"you may be offline)."
),
)
# The Hub GGUF probe (list_repo_files) can hang offline; skip it. Local check stays.
gguf_error = _local_gguf_backend_error(model)
if gguf_error is None and not local_only_load:
gguf_error = _hf_gguf_backend_error(model, hf_token)
Comment on lines +510 to +512

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate remote GGUF availability from cache offline

With the llama-server backend and offline mode enabled, this skips the only remote-GGUF validation and accepts an uncached GGUF-named model without force. The later LlamaServerBackend._resolve_model_path() still calls list_repo_files() before hf_hub_download(), so the first ingestion/retrieval either hangs under TRANSFORMERS_OFFLINE (which huggingface_hub does not honor) or fails under HF_HUB_OFFLINE. Check the selected GGUF candidate in the local cache before saving, or reject it with the existing forceable 409 when no cached GGUF is available.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the offline flag set (always at launch for the studio process; nothing sets it at runtime here), huggingface_hub honors it, so list_repo_files()/hf_hub_download() in _resolve_model_path() fail fast offline (LocalEntryNotFoundError, ~0ms) rather than hang. The residual is a clean error on an uncached GGUF, and the llama-server GGUF preflight is outside this PR's sentence-transformers embedding scope (a separate #6817 follow-up).

if gguf_error:
raise HTTPException(status_code = 409, detail = gguf_error)
set_rag_embedding_model(model)
Expand Down
50 changes: 50 additions & 0 deletions studio/backend/tests/test_embedding_model_security_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,56 @@ def test_hard_block_uses_non_forceable_status(client, monkeypatch):
assert unverified.status_code == 409


def test_offline_cached_non_st_model_is_accepted(client, monkeypatch):
# Offline, a cached transformers-native embedder (no modules.json) is unverifiable via HF
# metadata, but ST can load any cached encoder, so accept it (no 409).
c, saved = client
monkeypatch.setitem(sys.modules, "utils.security", _security_stub(blocked = False))
monkeypatch.setenv("HF_HUB_OFFLINE", "1")
import utils.models as _models
import utils.utils as _uu

monkeypatch.setattr(_models, "is_embedding_model", lambda *a, **k: False)
monkeypatch.setattr(_uu, "hf_cache_snapshot_is_loadable", lambda name: True)
r = c.put("/embedding-model", json = {"embedding_model": "acme/gte-modernbert"})
assert r.status_code == 200
assert saved.get("model") == "acme/gte-modernbert"


def test_offline_partial_or_uncached_model_still_409(client, monkeypatch):
# Offline but not loadable (uncached or metadata-only partial cache): keep the forceable
# 409, since the cache-only load would fail anyway.
c, _saved = client
monkeypatch.setitem(sys.modules, "utils.security", _security_stub(blocked = False))
monkeypatch.setenv("HF_HUB_OFFLINE", "1")
import utils.models as _models
import utils.utils as _uu

monkeypatch.setattr(_models, "is_embedding_model", lambda *a, **k: False)
monkeypatch.setattr(_uu, "hf_cache_snapshot_is_loadable", lambda name: False)
r = c.put("/embedding-model", json = {"embedding_model": "acme/uncached-embedder"})
assert r.status_code == 409


def test_offline_skips_remote_gguf_probe(client, monkeypatch):
# Offline + llama backend: the remote GGUF probe (list_repo_files) must be skipped so a
# dead-DNS session cannot hang.
c, _saved = client
monkeypatch.setenv("HF_HUB_OFFLINE", "1")
monkeypatch.setattr(settings, "_llama_backend_active", lambda: True)
monkeypatch.setattr(settings, "_local_gguf_backend_error", lambda model: None)

def _boom(*a, **k):
raise AssertionError("hit the network for the GGUF probe")

monkeypatch.setattr(settings, "_hf_gguf_backend_error", _boom)
import utils.models as _models

monkeypatch.setattr(_models, "is_embedding_model", lambda *a, **k: True)
r = c.put("/embedding-model", json = {"embedding_model": "acme/embedder"})
assert r.status_code == 200


def test_llama_backend_skips_the_st_pickle_scan(monkeypatch):
# On the llama-server backend the embedder loads GGUF (inert), not the ST repo's
# pickle, so a flagged ST repo with a clean GGUF companion must not be rejected here.
Expand Down
Loading
Loading