diff --git a/packages/sie_server/bundles/default.yaml b/packages/sie_server/bundles/default.yaml
index e495c0339..6a2b8ab5c 100644
--- a/packages/sie_server/bundles/default.yaml
+++ b/packages/sie_server/bundles/default.yaml
@@ -53,8 +53,8 @@ adapters:
- sie_server.adapters.paddleocr_vl
- sie_server.adapters.mineru_vl
deps:
- # Most flash adapters; sentence_transformer needs >=4.57
- transformers: '>=4.57,<5'
+ # Exact TF4 runtime reviewed with the load-time kernel/config security guard.
+ transformers: '==4.57.6'
# Pin torch — flash-attn's unpinned torch dep can pull in 2.10+ which breaks torchvision
torch: '>=2.9,<2.10'
# bge_m3, bge_m3_flash, nemo_colembed, nomic_flash, xlm_roberta_flash
diff --git a/packages/sie_server/pyproject.toml b/packages/sie_server/pyproject.toml
index b8d9c8d7d..a85642fa3 100644
--- a/packages/sie_server/pyproject.toml
+++ b/packages/sie_server/pyproject.toml
@@ -17,11 +17,12 @@ dependencies = [
# ML / Inference
# torch 2.9.x required - flash-attn prebuilt wheels are version-specific
"torch>=2.9,<2.10",
- # Bundle-controlled: each bundle overlays its exact transformers into
- # bundle-libs (bundles/default.yaml >=4.57,<5; transformers5.yaml / gemma via
- # sglang >=5). The base range is bounded <6 so the >=5 overlays resolve while
- # the base venv can't silently jump to a transformers 6 major.
- "transformers>=4.57,<6",
+ # Bundle-controlled: default/local and the root lock use exactly 4.57.6,
+ # whose loader receives the reviewed security backport. TF5 bundles use
+ # upstream-fixed >=5.3 releases. PEP 440 cannot express that disjoint set:
+ # exclude known unsupported releases here; runtime loading also refuses
+ # any future unreviewed 4.x version. Keep <6 for bundle overlay compatibility.
+ "transformers>=4.57.6,<6,!=5.0.*,!=5.1.*,!=5.2.*",
"tokenizers>=0.20,<1",
"sentence-transformers==5.7.0",
"safetensors>=0.4,<1",
@@ -130,9 +131,8 @@ gpu-metrics = ["nvidia-ml-py>=13.5,<14"]
top = ["textual>=3.0,<4"]
# Local embeddings + reranking on Apple Silicon / CPU (no Docker/mise). The torch /
# sentence-transformers / FlagEmbedding stack is already in the base deps; the base
-# leaves transformers at >=4.57,<6 so the transformers-5 bundle overlays resolve.
-# This extra tightens it to <5 — the version the embed/rerank adapters need — for a
-# bare install with no bundle overlay:
+# permits upstream-fixed transformers-5 bundle overlays. This extra pins the
+# exact reviewed TF4 loader for a bare install with no bundle overlay:
# uv tool install "sie-server[local]" && sie-server serve
#
# Generation is deliberately NOT a locked extra. mlx-lm requires transformers>=5; as
@@ -142,7 +142,7 @@ top = ["textual>=3.0,<4"]
# uvx --with "mlx-lm>=0.30.7" --from sie-server sie-server serve -b sglang -p 8081
# (Linux/CUDA generation uses the Docker image: SGLang pins timm==1.0.16, which
# conflicts with the embed/rerank stack's timm<1.0, so it can't share a pip env.)
-local = ["transformers>=4.57,<5"]
+local = ["transformers==4.57.6"]
[project.scripts]
sie-server = "sie_server.cli:app"
diff --git a/packages/sie_server/src/sie_server/adapters/_sentence_transformer_trust.py b/packages/sie_server/src/sie_server/adapters/_sentence_transformer_trust.py
index e5d12b423..aa98e14f3 100644
--- a/packages/sie_server/src/sie_server/adapters/_sentence_transformer_trust.py
+++ b/packages/sie_server/src/sie_server/adapters/_sentence_transformer_trust.py
@@ -15,6 +15,8 @@
import sentence_transformers
from sentence_transformers import CrossEncoder, SentenceTransformer, SparseEncoder
+from sie_server.adapters._transformers_compat import install_transformers_security_fixes
+
_SUPPORTED_VERSIONS = frozenset({"5.4.1", "5.6.0", "5.7.0"})
_ROOT_CLASSES = (SentenceTransformer, SparseEncoder, CrossEncoder)
_RESOLVER = "_load_module_class_from_ref"
@@ -139,6 +141,7 @@ def load_sentence_transformer[T: (SentenceTransformer, SparseEncoder, CrossEncod
releases. An unknown version fails before an untrusted model is loaded.
Explicit True preserves the original constructor path exactly.
"""
+ install_transformers_security_fixes()
if trust_remote_code is True:
return model_class(model_name_or_path, trust_remote_code=True, **kwargs)
if trust_remote_code is not False and trust_remote_code is not None:
diff --git a/packages/sie_server/src/sie_server/adapters/_transformers_compat.py b/packages/sie_server/src/sie_server/adapters/_transformers_compat.py
new file mode 100644
index 000000000..e1650cd81
--- /dev/null
+++ b/packages/sie_server/src/sie_server/adapters/_transformers_compat.py
@@ -0,0 +1,98 @@
+"""Load-time security backport for the supported Transformers 4 runtime.
+
+Transformers 5.3 restricts Hub attention kernels to ``kernels-community`` and
+ignores serialized private attention settings (GHSA-29pf-2h5f-8g72). The default
+SIE bundle still needs Transformers 4.57.6. Install this compatibility guard once,
+before loading models, to retain that runtime without retaining the vulnerable
+loader. It never wraps model inference or temporarily changes a trust setting.
+
+Upstream fix: huggingface/transformers@a7f8e7ff37d87d1a1a0c8cf607971c607741452f.
+"""
+
+from __future__ import annotations
+
+import functools
+import re
+import threading
+from typing import Any
+
+from packaging.version import Version
+
+_INSTALL_LOCK = threading.Lock()
+_PATCHED_VERSION = Version("4.57.6")
+_UPSTREAM_FIXED_VERSION = Version("5.3.0")
+# The native loader splits on ANY pipe, before parsing the repository/revision.
+# A pipe in a later component must not redirect an allowlisted-looking string.
+_KERNEL_PATTERN = re.compile(r"(?:paged\|)?kernels-community/[^/:|@\s]+(?:@[^/:|@\s]+)?(?::[^/:|@\s]+)?")
+_PRIVATE_IMPLEMENTATION_KEYS = frozenset(
+ {
+ "_attn_implementation_internal",
+ "_attn_implementation",
+ "_experts_implementation_internal",
+ "_experts_implementation",
+ }
+)
+_CONFIG_STRUCTURE_KEYS = frozenset({"__dict__", "__class__", "attribute_map", "sub_configs"})
+
+
+def _is_community_kernel(attn_implementation: str | None) -> bool:
+ """Recognize the upstream allowlisted namespace before any Hub access."""
+ return isinstance(attn_implementation, str) and _KERNEL_PATTERN.fullmatch(attn_implementation) is not None
+
+
+def install_transformers_security_fixes() -> bool:
+ """Install permanent loader guards; return whether this call installed them.
+
+ Call during process initialization, before model loading begins. Installation
+ is idempotent across threads. All subsequent loads use the same policy,
+ including loads with ``trust_remote_code=True``, just as in upstream 5.3.
+ Unsupported vulnerable versions fail closed instead of receiving an untested
+ private-API patch. Versions with the upstream fix require no patch.
+ """
+ # Importing this helper must not require Transformers in remote-only runtimes.
+ import transformers
+
+ version = Version(transformers.__version__)
+ if version >= _UPSTREAM_FIXED_VERSION:
+ return False
+ if version != _PATCHED_VERSION:
+ raise RuntimeError(
+ "SIE's Transformers security backport requires transformers==4.57.6; "
+ "use that supported runtime or transformers>=5.3.0."
+ )
+
+ from transformers import PretrainedConfig, modeling_utils
+ from transformers.integrations import hub_kernels
+
+ with _INSTALL_LOCK:
+ original_init = PretrainedConfig.__init__
+ if getattr(original_init, "_sie_kernel_security_backport", False):
+ if (
+ hub_kernels.is_kernel is not _is_community_kernel
+ or modeling_utils.is_kernel is not _is_community_kernel
+ ):
+ raise RuntimeError("The installed Transformers kernel security guard was replaced.")
+ return False
+
+ @functools.wraps(original_init)
+ def guarded_init(self: Any, *args: Any, **kwargs: Any) -> None:
+ # The property names are aliases for the private storage fields.
+ # Keeping either alias would let model-owned JSON restore the same
+ # vulnerable value after the native constructor chooses attention.
+ # Native attention propagation follows class-defined sub_configs.
+ # Model-owned maps could redirect it to __class__ and mutate the
+ # shared configuration class during an otherwise ordinary load.
+ forbidden_structure = _CONFIG_STRUCTURE_KEYS.intersection(kwargs)
+ if forbidden_structure:
+ raise ValueError("Model configuration cannot override Python object or config attribute structure.")
+ clean_kwargs = {key: value for key, value in kwargs.items() if key not in _PRIVATE_IMPLEMENTATION_KEYS}
+ original_init(self, *args, **clean_kwargs)
+
+ guarded_init.__dict__["_sie_kernel_security_backport"] = True
+ # Both functions resolve their globals in their original modules. The
+ # native kernel loader therefore also sees the restricted predicate.
+ # Apply this policy before exposing the constructor guard to callers.
+ hub_kernels.is_kernel = _is_community_kernel # ty: ignore[invalid-assignment]
+ modeling_utils.is_kernel = _is_community_kernel # ty: ignore[invalid-assignment]
+ PretrainedConfig.__init__ = guarded_init # ty: ignore[invalid-assignment]
+ return True
diff --git a/packages/sie_server/src/sie_server/adapters/bert_flash/__init__.py b/packages/sie_server/src/sie_server/adapters/bert_flash/__init__.py
index 7008d5e7a..4cb383b76 100644
--- a/packages/sie_server/src/sie_server/adapters/bert_flash/__init__.py
+++ b/packages/sie_server/src/sie_server/adapters/bert_flash/__init__.py
@@ -26,6 +26,7 @@
from sie_server.adapters._flash_base import FlashBaseAdapter
from sie_server.adapters._spec import AdapterSpec
+from sie_server.adapters._transformers_compat import install_transformers_security_fixes
from sie_server.adapters._types import ERR_NOT_LOADED, ComputePrecision, PoolingStrategy
from sie_server.adapters._utils import (
extract_texts,
@@ -154,6 +155,7 @@ def load(self, device: str) -> None:
if self._revision is not None:
shared_kwargs["revision"] = self._revision
+ install_transformers_security_fixes()
self._tokenizer = AutoTokenizer.from_pretrained(self._model_name_or_path, **shared_kwargs)
# Load model with eager attention - we'll run our own flash attention
diff --git a/packages/sie_server/src/sie_server/adapters/bert_flash_cross_encoder/__init__.py b/packages/sie_server/src/sie_server/adapters/bert_flash_cross_encoder/__init__.py
index 9faa8f1ec..bc0430231 100644
--- a/packages/sie_server/src/sie_server/adapters/bert_flash_cross_encoder/__init__.py
+++ b/packages/sie_server/src/sie_server/adapters/bert_flash_cross_encoder/__init__.py
@@ -8,6 +8,7 @@
from sie_server.adapters._flash_base import FlashBaseAdapter
from sie_server.adapters._spec import AdapterSpec
+from sie_server.adapters._transformers_compat import install_transformers_security_fixes
from sie_server.adapters._types import ERR_NOT_LOADED, ERR_REQUIRES_TEXT, ComputePrecision
from sie_server.adapters._utils import extract_text
from sie_server.core.inference_output import ScoreOutput
@@ -111,6 +112,7 @@ def load(self, device: str) -> None:
shared_kwargs["revision"] = self._revision
# Load tokenizer
+ install_transformers_security_fixes()
self._tokenizer = AutoTokenizer.from_pretrained(
self._model_name_or_path,
**shared_kwargs,
diff --git a/packages/sie_server/src/sie_server/adapters/bge_m3/__init__.py b/packages/sie_server/src/sie_server/adapters/bge_m3/__init__.py
index 9e06a35e3..d54c6154b 100644
--- a/packages/sie_server/src/sie_server/adapters/bge_m3/__init__.py
+++ b/packages/sie_server/src/sie_server/adapters/bge_m3/__init__.py
@@ -25,6 +25,7 @@
from sie_server.adapters._base_adapter import BaseAdapter
from sie_server.adapters._spec import AdapterSpec
+from sie_server.adapters._transformers_compat import install_transformers_security_fixes
from sie_server.adapters._types import ERR_NOT_LOADED, ERR_REQUIRES_TEXT, ComputePrecision
from sie_server.adapters.bge_m3_score_mixin import BGEM3ScoreMixin
from sie_server.core.inference_output import EncodeOutput, SparseVector
@@ -122,6 +123,7 @@ def load(self, device: str) -> None:
shared_kwargs["revision"] = self._revision
# Load tokenizer (fast Rust tokenizer)
+ install_transformers_security_fixes()
self._tokenizer = AutoTokenizer.from_pretrained(self._model_name_or_path, **shared_kwargs)
# Load base model with Flash Attention 2 if available
diff --git a/packages/sie_server/src/sie_server/adapters/bge_m3_flag/__init__.py b/packages/sie_server/src/sie_server/adapters/bge_m3_flag/__init__.py
index b0a5879ee..462ae5172 100644
--- a/packages/sie_server/src/sie_server/adapters/bge_m3_flag/__init__.py
+++ b/packages/sie_server/src/sie_server/adapters/bge_m3_flag/__init__.py
@@ -22,6 +22,7 @@
from sie_server.adapters._base_adapter import BaseAdapter
from sie_server.adapters._spec import AdapterSpec
+from sie_server.adapters._transformers_compat import install_transformers_security_fixes
from sie_server.adapters._types import ERR_NOT_LOADED, ERR_REQUIRES_TEXT, ComputePrecision
from sie_server.adapters.bge_m3_score_mixin import BGEM3ScoreMixin
from sie_server.core.inference_output import EncodeOutput, SparseVector
@@ -122,6 +123,7 @@ def load(self, device: str) -> None:
# kwarg — its constructor forwards nothing to the internal
# ``from_pretrained`` load — so ``self._revision`` cannot be honored on this
# path. Use the ``bge_m3`` / ``bge_m3_flash`` adapters to pin a revision.
+ install_transformers_security_fixes()
self._model = BGEM3FlagModel(
self._model_name_or_path,
use_fp16=use_fp16,
diff --git a/packages/sie_server/src/sie_server/adapters/bge_m3_flash/__init__.py b/packages/sie_server/src/sie_server/adapters/bge_m3_flash/__init__.py
index b3f603c2b..60cadf22f 100644
--- a/packages/sie_server/src/sie_server/adapters/bge_m3_flash/__init__.py
+++ b/packages/sie_server/src/sie_server/adapters/bge_m3_flash/__init__.py
@@ -14,6 +14,7 @@
from sie_server.adapters._flash_base import FlashBaseAdapter
from sie_server.adapters._flash_pack import build_position_ids
from sie_server.adapters._spec import AdapterSpec
+from sie_server.adapters._transformers_compat import install_transformers_security_fixes
from sie_server.adapters._types import ERR_NOT_LOADED, ComputePrecision
from sie_server.adapters._utils import validate_output_types
from sie_server.adapters.bge_m3_score_mixin import BGEM3ScoreMixin
@@ -114,6 +115,7 @@ def load(self, device: str) -> None:
if self._revision is not None:
shared_kwargs["revision"] = self._revision
+ install_transformers_security_fixes()
self._tokenizer = AutoTokenizer.from_pretrained(self._model_name_or_path, **shared_kwargs)
# Load model with eager attention - we'll run our own flash attention
diff --git a/packages/sie_server/src/sie_server/adapters/clip/__init__.py b/packages/sie_server/src/sie_server/adapters/clip/__init__.py
index 2270c22ba..135228fc8 100644
--- a/packages/sie_server/src/sie_server/adapters/clip/__init__.py
+++ b/packages/sie_server/src/sie_server/adapters/clip/__init__.py
@@ -31,6 +31,7 @@
from sie_server.adapters._base_adapter import BaseAdapter
from sie_server.adapters._spec import AdapterSpec
+from sie_server.adapters._transformers_compat import install_transformers_security_fixes
from sie_server.adapters._types import ERR_NOT_LOADED, ComputePrecision
from sie_server.core.inference_output import EncodeOutput
from sie_server.core.preprocessor import ImagePreprocessor
@@ -161,6 +162,7 @@ def load(self, device: str) -> None:
shared_kwargs["revision"] = self._revision
# Load processor (handles both text tokenization and image preprocessing)
+ install_transformers_security_fixes()
self._processor = CLIPProcessor.from_pretrained(
self._model_name_or_path,
**shared_kwargs,
diff --git a/packages/sie_server/src/sie_server/adapters/colbert/__init__.py b/packages/sie_server/src/sie_server/adapters/colbert/__init__.py
index 575ad4ed5..09a3d1f06 100644
--- a/packages/sie_server/src/sie_server/adapters/colbert/__init__.py
+++ b/packages/sie_server/src/sie_server/adapters/colbert/__init__.py
@@ -39,6 +39,7 @@
from sie_server.adapters._multivector import maxsim_scores_batched
from sie_server.adapters._pylate_dense import apply_dense_chain, load_pylate_dense_chain
from sie_server.adapters._spec import AdapterSpec
+from sie_server.adapters._transformers_compat import install_transformers_security_fixes
from sie_server.adapters._types import ERR_NOT_LOADED, ERR_REQUIRES_TEXT, ComputePrecision
from sie_server.adapters._utils import grouped_score_pairs
from sie_server.core.inference_output import EncodeOutput, ScoreOutput
@@ -221,6 +222,7 @@ def load(self, device: str) -> None:
if self._code_revision is not None:
shared_kwargs["code_revision"] = self._code_revision
+ install_transformers_security_fixes()
self._tokenizer = AutoTokenizer.from_pretrained(
self._model_name_or_path,
**shared_kwargs,
diff --git a/packages/sie_server/src/sie_server/adapters/colbert_modernbert_flash/adapter.py b/packages/sie_server/src/sie_server/adapters/colbert_modernbert_flash/adapter.py
index 38fdcb101..be19e2065 100644
--- a/packages/sie_server/src/sie_server/adapters/colbert_modernbert_flash/adapter.py
+++ b/packages/sie_server/src/sie_server/adapters/colbert_modernbert_flash/adapter.py
@@ -28,6 +28,7 @@
from sie_server.adapters._multivector import maxsim_scores_batched
from sie_server.adapters._pylate_dense import apply_dense_chain, load_pylate_dense_chain
from sie_server.adapters._spec import AdapterSpec
+from sie_server.adapters._transformers_compat import install_transformers_security_fixes
from sie_server.adapters._types import ERR_NOT_LOADED, ERR_REQUIRES_TEXT, ComputePrecision
from sie_server.adapters._utils import (
grouped_score_pairs,
@@ -182,6 +183,7 @@ def load(self, device: str) -> None:
dtype,
)
+ install_transformers_security_fixes()
self._tokenizer = self._load_tokenizer()
if self._doc_punctuation_skiplist:
diff --git a/packages/sie_server/src/sie_server/adapters/colbert_rotary_flash/__init__.py b/packages/sie_server/src/sie_server/adapters/colbert_rotary_flash/__init__.py
index 9fc9d6d37..3a7734c72 100644
--- a/packages/sie_server/src/sie_server/adapters/colbert_rotary_flash/__init__.py
+++ b/packages/sie_server/src/sie_server/adapters/colbert_rotary_flash/__init__.py
@@ -11,6 +11,7 @@
from sie_server.adapters._flash_base import FlashBaseAdapter
from sie_server.adapters._multivector import maxsim_scores_batched
from sie_server.adapters._spec import AdapterSpec
+from sie_server.adapters._transformers_compat import install_transformers_security_fixes
from sie_server.adapters._types import ERR_NOT_LOADED, ERR_REQUIRES_TEXT, ComputePrecision
from sie_server.adapters._utils import (
apply_rotary_pos_emb,
@@ -152,6 +153,7 @@ def load(self, device: str) -> None:
if self._code_revision is not None:
shared_kwargs["code_revision"] = self._code_revision
+ install_transformers_security_fixes()
self._tokenizer = AutoTokenizer.from_pretrained(
self._model_name_or_path,
trust_remote_code=True,
diff --git a/packages/sie_server/src/sie_server/adapters/colpali/__init__.py b/packages/sie_server/src/sie_server/adapters/colpali/__init__.py
index 73e32f72f..1f18a65da 100644
--- a/packages/sie_server/src/sie_server/adapters/colpali/__init__.py
+++ b/packages/sie_server/src/sie_server/adapters/colpali/__init__.py
@@ -29,6 +29,7 @@
from sie_server.adapters._base_adapter import BaseAdapter
from sie_server.adapters._multivector import maxsim_scores
from sie_server.adapters._spec import AdapterSpec
+from sie_server.adapters._transformers_compat import install_transformers_security_fixes
from sie_server.adapters._types import ComputePrecision
from sie_server.core.inference_output import EncodeOutput
from sie_server.core.postprocessor import MuveraConfig, MuveraPostprocessor
@@ -153,6 +154,7 @@ def load(self, device: str) -> None:
self._processor_load_kwargs = dict(shared_kwargs)
# Load processor
+ install_transformers_security_fixes()
self._processor = ColPaliProcessor.from_pretrained(
self._model_name_or_path,
**shared_kwargs,
diff --git a/packages/sie_server/src/sie_server/adapters/colqwen2/__init__.py b/packages/sie_server/src/sie_server/adapters/colqwen2/__init__.py
index 68291a562..12eb1f732 100644
--- a/packages/sie_server/src/sie_server/adapters/colqwen2/__init__.py
+++ b/packages/sie_server/src/sie_server/adapters/colqwen2/__init__.py
@@ -33,6 +33,7 @@
from sie_server.adapters._base_adapter import BaseAdapter
from sie_server.adapters._multivector import maxsim_scores
from sie_server.adapters._spec import AdapterSpec
+from sie_server.adapters._transformers_compat import install_transformers_security_fixes
from sie_server.adapters._types import ComputePrecision
from sie_server.adapters._vision_patch_embed import rebind_vision_patch_embed
from sie_server.core.inference_output import EncodeOutput
@@ -222,6 +223,7 @@ def load(self, device: str) -> None:
}
if self._revision is not None:
proc_kwargs["revision"] = self._revision
+ install_transformers_security_fixes()
self._processor = Qwen2VLProcessor.from_pretrained(self._model_name_or_path, **proc_kwargs)
tokenizer = self._processor.tokenizer # ty: ignore[unresolved-attribute]
tokenizer.padding_side = "left"
diff --git a/packages/sie_server/src/sie_server/adapters/colqwen3/__init__.py b/packages/sie_server/src/sie_server/adapters/colqwen3/__init__.py
index 7bfdb2508..1910c412d 100644
--- a/packages/sie_server/src/sie_server/adapters/colqwen3/__init__.py
+++ b/packages/sie_server/src/sie_server/adapters/colqwen3/__init__.py
@@ -12,6 +12,7 @@
from sie_server.adapters._base_adapter import BaseAdapter
from sie_server.adapters._multivector import maxsim_scores
from sie_server.adapters._spec import AdapterSpec
+from sie_server.adapters._transformers_compat import install_transformers_security_fixes
from sie_server.adapters._types import ComputePrecision
from sie_server.adapters._vision_patch_embed import rebind_vision_patch_embed
from sie_server.core.inference_output import EncodeOutput
@@ -125,6 +126,7 @@ def load(self, device: str) -> None:
attn_impl,
)
+ install_transformers_security_fixes()
self._processor = AutoProcessor.from_pretrained(
self._model_name_or_path,
trust_remote_code=self._trust_remote_code,
diff --git a/packages/sie_server/src/sie_server/adapters/colsmol/__init__.py b/packages/sie_server/src/sie_server/adapters/colsmol/__init__.py
index 3ca7e6e21..6dfe55908 100644
--- a/packages/sie_server/src/sie_server/adapters/colsmol/__init__.py
+++ b/packages/sie_server/src/sie_server/adapters/colsmol/__init__.py
@@ -41,6 +41,7 @@
from sie_server.adapters._base_adapter import BaseAdapter
from sie_server.adapters._multivector import maxsim_scores
from sie_server.adapters._spec import AdapterSpec
+from sie_server.adapters._transformers_compat import install_transformers_security_fixes
from sie_server.adapters._types import ComputePrecision
from sie_server.core.inference_output import EncodeOutput
from sie_server.core.postprocessor import MuveraConfig, MuveraPostprocessor
@@ -186,6 +187,7 @@ def load(self, device: str) -> None:
# The colSmol-* repos are LoRA adapters; discover the full base checkpoint
# (which carries the ColIdefics3 projection ``linear``) from adapter_config.
+ install_transformers_security_fixes()
peft_cfg = PeftConfig.from_pretrained(self._model_name_or_path, revision=self._revision)
base_id = peft_cfg.base_model_name_or_path
if not base_id:
diff --git a/packages/sie_server/src/sie_server/adapters/docling/adapter.py b/packages/sie_server/src/sie_server/adapters/docling/adapter.py
index ea7fe2dbd..d3b4fcf4a 100644
--- a/packages/sie_server/src/sie_server/adapters/docling/adapter.py
+++ b/packages/sie_server/src/sie_server/adapters/docling/adapter.py
@@ -8,6 +8,7 @@
from sie_server.adapters._base_adapter import BaseAdapter
from sie_server.adapters._spec import AdapterSpec
+from sie_server.adapters._transformers_compat import install_transformers_security_fixes
from sie_server.core.inference_output import ExtractItemError, ExtractOutput
from sie_server.types.inputs import InvalidMediaError, is_image_input, media_bytes
from sie_server.types.responses import ErrorCode
@@ -164,6 +165,7 @@ def load(self, device: str) -> None:
# Models cache globally, so subsequent per-task converters are cheap.
# This is the born-digital parse path that BOTH profiles depend on, so
# a staged artifact root keeps it fail-closed.
+ install_transformers_security_fixes()
try:
warm_converter = self._get_converter(ocr_enabled=False)
_payload, _pages, warm_error = self._convert_bytes(
diff --git a/packages/sie_server/src/sie_server/adapters/donut/__init__.py b/packages/sie_server/src/sie_server/adapters/donut/__init__.py
index 6ef6ce89b..87913aa1e 100644
--- a/packages/sie_server/src/sie_server/adapters/donut/__init__.py
+++ b/packages/sie_server/src/sie_server/adapters/donut/__init__.py
@@ -30,6 +30,7 @@
from sie_server.adapters._base_adapter import BaseAdapter
from sie_server.adapters._spec import AdapterSpec
+from sie_server.adapters._transformers_compat import install_transformers_security_fixes
from sie_server.adapters._types import ERR_NOT_LOADED, ComputePrecision
from sie_server.core.inference_output import EncodeOutput, ExtractOutput
from sie_server.types.inputs import decode_image
@@ -139,6 +140,7 @@ def load(self, device: str) -> None:
)
# Load processor
+ install_transformers_security_fixes()
self._processor = DonutProcessor.from_pretrained(
self._model_name_or_path,
**shared_kwargs,
diff --git a/packages/sie_server/src/sie_server/adapters/florence2/__init__.py b/packages/sie_server/src/sie_server/adapters/florence2/__init__.py
index e62070112..b66de5345 100644
--- a/packages/sie_server/src/sie_server/adapters/florence2/__init__.py
+++ b/packages/sie_server/src/sie_server/adapters/florence2/__init__.py
@@ -26,6 +26,7 @@
from sie_server.adapters._base_adapter import BaseAdapter
from sie_server.adapters._spec import AdapterSpec
+from sie_server.adapters._transformers_compat import install_transformers_security_fixes
from sie_server.adapters._types import ERR_NOT_LOADED, ComputePrecision
from sie_server.core.inference_output import EncodeOutput, ExtractOutput
from sie_server.core.preprocessor.vision import resolve_florence2_prompt
@@ -146,6 +147,7 @@ def load(self, device: str) -> None:
# Load the checkpoint's own (remote) processor. AutoProcessor alone is not
# safe on transformers >= 4.49 — see _load_processor for the incompatibility.
+ install_transformers_security_fixes()
self._processor = self._load_processor(shared_kwargs)
# Load model with trust_remote_code to use model's custom code
diff --git a/packages/sie_server/src/sie_server/adapters/gliclass/__init__.py b/packages/sie_server/src/sie_server/adapters/gliclass/__init__.py
index 1f0466359..3871fa514 100644
--- a/packages/sie_server/src/sie_server/adapters/gliclass/__init__.py
+++ b/packages/sie_server/src/sie_server/adapters/gliclass/__init__.py
@@ -74,6 +74,7 @@
from sie_server.adapters._base_adapter import BaseAdapter
from sie_server.adapters._spec import AdapterSpec
+from sie_server.adapters._transformers_compat import install_transformers_security_fixes
from sie_server.adapters._types import ERR_NOT_LOADED, ComputePrecision
from sie_server.adapters.base import released_bytes
from sie_server.adapters.errors import InputTooLongError
@@ -596,6 +597,7 @@ def load(self, device: str) -> None:
shared_kwargs: dict[str, Any] = {}
if self._revision is not None:
shared_kwargs["revision"] = self._revision
+ install_transformers_security_fixes()
config = GLiClassModelConfig.from_pretrained(self._model_name_or_path, **shared_kwargs)
if _restore_modernbert_rope_fields(config.encoder_config):
model = GLiClassModel.from_pretrained(self._model_name_or_path, config=config, **shared_kwargs)
diff --git a/packages/sie_server/src/sie_server/adapters/gliformer/adapter.py b/packages/sie_server/src/sie_server/adapters/gliformer/adapter.py
index 39bfa4032..8da528f27 100644
--- a/packages/sie_server/src/sie_server/adapters/gliformer/adapter.py
+++ b/packages/sie_server/src/sie_server/adapters/gliformer/adapter.py
@@ -29,6 +29,7 @@
from sie_server.adapters._base_adapter import BaseAdapter
from sie_server.adapters._spec import AdapterSpec
+from sie_server.adapters._transformers_compat import install_transformers_security_fixes
from sie_server.adapters._types import ERR_REQUIRES_TEXT, ComputePrecision
from sie_server.adapters.base import released_bytes
from sie_server.adapters.gliformer.cuda_graphs import GRAPH_MODES, EncoderGraphRunner, unsupported_reason
@@ -724,6 +725,7 @@ def load(self, device: str) -> None:
ignore_patterns=_SNAPSHOT_IGNORE_PATTERNS,
)
+ install_transformers_security_fixes()
with warnings.catch_warnings():
# The checkpoints request the optional flashdeberta kernels. Without
# them GLiFormer uses its eager attention, which is what SIE serves.
diff --git a/packages/sie_server/src/sie_server/adapters/gliner/__init__.py b/packages/sie_server/src/sie_server/adapters/gliner/__init__.py
index 0c4b4488c..08f9f69b6 100644
--- a/packages/sie_server/src/sie_server/adapters/gliner/__init__.py
+++ b/packages/sie_server/src/sie_server/adapters/gliner/__init__.py
@@ -46,6 +46,7 @@
gliner_prompt_counter,
)
from sie_server.adapters._spec import AdapterSpec
+from sie_server.adapters._transformers_compat import install_transformers_security_fixes
from sie_server.adapters._types import ERR_REQUIRES_TEXT, ComputePrecision
from sie_server.adapters._word_window import (
MAX_DOCUMENT_WINDOWS,
@@ -202,6 +203,7 @@ def load(self, device: str) -> None:
load_kwargs: dict[str, Any] = {}
if self._revision is not None:
load_kwargs["revision"] = self._revision
+ install_transformers_security_fixes()
self._model = GLiNER.from_pretrained(
self._model_name_or_path,
**load_kwargs,
diff --git a/packages/sie_server/src/sie_server/adapters/gliner2/adapter.py b/packages/sie_server/src/sie_server/adapters/gliner2/adapter.py
index d38b9d684..309447be7 100644
--- a/packages/sie_server/src/sie_server/adapters/gliner2/adapter.py
+++ b/packages/sie_server/src/sie_server/adapters/gliner2/adapter.py
@@ -15,6 +15,7 @@
from sie_server.adapters._base_adapter import BaseAdapter
from sie_server.adapters._prompt_limit import DEFAULT_MAX_SCHEMA_PROMPT_TOKENS, PromptLimit, check_label_chars
from sie_server.adapters._spec import AdapterSpec
+from sie_server.adapters._transformers_compat import install_transformers_security_fixes
from sie_server.adapters._types import ERR_REQUIRES_TEXT, ComputePrecision
from sie_server.adapters._word_window import (
MAX_WORD_CHARS,
@@ -196,6 +197,7 @@ def load(self, device: str) -> None:
model_path = snapshot_download(repo_id=model_path, revision=self._revision)
use_quantize = device != "cpu" and self._compute_precision == "float16"
+ install_transformers_security_fixes()
model = GLiNER2.from_pretrained(
model_path,
map_location=device,
diff --git a/packages/sie_server/src/sie_server/adapters/gliner2/decide.py b/packages/sie_server/src/sie_server/adapters/gliner2/decide.py
index aa2b442bf..a4170eddf 100644
--- a/packages/sie_server/src/sie_server/adapters/gliner2/decide.py
+++ b/packages/sie_server/src/sie_server/adapters/gliner2/decide.py
@@ -71,6 +71,7 @@
from sie_server.adapters._base_adapter import BaseAdapter
from sie_server.adapters._spec import AdapterSpec
+from sie_server.adapters._transformers_compat import install_transformers_security_fixes
from sie_server.adapters._types import ERR_NOT_LOADED, ComputePrecision
from sie_server.adapters.errors import InputTooLongError
from sie_server.adapters.gliner2.decide_graphs import (
@@ -473,6 +474,7 @@ def load(self, device: str) -> None:
checkpoint = self._checkpoint_dir()
encoder_config = _read_json(checkpoint / "encoder_config" / "config.json")
+ install_transformers_security_fixes()
with loadable_checkpoint(checkpoint) as path:
model = AutoExtractor.from_pretrained(str(path), map_location=device)
if not hasattr(model, "classifier") or not hasattr(model, "encoder"):
diff --git a/packages/sie_server/src/sie_server/adapters/gliner2/entities.py b/packages/sie_server/src/sie_server/adapters/gliner2/entities.py
index f7c6a50a0..4cc030416 100644
--- a/packages/sie_server/src/sie_server/adapters/gliner2/entities.py
+++ b/packages/sie_server/src/sie_server/adapters/gliner2/entities.py
@@ -20,6 +20,7 @@
from sie_server.adapters._base_adapter import BaseAdapter
from sie_server.adapters._prompt_limit import MAX_PROMPT_CHARS_PER_TOKEN, check_label_chars
from sie_server.adapters._spec import AdapterSpec
+from sie_server.adapters._transformers_compat import install_transformers_security_fixes
from sie_server.adapters._types import ComputePrecision
from sie_server.adapters._word_window import plan_forwards
from sie_server.adapters.gliner2.decisions import MARKERS
@@ -119,6 +120,7 @@ def load(self, device: str) -> None:
path = self._model_name_or_path
if not Path(path).is_dir():
path = snapshot_download(repo_id=path, revision=self._revision, allow_patterns=list(_CHECKPOINT_FILES))
+ install_transformers_security_fixes()
model = AutoExtractor.from_pretrained(path, map_location=device, quantize=False)
architecture = getattr(model, "architecture", None)
if architecture not in {"span", "boundary"}:
diff --git a/packages/sie_server/src/sie_server/adapters/gliner_bi/__init__.py b/packages/sie_server/src/sie_server/adapters/gliner_bi/__init__.py
index a64a9d260..22ea1079d 100644
--- a/packages/sie_server/src/sie_server/adapters/gliner_bi/__init__.py
+++ b/packages/sie_server/src/sie_server/adapters/gliner_bi/__init__.py
@@ -11,6 +11,7 @@
from sie_server.adapters._base_adapter import BaseAdapter
from sie_server.adapters._prompt_limit import check_label_chars
from sie_server.adapters._spec import AdapterSpec
+from sie_server.adapters._transformers_compat import install_transformers_security_fixes
from sie_server.adapters._types import ERR_REQUIRES_TEXT, ComputePrecision
from sie_server.adapters._word_window import (
MAX_DOCUMENT_WINDOWS,
@@ -169,6 +170,7 @@ def load(self, device: str) -> None:
if self._revision is not None:
load_kwargs["revision"] = self._revision
+ install_transformers_security_fixes()
self._model = GLiNER.from_pretrained(
self._model_name_or_path,
**load_kwargs,
diff --git a/packages/sie_server/src/sie_server/adapters/glirel/__init__.py b/packages/sie_server/src/sie_server/adapters/glirel/__init__.py
index 3d520e353..4d48ac601 100644
--- a/packages/sie_server/src/sie_server/adapters/glirel/__init__.py
+++ b/packages/sie_server/src/sie_server/adapters/glirel/__init__.py
@@ -24,6 +24,7 @@
from sie_server.adapters._base_adapter import BaseAdapter
from sie_server.adapters._prompt_limit import DEFAULT_MAX_PROMPT_TOKENS, PromptLimit, check_label_chars
from sie_server.adapters._spec import AdapterSpec
+from sie_server.adapters._transformers_compat import install_transformers_security_fixes
from sie_server.adapters._types import ERR_REQUIRES_TEXT, ComputePrecision
from sie_server.adapters._word_window import SubwordCounter, WindowedSplitter, split_word_counter, subword_budget
from sie_server.core.inference_output import ExtractOutput
@@ -127,6 +128,7 @@ def load(self, device: str) -> None:
load_kwargs: dict[str, Any] = {}
if self._revision is not None:
load_kwargs["revision"] = self._revision
+ install_transformers_security_fixes()
self._model = GLiREL.from_pretrained(self._model_name_or_path, **load_kwargs)
# Move to device
diff --git a/packages/sie_server/src/sie_server/adapters/glm_ocr/__init__.py b/packages/sie_server/src/sie_server/adapters/glm_ocr/__init__.py
index 8e9fc2435..5cb50c4bb 100644
--- a/packages/sie_server/src/sie_server/adapters/glm_ocr/__init__.py
+++ b/packages/sie_server/src/sie_server/adapters/glm_ocr/__init__.py
@@ -7,6 +7,7 @@
import torch
+from sie_server.adapters._transformers_compat import install_transformers_security_fixes
from sie_server.adapters._vision_patch_embed import rebind_vision_patch_embed
from sie_server.adapters.base import ModelAdapter, ModelCapabilities, ModelDims
from sie_server.core.inference_output import EncodeOutput, ExtractOutput
@@ -115,6 +116,7 @@ def load(self, device: str) -> None:
self._attn_implementation,
)
+ install_transformers_security_fixes()
self._processor = AutoProcessor.from_pretrained(self._model_name_or_path, **shared_kwargs)
self._model = AutoModelForImageTextToText.from_pretrained(
diff --git a/packages/sie_server/src/sie_server/adapters/grounding_dino/adapter.py b/packages/sie_server/src/sie_server/adapters/grounding_dino/adapter.py
index 52d6625e2..9d4c6474b 100644
--- a/packages/sie_server/src/sie_server/adapters/grounding_dino/adapter.py
+++ b/packages/sie_server/src/sie_server/adapters/grounding_dino/adapter.py
@@ -36,6 +36,7 @@
from sie_server.adapters._base_adapter import BaseAdapter
from sie_server.adapters._spec import AdapterSpec
+from sie_server.adapters._transformers_compat import install_transformers_security_fixes
from sie_server.adapters._types import ERR_NOT_LOADED, ComputePrecision
from sie_server.core.inference_output import EncodeOutput, ExtractOutput
from sie_server.core.preprocessor import DetectionPreprocessor
@@ -156,6 +157,7 @@ def load(self, device: str) -> None:
dtype,
)
+ install_transformers_security_fixes()
self._processor = AutoProcessor.from_pretrained(
self._model_name_or_path,
**shared_kwargs,
diff --git a/packages/sie_server/src/sie_server/adapters/gte_sparse_flash.py b/packages/sie_server/src/sie_server/adapters/gte_sparse_flash.py
index 780bd0987..467a25896 100644
--- a/packages/sie_server/src/sie_server/adapters/gte_sparse_flash.py
+++ b/packages/sie_server/src/sie_server/adapters/gte_sparse_flash.py
@@ -11,6 +11,7 @@
from sie_server.adapters._flash_base import FlashBaseAdapter
from sie_server.adapters._sparse_rows import sparse_rows
from sie_server.adapters._spec import AdapterSpec
+from sie_server.adapters._transformers_compat import install_transformers_security_fixes
from sie_server.adapters._types import ERR_NOT_LOADED, ERR_REQUIRES_TEXT, ComputePrecision
from sie_server.adapters._utils import (
apply_rotary_pos_emb,
@@ -150,6 +151,7 @@ def load(self, device: str) -> None:
if self._code_revision is not None:
shared_kwargs["code_revision"] = self._code_revision
+ install_transformers_security_fixes()
self._tokenizer = AutoTokenizer.from_pretrained(
self._model_name_or_path,
**shared_kwargs,
diff --git a/packages/sie_server/src/sie_server/adapters/jina_flash_cross_encoder/__init__.py b/packages/sie_server/src/sie_server/adapters/jina_flash_cross_encoder/__init__.py
index 12c93e698..a4ffe7e8e 100644
--- a/packages/sie_server/src/sie_server/adapters/jina_flash_cross_encoder/__init__.py
+++ b/packages/sie_server/src/sie_server/adapters/jina_flash_cross_encoder/__init__.py
@@ -7,6 +7,7 @@
from sie_server.adapters._flash_base import FlashBaseAdapter
from sie_server.adapters._spec import AdapterSpec
+from sie_server.adapters._transformers_compat import install_transformers_security_fixes
from sie_server.adapters._types import ERR_NOT_LOADED, ERR_REQUIRES_TEXT, ComputePrecision
from sie_server.adapters._utils import extract_text
from sie_server.core.inference_output import ScoreOutput
@@ -100,6 +101,7 @@ def load(self, device: str) -> None:
shared_kwargs["revision"] = self._revision
# Load tokenizer
+ install_transformers_security_fixes()
self._tokenizer = AutoTokenizer.from_pretrained(
self._model_name_or_path,
**shared_kwargs,
diff --git a/packages/sie_server/src/sie_server/adapters/laya/adapter.py b/packages/sie_server/src/sie_server/adapters/laya/adapter.py
index c240aa3a1..5c8f25a8a 100644
--- a/packages/sie_server/src/sie_server/adapters/laya/adapter.py
+++ b/packages/sie_server/src/sie_server/adapters/laya/adapter.py
@@ -85,6 +85,7 @@
unsupported_reason,
)
from sie_server.adapters._spec import AdapterSpec
+from sie_server.adapters._transformers_compat import install_transformers_security_fixes
from sie_server.adapters._types import ERR_NOT_LOADED, ComputePrecision
from sie_server.adapters.laya.model import LayaDecisionHead, LayaModel, apply_rope_parameters, split_checkpoint
from sie_server.adapters.laya.questions import (
@@ -309,6 +310,7 @@ def load(self, device: str) -> None:
trained_max_len = int(cfg.get("max_len", 512))
self._max_len = min(trained_max_len, self._max_seq_length) if self._max_seq_length else trained_max_len
self._head_max_len = int(cfg.get("head_max_len", 192))
+ install_transformers_security_fixes()
self._calibration = Calibration.from_config(cfg)
tokenizer = self._load_tokenizer(checkpoint_dir / "tokenizer")
diff --git a/packages/sie_server/src/sie_server/adapters/lighton_ocr/adapter.py b/packages/sie_server/src/sie_server/adapters/lighton_ocr/adapter.py
index b1fb95a13..3231c1095 100644
--- a/packages/sie_server/src/sie_server/adapters/lighton_ocr/adapter.py
+++ b/packages/sie_server/src/sie_server/adapters/lighton_ocr/adapter.py
@@ -9,6 +9,7 @@
import torch
import torch.nn.functional as F
+from sie_server.adapters._transformers_compat import install_transformers_security_fixes
from sie_server.adapters._vision_patch_embed import rebind_vision_patch_embed
from sie_server.adapters.base import ModelAdapter, ModelCapabilities, ModelDims
from sie_server.adapters.lighton_ocr._markdown import lightonocr3_markdown
@@ -131,6 +132,7 @@ def load(self, device: str) -> None:
self._attn_implementation,
)
+ install_transformers_security_fixes()
self._processor = LightOnOcrProcessor.from_pretrained(
self._model_name_or_path,
**shared_kwargs,
@@ -577,6 +579,7 @@ def load(self, device: str) -> None:
shared_kwargs: dict[str, Any] = {"trust_remote_code": False}
if self._revision is not None:
shared_kwargs["revision"] = self._revision
+ install_transformers_security_fixes()
self._processor = AutoProcessor.from_pretrained(self._model_name_or_path, **shared_kwargs)
self._model = Qwen3_5ForConditionalGeneration.from_pretrained(
self._model_name_or_path,
diff --git a/packages/sie_server/src/sie_server/adapters/mineru_vl/__init__.py b/packages/sie_server/src/sie_server/adapters/mineru_vl/__init__.py
index e95f121c9..a42d941b5 100644
--- a/packages/sie_server/src/sie_server/adapters/mineru_vl/__init__.py
+++ b/packages/sie_server/src/sie_server/adapters/mineru_vl/__init__.py
@@ -8,6 +8,7 @@
from sie_server.adapters._base_adapter import BaseAdapter
from sie_server.adapters._spec import AdapterSpec
+from sie_server.adapters._transformers_compat import install_transformers_security_fixes
from sie_server.adapters._types import ERR_NOT_LOADED, ComputePrecision
from sie_server.core.inference_output import EncodeOutput, ExtractOutput
from sie_server.types.inputs import decode_image
@@ -202,6 +203,7 @@ def load(self, device: str) -> None:
self._attn_implementation,
)
+ install_transformers_security_fixes()
self._processor = AutoProcessor.from_pretrained(
self._model_name_or_path,
use_fast=True,
diff --git a/packages/sie_server/src/sie_server/adapters/modernbert_flash/__init__.py b/packages/sie_server/src/sie_server/adapters/modernbert_flash/__init__.py
index c311d6366..38869fe01 100644
--- a/packages/sie_server/src/sie_server/adapters/modernbert_flash/__init__.py
+++ b/packages/sie_server/src/sie_server/adapters/modernbert_flash/__init__.py
@@ -21,6 +21,7 @@
parse_graph_mode,
)
from sie_server.adapters._spec import AdapterSpec
+from sie_server.adapters._transformers_compat import install_transformers_security_fixes
from sie_server.adapters._types import ERR_NOT_LOADED, ComputePrecision, PoolingStrategy
from sie_server.adapters._utils import (
extract_texts,
@@ -154,6 +155,7 @@ def load(self, device: str) -> None:
if self._revision is not None:
shared_kwargs["revision"] = self._revision
+ install_transformers_security_fixes()
self._tokenizer = AutoTokenizer.from_pretrained(
self._model_name_or_path,
**shared_kwargs,
diff --git a/packages/sie_server/src/sie_server/adapters/modernbert_flash_cross_encoder/__init__.py b/packages/sie_server/src/sie_server/adapters/modernbert_flash_cross_encoder/__init__.py
index be8b54753..8fb2cfa7b 100644
--- a/packages/sie_server/src/sie_server/adapters/modernbert_flash_cross_encoder/__init__.py
+++ b/packages/sie_server/src/sie_server/adapters/modernbert_flash_cross_encoder/__init__.py
@@ -16,6 +16,7 @@
)
from sie_server.adapters._packed_rope import packed_rope_available, rotate_packed_qkv_
from sie_server.adapters._spec import AdapterSpec
+from sie_server.adapters._transformers_compat import install_transformers_security_fixes
from sie_server.adapters._types import ERR_NOT_LOADED, ERR_REQUIRES_TEXT, ComputePrecision
from sie_server.adapters._utils import extract_text
from sie_server.core.inference_output import ScoreOutput
@@ -120,6 +121,7 @@ def load(self, device: str) -> None:
shared_kwargs["revision"] = self._revision
# Load tokenizer
+ install_transformers_security_fixes()
self._tokenizer = AutoTokenizer.from_pretrained(
self._model_name_or_path,
**shared_kwargs,
diff --git a/packages/sie_server/src/sie_server/adapters/nemo_colembed/__init__.py b/packages/sie_server/src/sie_server/adapters/nemo_colembed/__init__.py
index c9e1f4a40..3833aaee3 100644
--- a/packages/sie_server/src/sie_server/adapters/nemo_colembed/__init__.py
+++ b/packages/sie_server/src/sie_server/adapters/nemo_colembed/__init__.py
@@ -35,6 +35,7 @@
from sie_server.adapters._base_adapter import BaseAdapter
from sie_server.adapters._multivector import maxsim_scores
from sie_server.adapters._spec import AdapterSpec
+from sie_server.adapters._transformers_compat import install_transformers_security_fixes
from sie_server.adapters._types import ComputePrecision
from sie_server.adapters._vision_patch_embed import rebind_vision_patch_embed
from sie_server.core.inference_output import EncodeOutput
@@ -167,6 +168,7 @@ def load(self, device: str) -> None:
if self._revision is not None:
automodel_kwargs["revision"] = self._revision
loaded_via_v1_path = False
+ install_transformers_security_fixes()
try:
self._model = AutoModel.from_pretrained(
self._model_name_or_path,
diff --git a/packages/sie_server/src/sie_server/adapters/nli_classification/__init__.py b/packages/sie_server/src/sie_server/adapters/nli_classification/__init__.py
index d0f50f498..e8674ea6c 100644
--- a/packages/sie_server/src/sie_server/adapters/nli_classification/__init__.py
+++ b/packages/sie_server/src/sie_server/adapters/nli_classification/__init__.py
@@ -13,6 +13,7 @@
from sie_server.adapters._base_adapter import BaseAdapter
from sie_server.adapters._spec import AdapterSpec
+from sie_server.adapters._transformers_compat import install_transformers_security_fixes
from sie_server.adapters._types import ERR_NOT_LOADED, ComputePrecision
from sie_server.core.inference_output import ExtractOutput
from sie_server.types.responses import Classification
@@ -113,6 +114,7 @@ def load(self, device: str) -> None:
}
if self._revision is not None:
pipeline_kwargs["revision"] = self._revision
+ install_transformers_security_fixes()
self._pipeline = pipeline("zero-shot-classification", **pipeline_kwargs)
def _extract_text(self, item: Item) -> str:
diff --git a/packages/sie_server/src/sie_server/adapters/nli_classification_flash/__init__.py b/packages/sie_server/src/sie_server/adapters/nli_classification_flash/__init__.py
index 302253a14..b85b227ff 100644
--- a/packages/sie_server/src/sie_server/adapters/nli_classification_flash/__init__.py
+++ b/packages/sie_server/src/sie_server/adapters/nli_classification_flash/__init__.py
@@ -8,6 +8,7 @@
from sie_server.adapters._flash_base import FlashBaseAdapter
from sie_server.adapters._spec import AdapterSpec
+from sie_server.adapters._transformers_compat import install_transformers_security_fixes
from sie_server.adapters._types import ERR_NOT_LOADED, ERR_REQUIRES_TEXT, ComputePrecision
from sie_server.core.extract_cost import MAX_EXTRACT_LABELS
from sie_server.core.inference_output import ExtractOutput
@@ -113,6 +114,7 @@ def load(self, device: str) -> None:
shared_kwargs["revision"] = self._revision
# Load tokenizer
+ install_transformers_security_fixes()
self._tokenizer = AutoTokenizer.from_pretrained(self._model_name_or_path, **shared_kwargs)
# Load model (use 'dtype' not deprecated 'torch_dtype')
diff --git a/packages/sie_server/src/sie_server/adapters/owlv2/adapter.py b/packages/sie_server/src/sie_server/adapters/owlv2/adapter.py
index 8390a6916..5c7cc2a44 100644
--- a/packages/sie_server/src/sie_server/adapters/owlv2/adapter.py
+++ b/packages/sie_server/src/sie_server/adapters/owlv2/adapter.py
@@ -36,6 +36,7 @@
from sie_server.adapters._base_adapter import BaseAdapter
from sie_server.adapters._spec import AdapterSpec
+from sie_server.adapters._transformers_compat import install_transformers_security_fixes
from sie_server.adapters._types import ERR_NOT_LOADED, ComputePrecision
from sie_server.core.inference_output import EncodeOutput, ExtractOutput
from sie_server.core.preprocessor import DetectionPreprocessor
@@ -136,6 +137,7 @@ def load(self, device: str) -> None:
# 0.24 s on a 640 x 480 photo and 15.6 s on an 8192 x 1728 one on an
# L4-class host, against a 0.1 s forward pass, and produced the same
# 960 x 960 input to within resampling noise.
+ install_transformers_security_fixes()
self._processor = Owlv2Processor.from_pretrained(
self._model_name_or_path,
use_fast=True,
diff --git a/packages/sie_server/src/sie_server/adapters/paddleocr_vl/__init__.py b/packages/sie_server/src/sie_server/adapters/paddleocr_vl/__init__.py
index 023cd00e6..cb10452d9 100644
--- a/packages/sie_server/src/sie_server/adapters/paddleocr_vl/__init__.py
+++ b/packages/sie_server/src/sie_server/adapters/paddleocr_vl/__init__.py
@@ -9,6 +9,7 @@
from sie_server.adapters._base_adapter import BaseAdapter
from sie_server.adapters._spec import AdapterSpec
+from sie_server.adapters._transformers_compat import install_transformers_security_fixes
from sie_server.adapters._types import ERR_NOT_LOADED, ComputePrecision
from sie_server.adapters._vision_patch_embed import rebind_vision_patch_embed
from sie_server.core.inference_output import EncodeOutput, ExtractOutput
@@ -152,6 +153,7 @@ def load(self, device: str) -> None:
self._attn_implementation,
)
+ install_transformers_security_fixes()
self._processor = AutoProcessor.from_pretrained(
model_dir,
trust_remote_code=self._trust_remote_code,
diff --git a/packages/sie_server/src/sie_server/adapters/parakeet/adapter.py b/packages/sie_server/src/sie_server/adapters/parakeet/adapter.py
index 33ff3796a..b8b65c66c 100644
--- a/packages/sie_server/src/sie_server/adapters/parakeet/adapter.py
+++ b/packages/sie_server/src/sie_server/adapters/parakeet/adapter.py
@@ -47,6 +47,7 @@
from sie_server.adapters._base_adapter import BaseAdapter
from sie_server.adapters._spec import AdapterSpec
+from sie_server.adapters._transformers_compat import install_transformers_security_fixes
from sie_server.adapters._types import ComputePrecision
from sie_server.adapters.parakeet.decoding import TdtHypothesis, greedy_tdt_decode
from sie_server.core.inference_output import EncodeOutput, ExtractOutput
@@ -191,6 +192,7 @@ def load(self, device: str) -> None:
device,
self._dtype,
)
+ install_transformers_security_fixes()
processor = AutoProcessor.from_pretrained(self._model_name_or_path, **revision_kwargs)
model = AutoModelForTDT.from_pretrained(self._model_name_or_path, dtype=self._dtype, **revision_kwargs)
model.to(device)
diff --git a/packages/sie_server/src/sie_server/adapters/pplx_decider/adapter.py b/packages/sie_server/src/sie_server/adapters/pplx_decider/adapter.py
index f57fd7328..bab4fc409 100644
--- a/packages/sie_server/src/sie_server/adapters/pplx_decider/adapter.py
+++ b/packages/sie_server/src/sie_server/adapters/pplx_decider/adapter.py
@@ -17,6 +17,7 @@
from sie_server.adapters._base_adapter import BaseAdapter
from sie_server.adapters._spec import AdapterSpec
+from sie_server.adapters._transformers_compat import install_transformers_security_fixes
from sie_server.adapters._types import ERR_NOT_LOADED, ComputePrecision
from sie_server.adapters.errors import InputTooLongError
from sie_server.adapters.pplx_decider.model import DecisionModel, checkpoint_config, load_model, validate_tokenizer
@@ -119,6 +120,7 @@ def load(self, device: str) -> None:
)
)
config = checkpoint_config(directory)
+ install_transformers_security_fixes()
processor = AutoProcessor.from_pretrained(str(directory), local_files_only=True, trust_remote_code=False)
processor.tokenizer.padding_side = "left"
validate_tokenizer(processor.tokenizer, config)
diff --git a/packages/sie_server/src/sie_server/adapters/pytorch_embedding/__init__.py b/packages/sie_server/src/sie_server/adapters/pytorch_embedding/__init__.py
index 557539131..6fcc8d7f8 100644
--- a/packages/sie_server/src/sie_server/adapters/pytorch_embedding/__init__.py
+++ b/packages/sie_server/src/sie_server/adapters/pytorch_embedding/__init__.py
@@ -48,6 +48,7 @@
from sie_server.adapters._base_adapter import BaseAdapter
from sie_server.adapters._spec import AdapterSpec
+from sie_server.adapters._transformers_compat import install_transformers_security_fixes
from sie_server.adapters._types import ERR_NOT_LOADED, ERR_REQUIRES_TEXT, ComputePrecision
from sie_server.adapters.peft_lora_mixin import PEFTLoRAMixin
from sie_server.core.inference_output import EncodeOutput
@@ -201,6 +202,7 @@ def load(self, device: str) -> None:
if self._revision is not None:
shared_kwargs["revision"] = self._revision
+ install_transformers_security_fixes()
self._tokenizer = AutoTokenizer.from_pretrained(
self._model_name_or_path,
padding_side=padding_side,
diff --git a/packages/sie_server/src/sie_server/adapters/qwen2_flash/__init__.py b/packages/sie_server/src/sie_server/adapters/qwen2_flash/__init__.py
index c4c7bbf35..b66e3b255 100644
--- a/packages/sie_server/src/sie_server/adapters/qwen2_flash/__init__.py
+++ b/packages/sie_server/src/sie_server/adapters/qwen2_flash/__init__.py
@@ -13,6 +13,7 @@
from sie_server.adapters._flash_base import FlashBaseAdapter
from sie_server.adapters._flash_pack import build_position_ids, mean_pool_packed
from sie_server.adapters._spec import AdapterSpec
+from sie_server.adapters._transformers_compat import install_transformers_security_fixes
from sie_server.adapters._types import ERR_NOT_LOADED, ComputePrecision, PoolingStrategy
from sie_server.adapters._utils import (
apply_rotary_pos_emb,
@@ -152,6 +153,7 @@ def load(self, device: str) -> None:
if self._revision is not None:
shared_kwargs["revision"] = self._revision
+ install_transformers_security_fixes()
self._tokenizer = AutoTokenizer.from_pretrained(self._model_name_or_path, **shared_kwargs)
# Load config first to disable optional xformers features
diff --git a/packages/sie_server/src/sie_server/adapters/qwen2_flash_cross_encoder/adapter.py b/packages/sie_server/src/sie_server/adapters/qwen2_flash_cross_encoder/adapter.py
index d217fc800..ca6889005 100644
--- a/packages/sie_server/src/sie_server/adapters/qwen2_flash_cross_encoder/adapter.py
+++ b/packages/sie_server/src/sie_server/adapters/qwen2_flash_cross_encoder/adapter.py
@@ -11,6 +11,7 @@
from sie_server.adapters._flash_pack import build_position_ids
from sie_server.adapters._modernbert_flash_graphs import PackedForward, VarlenGraphRunner, unsupported_reason
from sie_server.adapters._spec import AdapterSpec
+from sie_server.adapters._transformers_compat import install_transformers_security_fixes
from sie_server.adapters._types import ERR_NOT_LOADED, ERR_REQUIRES_TEXT, ComputePrecision
from sie_server.adapters._utils import apply_rotary_pos_emb
from sie_server.adapters.qwen2_flash import _fused_layers
@@ -311,6 +312,7 @@ def load(self, device: str) -> None:
shared_kwargs["revision"] = self._revision
# Load tokenizer
+ install_transformers_security_fixes()
self._tokenizer = AutoTokenizer.from_pretrained(
self._model_name_or_path,
**shared_kwargs,
diff --git a/packages/sie_server/src/sie_server/adapters/qwen3_vl_embedding/__init__.py b/packages/sie_server/src/sie_server/adapters/qwen3_vl_embedding/__init__.py
index 352680bfa..7349013fb 100644
--- a/packages/sie_server/src/sie_server/adapters/qwen3_vl_embedding/__init__.py
+++ b/packages/sie_server/src/sie_server/adapters/qwen3_vl_embedding/__init__.py
@@ -11,6 +11,7 @@
from sie_server.adapters._base_adapter import BaseAdapter
from sie_server.adapters._spec import AdapterSpec
+from sie_server.adapters._transformers_compat import install_transformers_security_fixes
from sie_server.adapters._types import ERR_NOT_LOADED, ComputePrecision
from sie_server.adapters._vision_patch_embed import rebind_vision_patch_embed
from sie_server.core.inference_output import EncodeOutput
@@ -167,6 +168,7 @@ def load(self, device: str) -> None:
}
if self._revision is not None:
proc_kwargs["revision"] = self._revision
+ install_transformers_security_fixes()
self._processor = AutoProcessor.from_pretrained(
self._model_name_or_path,
**proc_kwargs,
diff --git a/packages/sie_server/src/sie_server/adapters/qwen3_vl_reranker/adapter.py b/packages/sie_server/src/sie_server/adapters/qwen3_vl_reranker/adapter.py
index cbecf1272..ebd4f28eb 100644
--- a/packages/sie_server/src/sie_server/adapters/qwen3_vl_reranker/adapter.py
+++ b/packages/sie_server/src/sie_server/adapters/qwen3_vl_reranker/adapter.py
@@ -10,6 +10,7 @@
from sie_server.adapters._base_adapter import BaseAdapter
from sie_server.adapters._spec import AdapterSpec
+from sie_server.adapters._transformers_compat import install_transformers_security_fixes
from sie_server.adapters._types import ComputePrecision
from sie_server.adapters._vision_patch_embed import rebind_vision_patch_embed
from sie_server.core.inference_output import ScoreOutput
@@ -151,6 +152,7 @@ def load(self, device: str) -> None:
# file reference is None in some transformers versions). Try
# AutoProcessor first; if that fails, load processor from the base
# Qwen3-VL-2B-Instruct model and override its tokenizer.
+ install_transformers_security_fixes()
try:
self._processor = AutoProcessor.from_pretrained(
self._model_name_or_path,
diff --git a/packages/sie_server/src/sie_server/adapters/rope_flash/__init__.py b/packages/sie_server/src/sie_server/adapters/rope_flash/__init__.py
index ecc7a95e8..0494048bc 100644
--- a/packages/sie_server/src/sie_server/adapters/rope_flash/__init__.py
+++ b/packages/sie_server/src/sie_server/adapters/rope_flash/__init__.py
@@ -10,6 +10,7 @@
from sie_server.adapters._flash_base import FlashBaseAdapter
from sie_server.adapters._flash_pack import mean_pool_packed
from sie_server.adapters._spec import AdapterSpec
+from sie_server.adapters._transformers_compat import install_transformers_security_fixes
from sie_server.adapters._types import ERR_NOT_LOADED, ComputePrecision, PoolingStrategy
from sie_server.adapters._utils import (
apply_rotary_pos_emb,
@@ -123,6 +124,7 @@ def load(self, device: str) -> None:
if self._revision is not None:
shared_kwargs["revision"] = self._revision
+ install_transformers_security_fixes()
self._tokenizer = AutoTokenizer.from_pretrained(self._model_name_or_path, **shared_kwargs)
# Load config first to disable optional xformers features
diff --git a/packages/sie_server/src/sie_server/adapters/sam3/adapter.py b/packages/sie_server/src/sie_server/adapters/sam3/adapter.py
index 618d03d26..892947767 100644
--- a/packages/sie_server/src/sie_server/adapters/sam3/adapter.py
+++ b/packages/sie_server/src/sie_server/adapters/sam3/adapter.py
@@ -43,6 +43,7 @@
from sie_server.adapters._base_adapter import BaseAdapter
from sie_server.adapters._spec import AdapterSpec
+from sie_server.adapters._transformers_compat import install_transformers_security_fixes
from sie_server.adapters._types import ERR_NOT_LOADED, ComputePrecision
from sie_server.core.inference_output import EncodeOutput, ExtractOutput
from sie_server.core.preprocessor import DetectionPreprocessor
@@ -232,6 +233,7 @@ def load(self, device: str) -> None:
logger.info("Loading SAM 3 model %s on device=%s with dtype=%s", self._model_name_or_path, device, dtype)
+ install_transformers_security_fixes()
self._processor = processor_class.from_pretrained(self._model_name_or_path, **shared_kwargs)
self._model = model_class.from_pretrained(self._model_name_or_path, dtype=dtype, **shared_kwargs)
self._model.to(device)
diff --git a/packages/sie_server/src/sie_server/adapters/sglang/generation.py b/packages/sie_server/src/sie_server/adapters/sglang/generation.py
index 92b09deed..8a79aa645 100644
--- a/packages/sie_server/src/sie_server/adapters/sglang/generation.py
+++ b/packages/sie_server/src/sie_server/adapters/sglang/generation.py
@@ -53,6 +53,7 @@
resolve_reasoning_format,
)
from sie_server.adapters._spec import AdapterSpec
+from sie_server.adapters._transformers_compat import install_transformers_security_fixes
from sie_server.adapters._types import ERR_NOT_LOADED, ComputePrecision
from sie_server.adapters.sglang import _server
from sie_server.adapters.sglang._compat.sie_thinking_budget import PARAM as THINKING_BUDGET_PARAM
@@ -221,6 +222,7 @@ def _reasoning_control_token_ids(
"""
from transformers import AutoTokenizer
+ install_transformers_security_fixes()
tokenizer = AutoTokenizer.from_pretrained(
model_name_or_path, revision=revision, trust_remote_code=trust_remote_code
)
diff --git a/packages/sie_server/src/sie_server/adapters/sglang_vision_extract/adapter.py b/packages/sie_server/src/sie_server/adapters/sglang_vision_extract/adapter.py
index e58534b47..40c87b025 100644
--- a/packages/sie_server/src/sie_server/adapters/sglang_vision_extract/adapter.py
+++ b/packages/sie_server/src/sie_server/adapters/sglang_vision_extract/adapter.py
@@ -19,6 +19,7 @@
from sie_server.adapters._generation_base import GenerationResult, collect_generation
from sie_server.adapters._spec import AdapterSpec
+from sie_server.adapters._transformers_compat import install_transformers_security_fixes
from sie_server.adapters._types import ComputePrecision
from sie_server.adapters.base import ModelCapabilities, ModelDims
from sie_server.adapters.lighton_ocr._markdown import lightonocr3_markdown
@@ -175,6 +176,7 @@ def load(self, device: str) -> None:
processor_kwargs: dict[str, Any] = {"trust_remote_code": self._trust_remote_code}
if self._processor_use_fast is not None:
processor_kwargs["use_fast"] = self._processor_use_fast
+ install_transformers_security_fixes()
self._processor = AutoProcessor.from_pretrained(processor_dir, **processor_kwargs)
try:
diff --git a/packages/sie_server/src/sie_server/adapters/siglip/adapter.py b/packages/sie_server/src/sie_server/adapters/siglip/adapter.py
index fea4249c3..b1400b366 100644
--- a/packages/sie_server/src/sie_server/adapters/siglip/adapter.py
+++ b/packages/sie_server/src/sie_server/adapters/siglip/adapter.py
@@ -46,6 +46,7 @@
import tempfile
import threading
from concurrent.futures import ThreadPoolExecutor
+from importlib.util import find_spec
from pathlib import Path
from typing import TYPE_CHECKING, Any, ClassVar, Literal
@@ -55,6 +56,7 @@
from sie_server.adapters._base_adapter import BaseAdapter
from sie_server.adapters._spec import AdapterSpec
+from sie_server.adapters._transformers_compat import install_transformers_security_fixes
from sie_server.adapters._types import ComputePrecision
from sie_server.core.inference_output import EncodeOutput
from sie_server.core.preprocessor.image import ImagePreprocessor, OpenCLIPImagePreprocessor
@@ -273,6 +275,7 @@ def _load_transformers(self, device: str, dtype: torch.dtype) -> None:
if self._revision is not None:
shared_kwargs["revision"] = self._revision
+ install_transformers_security_fixes()
self._processor = SiglipProcessor.from_pretrained(
self._model_name_or_path,
use_fast=False,
@@ -332,6 +335,14 @@ def _load_open_clip(self, device: str, dtype: torch.dtype) -> None:
import open_clip
from huggingface_hub import snapshot_download
+ # The factory can reread mutable configs and registries. Guard any
+ # available Transformers runtime before construction, without using a
+ # separate config preview to decide whether its HF text tower is safe.
+ # Native-only OpenCLIP remains usable when Transformers is absent;
+ # an installed unsupported version deliberately fails closed.
+ if find_spec("transformers") is not None:
+ install_transformers_security_fixes()
+
assert self._open_clip_model_id is not None # guarded in __init__
if not self._open_clip_model_id.startswith("hf-hub:"):
model, _train_preproc, val_preproc = open_clip.create_model_and_transforms(self._open_clip_model_id)
diff --git a/packages/sie_server/src/sie_server/adapters/splade_flash/adapter.py b/packages/sie_server/src/sie_server/adapters/splade_flash/adapter.py
index 0e56b5c8f..ba225b972 100644
--- a/packages/sie_server/src/sie_server/adapters/splade_flash/adapter.py
+++ b/packages/sie_server/src/sie_server/adapters/splade_flash/adapter.py
@@ -15,6 +15,7 @@
from sie_server.adapters._flash_pack import build_position_ids
from sie_server.adapters._sparse_rows import sparse_rows
from sie_server.adapters._spec import AdapterSpec
+from sie_server.adapters._transformers_compat import install_transformers_security_fixes
from sie_server.adapters._types import ERR_NOT_LOADED, ComputePrecision
from sie_server.adapters._utils import extract_texts, resolve_query_instruction, validate_output_types
from sie_server.adapters.base import ModelAdapter
@@ -128,6 +129,7 @@ def load(self, device: str) -> None:
if self._revision is not None:
shared_kwargs["revision"] = self._revision
+ install_transformers_security_fixes()
self._tokenizer = AutoTokenizer.from_pretrained(
self._model_name_or_path,
**shared_kwargs,
diff --git a/packages/sie_server/src/sie_server/adapters/stablebridge_pruner/__init__.py b/packages/sie_server/src/sie_server/adapters/stablebridge_pruner/__init__.py
index 16c27d51e..c17eeda91 100644
--- a/packages/sie_server/src/sie_server/adapters/stablebridge_pruner/__init__.py
+++ b/packages/sie_server/src/sie_server/adapters/stablebridge_pruner/__init__.py
@@ -13,6 +13,7 @@
from torch import nn
from transformers import AutoModelForSequenceClassification, AutoTokenizer
+from sie_server.adapters._transformers_compat import install_transformers_security_fixes
from sie_server.adapters.base import ModelAdapter, ModelCapabilities, ModelDims
from sie_server.core.inference_output import ExtractOutput, ScoreOutput
from sie_server.core.preprocessor import CharCountPreprocessor
@@ -125,6 +126,7 @@ def load(self, device: str) -> None:
if self._revision is not None:
shared_kwargs["revision"] = self._revision
+ install_transformers_security_fixes()
self._tokenizer = AutoTokenizer.from_pretrained(
self._model_name_or_path,
**shared_kwargs,
diff --git a/packages/sie_server/src/sie_server/adapters/topk_embed/adapter.py b/packages/sie_server/src/sie_server/adapters/topk_embed/adapter.py
index b355e498f..36d3b5baa 100644
--- a/packages/sie_server/src/sie_server/adapters/topk_embed/adapter.py
+++ b/packages/sie_server/src/sie_server/adapters/topk_embed/adapter.py
@@ -53,6 +53,7 @@
from sie_server.adapters._base_adapter import BaseAdapter
from sie_server.adapters._multivector import maxsim_scores_batched
from sie_server.adapters._spec import AdapterSpec
+from sie_server.adapters._transformers_compat import install_transformers_security_fixes
from sie_server.adapters._types import ComputePrecision
from sie_server.adapters._utils import grouped_score_pairs, validate_output_types
from sie_server.adapters.topk_embed import vision_rotary
@@ -276,6 +277,7 @@ def load(self, device: str) -> None:
topk = {key: raw.pop(key) for key in _TOPK_CONFIG_KEYS if key in raw}
for key in _REMOTE_CODE_CONFIG_KEYS:
raw.pop(key, None)
+ install_transformers_security_fixes()
config = Qwen3_5Config(**raw)
# Bidirectional full-attention layers; the linear-attention layers stay causal recurrences.
config.text_config.is_causal = False
diff --git a/packages/sie_server/src/sie_server/adapters/whisper/adapter.py b/packages/sie_server/src/sie_server/adapters/whisper/adapter.py
index 04a53c6c8..9725d4e27 100644
--- a/packages/sie_server/src/sie_server/adapters/whisper/adapter.py
+++ b/packages/sie_server/src/sie_server/adapters/whisper/adapter.py
@@ -12,6 +12,7 @@
from sie_server.adapters._base_adapter import BaseAdapter
from sie_server.adapters._spec import AdapterSpec
+from sie_server.adapters._transformers_compat import install_transformers_security_fixes
from sie_server.adapters._types import ComputePrecision
from sie_server.core.inference_output import EncodeOutput, ExtractOutput
from sie_server.core.prepared import AudioPayload
@@ -79,6 +80,7 @@ def load(self, device: str) -> None:
dtype,
self._attn_implementation,
)
+ install_transformers_security_fixes()
self._processor = AutoProcessor.from_pretrained(
self._model_name_or_path,
**shared_kwargs,
diff --git a/packages/sie_server/src/sie_server/adapters/xlm_roberta_flash/__init__.py b/packages/sie_server/src/sie_server/adapters/xlm_roberta_flash/__init__.py
index 397a17eaf..a9eeec817 100644
--- a/packages/sie_server/src/sie_server/adapters/xlm_roberta_flash/__init__.py
+++ b/packages/sie_server/src/sie_server/adapters/xlm_roberta_flash/__init__.py
@@ -12,6 +12,7 @@
from sie_server.adapters._flash_base import FlashBaseAdapter
from sie_server.adapters._flash_pack import build_position_ids, mean_pool_packed
from sie_server.adapters._spec import AdapterSpec
+from sie_server.adapters._transformers_compat import install_transformers_security_fixes
from sie_server.adapters._types import ERR_NOT_LOADED, ComputePrecision, PoolingStrategy
from sie_server.adapters._utils import extract_texts, validate_output_types
from sie_server.adapters.peft_lora_mixin import PEFTLoRAMixin
@@ -117,6 +118,7 @@ def load(self, device: str) -> None:
if self._revision is not None:
shared_kwargs["revision"] = self._revision
+ install_transformers_security_fixes()
self._tokenizer = AutoTokenizer.from_pretrained(self._model_name_or_path, **shared_kwargs)
# Load model with eager attention - we'll run our own flash attention
diff --git a/packages/sie_server/tests/adapters/test_lighton_ocr3.py b/packages/sie_server/tests/adapters/test_lighton_ocr3.py
index 719087b75..e4c91df4a 100644
--- a/packages/sie_server/tests/adapters/test_lighton_ocr3.py
+++ b/packages/sie_server/tests/adapters/test_lighton_ocr3.py
@@ -286,6 +286,7 @@ def load_model(name: str, **kwargs: Any) -> _Model:
transformers = ModuleType("transformers")
transformers.__dict__.update(
+ __version__="5.17.0",
AutoProcessor=SimpleNamespace(from_pretrained=load_processor),
Qwen3_5ForConditionalGeneration=SimpleNamespace(from_pretrained=load_model),
)
@@ -353,6 +354,7 @@ def test_load_rebinds_qwen35_patch_embed_to_equivalent_linear(monkeypatch: pytes
expected = conv(patches).clone()
transformers = ModuleType("transformers")
transformers.__dict__.update(
+ __version__="5.17.0",
AutoProcessor=SimpleNamespace(from_pretrained=lambda *_, **__: _Processor()),
Qwen3_5ForConditionalGeneration=SimpleNamespace(from_pretrained=lambda *_, **__: loaded_model),
)
diff --git a/packages/sie_server/tests/adapters/test_sam3.py b/packages/sie_server/tests/adapters/test_sam3.py
index 408e41e5d..8bd74d5cb 100644
--- a/packages/sie_server/tests/adapters/test_sam3.py
+++ b/packages/sie_server/tests/adapters/test_sam3.py
@@ -194,6 +194,7 @@ def test_load_uses_sam3_classes_revision_and_dtype(monkeypatch, device, precisio
sys.modules,
"transformers",
SimpleNamespace(
+ __version__="5.17.0",
Sam3Processor=SimpleNamespace(from_pretrained=processor_loader),
Sam3Model=SimpleNamespace(from_pretrained=model_loader),
),
diff --git a/packages/sie_server/tests/adapters/test_transformers_compat.py b/packages/sie_server/tests/adapters/test_transformers_compat.py
new file mode 100644
index 000000000..834af5408
--- /dev/null
+++ b/packages/sie_server/tests/adapters/test_transformers_compat.py
@@ -0,0 +1,250 @@
+"""Offline adversarial controls for the actual Transformers 4 model loader."""
+
+from __future__ import annotations
+
+import copy
+import json
+from concurrent.futures import ThreadPoolExecutor
+from pathlib import Path
+from typing import Any
+
+import pytest
+import torch
+import transformers
+from sie_server.adapters._transformers_compat import install_transformers_security_fixes
+from transformers import (
+ AutoModelForCausalLM,
+ CLIPConfig,
+ LlamaConfig,
+ LlamaForCausalLM,
+ PretrainedConfig,
+ modeling_utils,
+)
+from transformers.integrations import hub_kernels
+
+pytestmark = pytest.mark.skipif(transformers.__version__ != "4.57.6", reason="Tests the exact supported 4.x runtime")
+
+_ATTACKER_KERNEL = "sie-security-probe.invalid/owned-marker:attention"
+# Capture native bindings at collection, before adapter tests install the
+# permanent production policy. Restore the preceding test's policy on teardown.
+_NATIVE_CONFIG_INIT = PretrainedConfig.__init__
+_NATIVE_HUB_IS_KERNEL = hub_kernels.is_kernel
+_NATIVE_MODEL_IS_KERNEL = modeling_utils.is_kernel
+
+
+@pytest.fixture(autouse=True)
+def _restore_loader_and_disable_network(monkeypatch: pytest.MonkeyPatch) -> None:
+ # Production installs permanently. Tests restore the original bindings so a
+ # preceding test cannot make an unguarded negative control pass accidentally.
+ monkeypatch.setenv("HF_HUB_OFFLINE", "1")
+ monkeypatch.setenv("TRANSFORMERS_OFFLINE", "1")
+ assert not getattr(_NATIVE_CONFIG_INIT, "_sie_kernel_security_backport", False)
+ monkeypatch.setattr(PretrainedConfig, "__init__", _NATIVE_CONFIG_INIT)
+ monkeypatch.setattr(hub_kernels, "is_kernel", _NATIVE_HUB_IS_KERNEL)
+ monkeypatch.setattr(modeling_utils, "is_kernel", _NATIVE_MODEL_IS_KERNEL)
+
+
+@pytest.fixture
+def checkpoint(tmp_path: Path) -> Path:
+ with torch.random.fork_rng():
+ torch.manual_seed(137)
+ config = LlamaConfig(
+ vocab_size=16,
+ hidden_size=8,
+ intermediate_size=16,
+ num_hidden_layers=1,
+ num_attention_heads=2,
+ num_key_value_heads=2,
+ attn_implementation="eager",
+ )
+ LlamaForCausalLM(config).save_pretrained(tmp_path)
+ return tmp_path
+
+
+@pytest.fixture
+def kernel_requests(monkeypatch: pytest.MonkeyPatch) -> list[str]:
+ calls: list[str] = []
+
+ def intercept(repo_id: str, **kwargs: Any) -> None:
+ calls.append(repo_id)
+ raise RuntimeError("Owned offline kernel boundary reached; no fetch or external code executed")
+
+ # Exercise the real native loader even if the optional kernels package is
+ # absent. Intercept only its network/code-loading boundary.
+ monkeypatch.setattr(hub_kernels, "_kernels_available", True)
+ monkeypatch.setattr(hub_kernels, "get_kernel", intercept, raising=False)
+ return calls
+
+
+def _write_config(checkpoint: Path, updates: dict[str, Any]) -> None:
+ path = checkpoint / "config.json"
+ config = json.loads(path.read_text())
+ config.update(updates)
+ path.write_text(json.dumps(config))
+
+
+@pytest.mark.parametrize("field", ["_attn_implementation_internal", "_attn_implementation"])
+@pytest.mark.parametrize("trust", [False, None, True])
+def test_unguarded_checkpoint_reaches_kernel_boundary(
+ checkpoint: Path, kernel_requests: list[str], field: str, trust: bool | None
+) -> None:
+ _write_config(checkpoint, {field: _ATTACKER_KERNEL})
+ with pytest.raises(ValueError, match="Owned offline kernel boundary reached"):
+ AutoModelForCausalLM.from_pretrained(
+ checkpoint, local_files_only=True, trust_remote_code=trust, attn_implementation="eager"
+ )
+ assert kernel_requests == ["sie-security-probe.invalid/owned-marker"]
+
+
+@pytest.mark.parametrize("field", ["_attn_implementation_internal", "_attn_implementation"])
+@pytest.mark.parametrize("trust", [False, None, True])
+def test_checkpoint_cannot_replace_explicit_attention(
+ checkpoint: Path, kernel_requests: list[str], field: str, trust: bool | None
+) -> None:
+ _write_config(checkpoint, {field: _ATTACKER_KERNEL})
+ assert install_transformers_security_fixes()
+ model = AutoModelForCausalLM.from_pretrained(
+ checkpoint, local_files_only=True, trust_remote_code=trust, attn_implementation="eager"
+ )
+ assert type(model) is LlamaForCausalLM
+ assert model.config._attn_implementation == "eager"
+ assert not kernel_requests
+
+
+@pytest.mark.parametrize(
+ "kernel",
+ [
+ _ATTACKER_KERNEL,
+ "paged|sie-security-probe.invalid/owned-marker@main:attention",
+ "kernels-community.evil/owned-marker",
+ "Kernels-community/owned-marker",
+ " kernels-community/owned-marker",
+ "kernels-community/../owned-marker",
+ "other|kernels-community/owned-marker",
+ "kernels-community/name|owned-marker:attention",
+ "kernels-community/name@main|owned-marker:attention",
+ "kernels-community/name:attention|owned-marker",
+ "kernels-community/name@main@other:attention",
+ "paged|paged|kernels-community/owned-marker:attention",
+ "kernels-community/owned-marker:attention\n",
+ ],
+)
+def test_explicit_foreign_kernel_never_reaches_loader(
+ checkpoint: Path, kernel_requests: list[str], kernel: str
+) -> None:
+ install_transformers_security_fixes()
+ with pytest.raises(ValueError, match="not supported"):
+ AutoModelForCausalLM.from_pretrained(
+ checkpoint, local_files_only=True, trust_remote_code=False, attn_implementation=kernel
+ )
+ # The direct native entrypoint and its modeling_utils alias must also be
+ # guarded; checking only the caller's predicate would leave a second route.
+ hub_kernels.load_and_register_kernel(kernel)
+ modeling_utils.load_and_register_kernel(kernel)
+ assert not kernel_requests
+
+
+@pytest.mark.parametrize(
+ "kernel",
+ [
+ "kernels-community/owned-marker:attention",
+ "kernels-community/owned-marker@main:attention",
+ "paged|kernels-community/owned-marker@main:attention",
+ ],
+)
+def test_upstream_community_namespace_remains_available(kernel_requests: list[str], kernel: str) -> None:
+ install_transformers_security_fixes()
+ with pytest.raises(ValueError, match="Owned offline kernel boundary reached"):
+ hub_kernels.load_and_register_kernel(kernel)
+ assert kernel_requests == ["kernels-community/owned-marker"]
+
+
+@pytest.mark.parametrize("field", ["_attn_implementation_internal", "_attn_implementation"])
+def test_nested_native_configs_discard_serialized_private_attention(field: str) -> None:
+ data = {
+ "text_config": {"hidden_size": 8, "num_attention_heads": 2, field: _ATTACKER_KERNEL},
+ "vision_config": {"hidden_size": 8, "num_attention_heads": 2, field: _ATTACKER_KERNEL},
+ }
+ untouched = copy.deepcopy(data)
+ native_subconfigs = dict(CLIPConfig.sub_configs)
+ install_transformers_security_fixes()
+ config = CLIPConfig(**data)
+ assert config.text_config._attn_implementation is None
+ assert config.vision_config._attn_implementation is None
+ assert data == untouched
+ assert CLIPConfig.sub_configs == native_subconfigs
+
+
+@pytest.mark.parametrize("field", ["__dict__", "__class__", "attribute_map", "sub_configs"])
+def test_checkpoint_cannot_replace_config_structure(checkpoint: Path, kernel_requests: list[str], field: str) -> None:
+ attack = (
+ {"__class__": "owned-json-marker"}
+ if field == "sub_configs"
+ else {"_attn_implementation_internal": _ATTACKER_KERNEL}
+ )
+ _write_config(checkpoint, {field: attack})
+ native_attention_property = LlamaConfig._attn_implementation
+ install_transformers_security_fixes()
+ with pytest.raises(ValueError, match="cannot override"):
+ AutoModelForCausalLM.from_pretrained(checkpoint, local_files_only=True, trust_remote_code=False)
+ assert not kernel_requests
+ assert LlamaConfig._attn_implementation is native_attention_property
+
+
+@pytest.mark.parametrize("attention", ["eager", "sdpa"])
+def test_native_forward_weights_and_outputs_unchanged(checkpoint: Path, attention: str) -> None:
+ before = AutoModelForCausalLM.from_pretrained(checkpoint, local_files_only=True, attn_implementation=attention)
+ original_forward = LlamaForCausalLM.forward
+ tokens = torch.tensor([[1, 2, 3]])
+ with torch.inference_mode():
+ expected = before(tokens).logits
+ install_transformers_security_fixes()
+ after = AutoModelForCausalLM.from_pretrained(checkpoint, local_files_only=True, attn_implementation=attention)
+ assert type(before) is type(after) is LlamaForCausalLM
+ assert LlamaForCausalLM.forward is original_forward
+ assert before.config.to_dict() == after.config.to_dict()
+ for key, value in before.state_dict().items():
+ assert torch.equal(value, after.state_dict()[key])
+ with torch.inference_mode():
+ assert torch.equal(expected, after(tokens).logits)
+
+
+def test_installation_is_idempotent_across_concurrent_loaders(kernel_requests: list[str]) -> None:
+ original_forward = LlamaForCausalLM.forward
+
+ def load(_: int) -> bool:
+ installed = install_transformers_security_fixes()
+ config = LlamaConfig(_attn_implementation_internal=_ATTACKER_KERNEL)
+ assert config._attn_implementation is None
+ return installed
+
+ with ThreadPoolExecutor(max_workers=8) as executor:
+ assert sum(executor.map(load, range(32))) == 1
+ assert LlamaForCausalLM.forward is original_forward
+ assert not kernel_requests
+
+
+@pytest.mark.parametrize("version", ["4.57.5", "5.0.0", "5.2.0", "5.3.0rc1"])
+def test_unsupported_vulnerable_runtime_fails_before_mutation(monkeypatch: pytest.MonkeyPatch, version: str) -> None:
+ original = (PretrainedConfig.__init__, hub_kernels.is_kernel, modeling_utils.is_kernel)
+ # Importing model classes can replace the root lazy module; resolve the
+ # current sys.modules binding, as the optional installer import does.
+ monkeypatch.setattr("transformers.__version__", version)
+ with pytest.raises(RuntimeError, match=r"requires transformers==4\.57\.6"):
+ install_transformers_security_fixes()
+ assert original == (PretrainedConfig.__init__, hub_kernels.is_kernel, modeling_utils.is_kernel)
+
+
+@pytest.mark.parametrize("version", ["5.3.0", "5.12.1", "5.17.0"])
+def test_fixed_runtime_is_untouched(monkeypatch: pytest.MonkeyPatch, version: str) -> None:
+ original = (PretrainedConfig.__init__, hub_kernels.is_kernel, modeling_utils.is_kernel)
+ monkeypatch.setattr("transformers.__version__", version)
+ assert not install_transformers_security_fixes()
+ assert original == (PretrainedConfig.__init__, hub_kernels.is_kernel, modeling_utils.is_kernel)
+
+
+def test_replaced_guard_is_detected(monkeypatch: pytest.MonkeyPatch) -> None:
+ install_transformers_security_fixes()
+ monkeypatch.setattr(hub_kernels, "is_kernel", lambda _: True)
+ with pytest.raises(RuntimeError, match="guard was replaced"):
+ install_transformers_security_fixes()
diff --git a/packages/sie_server/tests/adapters/test_transformers_security_integration.py b/packages/sie_server/tests/adapters/test_transformers_security_integration.py
new file mode 100644
index 000000000..c5d0193cc
--- /dev/null
+++ b/packages/sie_server/tests/adapters/test_transformers_security_integration.py
@@ -0,0 +1,802 @@
+"""Offline controls at SIE's actual native/dependent model-load boundaries."""
+
+from __future__ import annotations
+
+import importlib
+import json
+import os
+import subprocess
+import sys
+from collections.abc import Iterator
+from pathlib import Path
+from types import SimpleNamespace
+from typing import Any
+from unittest.mock import Mock
+
+import pytest
+import torch
+import transformers
+from sie_server.adapters import _transformers_compat as compat
+from sie_server.adapters.cross_encoder import CrossEncoderAdapter
+from sie_server.adapters.cross_encoder.runtime_lora import CrossEncoderLoraAdapter
+from sie_server.adapters.docling.adapter import DoclingAdapter
+from sie_server.adapters.pytorch_embedding import PyTorchEmbeddingAdapter
+from sie_server.adapters.sglang import generation as sglang_generation
+from sie_server.adapters.sglang_vision_extract import adapter as sglang_vision
+from sie_server.adapters.siglip.adapter import SiglipAdapter
+from sie_server.types.inputs import Item
+from transformers import (
+ AutoConfig,
+ AutoProcessor,
+ AutoTokenizer,
+ BertConfig,
+ BertForSequenceClassification,
+ BertModel,
+ BertTokenizerFast,
+ PretrainedConfig,
+ modeling_utils,
+)
+from transformers.integrations import hub_kernels
+
+_EXACT_TF4 = pytest.mark.skipif(transformers.__version__ != "4.57.6", reason="Exact TF4 backport controls")
+_ATTACK = "sie-security-probe.invalid/owned-marker:attention"
+_PRIVATE_FIELDS = (
+ "_attn_implementation_internal",
+ "_attn_implementation",
+ "_experts_implementation_internal",
+ "_experts_implementation",
+)
+_STRUCTURE_FIELDS = ("__dict__", "__class__", "attribute_map", "sub_configs")
+_NATIVE_CONFIG_INIT = PretrainedConfig.__init__
+_NATIVE_HUB_IS_KERNEL = hub_kernels.is_kernel
+_NATIVE_MODEL_IS_KERNEL = modeling_utils.is_kernel
+
+
+@pytest.fixture(autouse=True)
+def offline_native_loader(monkeypatch: pytest.MonkeyPatch) -> Iterator[None]:
+ monkeypatch.setenv("HF_HUB_OFFLINE", "1")
+ monkeypatch.setenv("TRANSFORMERS_OFFLINE", "1")
+ assert not getattr(_NATIVE_CONFIG_INIT, "_sie_kernel_security_backport", False)
+ monkeypatch.setattr(PretrainedConfig, "__init__", _NATIVE_CONFIG_INIT)
+ monkeypatch.setattr(hub_kernels, "is_kernel", _NATIVE_HUB_IS_KERNEL)
+ monkeypatch.setattr(modeling_utils, "is_kernel", _NATIVE_MODEL_IS_KERNEL)
+ threads = torch.get_num_threads()
+ torch.set_num_threads(1)
+ yield
+ torch.set_num_threads(threads)
+
+
+@pytest.fixture
+def kernel_boundary(monkeypatch: pytest.MonkeyPatch) -> Mock:
+ boundary = Mock(side_effect=AssertionError("Forbidden kernel loader reached"))
+ monkeypatch.setattr(hub_kernels, "_kernels_available", True)
+ monkeypatch.setattr(hub_kernels, "get_kernel", boundary, raising=False)
+ return boundary
+
+
+def _update_config(path: Path, **values: Any) -> None:
+ config = json.loads(path.read_text())
+ config.update(values)
+ path.write_text(json.dumps(config))
+
+
+def _tokenizer(directory: Path) -> BertTokenizerFast:
+ directory.mkdir(exist_ok=True)
+ vocab = ["[PAD]", "[UNK]", "[CLS]", "[SEP]", "[MASK]", "alice", "bob", "met", "person", "."]
+ vocabulary = directory / "vocab.txt"
+ vocabulary.write_text("\n".join(vocab) + "\n")
+ tokenizer = BertTokenizerFast(vocab_file=str(vocabulary), model_max_length=64)
+ tokenizer.save_pretrained(directory)
+ return tokenizer
+
+
+def _bert_config(tokenizer: BertTokenizerFast) -> BertConfig:
+ return BertConfig(
+ vocab_size=len(tokenizer),
+ hidden_size=16,
+ intermediate_size=32,
+ num_hidden_layers=1,
+ num_attention_heads=2,
+ max_position_embeddings=128,
+ hidden_dropout_prob=0,
+ attention_probs_dropout_prob=0,
+ num_labels=1,
+ )
+
+
+@pytest.fixture
+def bert_checkpoints(tmp_path: Path) -> tuple[Path, Path]:
+ embedding, cross_encoder = tmp_path / "embedding", tmp_path / "cross-encoder"
+ with torch.random.fork_rng():
+ torch.manual_seed(81)
+ BertModel(_bert_config(_tokenizer(embedding))).save_pretrained(embedding)
+ BertForSequenceClassification(_bert_config(_tokenizer(cross_encoder))).save_pretrained(cross_encoder)
+ return embedding, cross_encoder
+
+
+@_EXACT_TF4
+@pytest.mark.parametrize("field", _PRIVATE_FIELDS)
+def test_direct_embedding_load_drops_serialized_private_settings(
+ bert_checkpoints: tuple[Path, Path], kernel_boundary: Mock, field: str
+) -> None:
+ path, _ = bert_checkpoints
+ tokens = torch.tensor([[2, 5, 6, 3]])
+ native = BertModel.from_pretrained(path, attn_implementation="eager").eval()
+ with torch.inference_mode():
+ expected = native(tokens).last_hidden_state
+ _update_config(path / "config.json", **{field: _ATTACK})
+ adapter = PyTorchEmbeddingAdapter(path, attn_implementation="eager", compute_precision="float32")
+ try:
+ adapter.load("cpu")
+ assert type(adapter._model) is BertModel
+ assert "forward" not in adapter._model.__dict__
+ assert adapter._model.config._attn_implementation == "eager"
+ for key, value in native.state_dict().items():
+ assert torch.equal(value, adapter._model.state_dict()[key])
+ with torch.inference_mode():
+ assert torch.equal(expected, adapter._model(tokens).last_hidden_state)
+ kernel_boundary.assert_not_called()
+ finally:
+ adapter.unload()
+
+
+@_EXACT_TF4
+@pytest.mark.parametrize("field", _STRUCTURE_FIELDS)
+def test_direct_load_rejects_config_structure_before_model_construction(
+ bert_checkpoints: tuple[Path, Path], kernel_boundary: Mock, field: str
+) -> None:
+ path, _ = bert_checkpoints
+ _update_config(path / "config.json", **{field: {"__class__": "marker"}})
+ adapter = PyTorchEmbeddingAdapter(path)
+ with pytest.raises(ValueError, match="config attribute structure"):
+ adapter.load("cpu")
+ assert adapter._model is None
+ kernel_boundary.assert_not_called()
+
+
+@_EXACT_TF4
+@pytest.mark.parametrize("field", _PRIVATE_FIELDS)
+def test_sglang_parent_processor_guards_actual_config_fallback(
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch, field: str
+) -> None:
+ tokenizer = _tokenizer(tmp_path)
+ _bert_config(tokenizer).save_pretrained(tmp_path)
+ expected = AutoProcessor.from_pretrained(tmp_path, trust_remote_code=False)
+ _update_config(tmp_path / "config.json", **{field: _ATTACK})
+ native = AutoConfig.from_pretrained
+ observations = []
+
+ def observed_config(cls: type, *args: Any, **kwargs: Any) -> Any:
+ assert getattr(PretrainedConfig.__init__, "_sie_kernel_security_backport", False)
+ config = native(*args, **kwargs)
+ observations.append(config)
+ return config
+
+ monkeypatch.setattr(AutoConfig, "from_pretrained", classmethod(observed_config))
+ engine = Mock()
+ monkeypatch.setattr(sglang_vision.SGLangGenerationAdapter, "load", engine)
+ adapter = sglang_vision.SGLangVisionExtractAdapter(tmp_path, trust_remote_code=False)
+ start_loop = Mock()
+ monkeypatch.setattr(adapter, "_start_request_loop", start_loop)
+ try:
+ adapter.load("cuda:0") # Only the real CPU processor runs; the engine boundary is mocked.
+ assert observations
+ assert all(getattr(config, field, None) is None for config in observations)
+ assert type(adapter._processor) is type(expected) is BertTokenizerFast
+ assert adapter._processor(["alice", "bob"]) == expected(["alice", "bob"])
+ engine.assert_called_once_with("cuda:0")
+ start_loop.assert_called_once_with()
+ finally:
+ adapter.unload()
+
+
+@_EXACT_TF4
+@pytest.mark.parametrize("field", _STRUCTURE_FIELDS)
+def test_sglang_parent_processor_rejects_config_structure_before_engine(
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch, field: str
+) -> None:
+ _bert_config(_tokenizer(tmp_path)).save_pretrained(tmp_path)
+ _update_config(tmp_path / "config.json", **{field: {"__class__": "owned-marker"}})
+ engine = Mock()
+ monkeypatch.setattr(sglang_vision.SGLangGenerationAdapter, "load", engine)
+ adapter = sglang_vision.SGLangVisionExtractAdapter(tmp_path, trust_remote_code=False)
+ start_loop = Mock()
+ monkeypatch.setattr(adapter, "_start_request_loop", start_loop)
+ with pytest.raises(ValueError, match="config attribute structure"):
+ adapter.load("cuda:0")
+ assert adapter._processor is None
+ engine.assert_not_called()
+ start_loop.assert_not_called()
+
+
+@_EXACT_TF4
+def test_sglang_parent_processor_refuses_unsupported_version_before_constructor(
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+) -> None:
+ monkeypatch.setattr("transformers.__version__", "4.57.5")
+ processor = Mock(side_effect=AssertionError("Processor must not run"))
+ engine = Mock(side_effect=AssertionError("Engine must not run"))
+ monkeypatch.setattr(AutoProcessor, "from_pretrained", processor)
+ monkeypatch.setattr(sglang_vision.SGLangGenerationAdapter, "load", engine)
+ adapter = sglang_vision.SGLangVisionExtractAdapter(tmp_path, trust_remote_code=False)
+ with pytest.raises(RuntimeError, match=r"requires transformers==4\.57\.6"):
+ adapter.load("cuda:0")
+ assert adapter._processor is None
+ processor.assert_not_called()
+ engine.assert_not_called()
+
+
+def _reasoning_tokenizer(directory: Path) -> BertTokenizerFast:
+ tokenizer = _tokenizer(directory)
+ tokenizer.add_special_tokens({"additional_special_tokens": ["", ""]})
+ tokenizer.save_pretrained(directory)
+ _bert_config(tokenizer).save_pretrained(directory)
+ config_path = directory / "tokenizer_config.json"
+ config = json.loads(config_path.read_text())
+ del config["tokenizer_class"] # Exercise AutoTokenizer's actual AutoConfig fallback.
+ config_path.write_text(json.dumps(config))
+ return tokenizer
+
+
+@_EXACT_TF4
+@pytest.mark.parametrize("field", _PRIVATE_FIELDS)
+def test_sglang_parent_reasoning_tokenizer_guards_actual_config_fallback(
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch, field: str
+) -> None:
+ tokenizer = _reasoning_tokenizer(tmp_path)
+ _update_config(tmp_path / "config.json", **{field: _ATTACK})
+ native = AutoConfig.from_pretrained
+ observations = []
+
+ def observed_config(cls: type, *args: Any, **kwargs: Any) -> Any:
+ assert getattr(PretrainedConfig.__init__, "_sie_kernel_security_backport", False)
+ config = native(*args, **kwargs)
+ observations.append(config)
+ return config
+
+ monkeypatch.setattr(AutoConfig, "from_pretrained", classmethod(observed_config))
+ ids = sglang_generation._reasoning_control_token_ids(
+ str(tmp_path), revision=None, trust_remote_code=False, reasoning_format="qwen3"
+ )
+ assert observations
+ assert all(getattr(config, field, None) is None for config in observations)
+ assert ids == {
+ "open": tokenizer.convert_tokens_to_ids(""),
+ "close": tokenizer.convert_tokens_to_ids(""),
+ "newline": None,
+ }
+
+
+@_EXACT_TF4
+@pytest.mark.parametrize("field", _STRUCTURE_FIELDS)
+def test_sglang_parent_reasoning_tokenizer_rejects_structure_before_engine(
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch, field: str
+) -> None:
+ _reasoning_tokenizer(tmp_path)
+ _update_config(tmp_path / "config.json", **{field: {"__class__": "owned-marker"}})
+ reserve_port = Mock(side_effect=AssertionError("Engine resources must not be reserved"))
+ monkeypatch.setattr(sglang_generation._server, "find_free_port", reserve_port)
+ adapter = sglang_generation.SGLangGenerationAdapter(
+ str(tmp_path), reasoning_parser="qwen3", thinking_budget=32, trust_remote_code=False
+ )
+ with pytest.raises(ValueError, match="config attribute structure"):
+ adapter.load("cuda:0")
+ assert adapter._thinking_budget_tokens is None
+ reserve_port.assert_not_called()
+
+
+@_EXACT_TF4
+def test_sglang_parent_reasoning_tokenizer_refuses_unsupported_version_before_constructor(
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+) -> None:
+ monkeypatch.setattr("transformers.__version__", "4.57.5")
+ tokenizer = Mock(side_effect=AssertionError("Tokenizer must not run"))
+ reserve_port = Mock(side_effect=AssertionError("Engine resources must not be reserved"))
+ monkeypatch.setattr(AutoTokenizer, "from_pretrained", tokenizer)
+ monkeypatch.setattr(sglang_generation._server, "find_free_port", reserve_port)
+ adapter = sglang_generation.SGLangGenerationAdapter(
+ str(tmp_path), reasoning_parser="qwen3", thinking_budget=32, trust_remote_code=False
+ )
+ with pytest.raises(RuntimeError, match=r"requires transformers==4\.57\.6"):
+ adapter.load("cuda:0")
+ assert adapter._thinking_budget_tokens is None
+ tokenizer.assert_not_called()
+ reserve_port.assert_not_called()
+
+
+@_EXACT_TF4
+@pytest.mark.parametrize("adapter_class", [CrossEncoderAdapter, CrossEncoderLoraAdapter])
+@pytest.mark.parametrize("trust", [False, True])
+def test_st_and_inherited_lora_load_install_before_native_constructor(
+ bert_checkpoints: tuple[Path, Path], kernel_boundary: Mock, adapter_class: type, trust: bool
+) -> None:
+ _, path = bert_checkpoints
+ _update_config(path / "config.json", _attn_implementation_internal=_ATTACK)
+ kwargs = {"lora_paths": {"deferred": str(path / "never-read")}} if adapter_class is CrossEncoderLoraAdapter else {}
+ adapter = adapter_class(path, trust_remote_code=trust, compute_precision="float32", **kwargs)
+ try:
+ adapter.load("cpu")
+ assert type(adapter._model.model) is BertForSequenceClassification
+ assert "forward" not in adapter._model.model.__dict__
+ scores = adapter.score(Item(text="alice"), [Item(text="alice met bob"), Item(text="bob")])
+ assert len(scores) == 2
+ assert torch.isfinite(torch.tensor(scores)).all()
+ kernel_boundary.assert_not_called()
+ finally:
+ adapter.unload()
+
+
+@_EXACT_TF4
+@pytest.mark.parametrize("variant", ["entities", "classification", "entity_lora"])
+def test_actual_dependent_gliner2_load_and_inherited_variants(
+ tmp_path: Path, kernel_boundary: Mock, variant: str
+) -> None:
+ gliner2 = pytest.importorskip("gliner2")
+ from gliner2.model import ExtractorConfig
+ from sie_server.adapters.gliner2.adapter import GLiNER2Adapter
+ from sie_server.adapters.gliner2.classification import GLiNER2ClassificationAdapter
+ from sie_server.adapters.gliner2.entity_lora import GLiNER2EntityLoraAdapter
+
+ checkpoint = tmp_path / "extractor"
+ tokenizer = _tokenizer(checkpoint)
+ with torch.random.fork_rng():
+ torch.manual_seed(83)
+ native = gliner2.GLiNER2(
+ ExtractorConfig(model_name="generated-local", max_width=3, counting_layer="count_lstm"),
+ encoder_config=_bert_config(tokenizer),
+ tokenizer=tokenizer,
+ )
+ native.save_pretrained(str(checkpoint))
+ _update_config(checkpoint / "encoder_config/config.json", _attn_implementation_internal=_ATTACK)
+ adapter_class = {"entities": GLiNER2Adapter, "classification": GLiNER2ClassificationAdapter}.get(variant)
+ if adapter_class is None:
+ adapter = GLiNER2EntityLoraAdapter(checkpoint, lora_paths={"deferred": str(tmp_path / "never-read")})
+ else:
+ adapter = adapter_class(checkpoint)
+ try:
+ adapter.load("cpu")
+ assert type(adapter._model) is type(native)
+ assert type(adapter._model.encoder) is BertModel
+ assert "forward" not in adapter._model.encoder.__dict__
+ for name, value in native.state_dict().items():
+ assert torch.equal(value, adapter._model.state_dict()[name])
+ tokens = torch.tensor([[2, 5, 6, 3]])
+ native.eval()
+ adapter._model.eval()
+ with torch.inference_mode():
+ assert torch.equal(
+ native.encoder(tokens).last_hidden_state, adapter._model.encoder(tokens).last_hidden_state
+ )
+ kernel_boundary.assert_not_called()
+ finally:
+ adapter.unload()
+
+
+@_EXACT_TF4
+def test_docling_guard_failure_is_not_swallowed_by_prewarm(monkeypatch: pytest.MonkeyPatch) -> None:
+ monkeypatch.setattr("transformers.__version__", "4.57.5")
+ adapter = DoclingAdapter("owned-fixture")
+ converter = Mock(side_effect=AssertionError("Converter must not run"))
+ monkeypatch.setattr(adapter, "_get_converter", converter)
+ monkeypatch.setattr(adapter, "_resolve_artifacts_path", lambda: None)
+ with pytest.raises(RuntimeError, match=r"requires transformers==4\.57\.6"):
+ adapter.load("cpu")
+ assert not adapter._loaded
+ converter.assert_not_called()
+
+
+@_EXACT_TF4
+def test_docling_lazy_models_inherit_permanent_load_policy(
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch, kernel_boundary: Mock
+) -> None:
+ BertConfig().save_pretrained(tmp_path)
+ _update_config(tmp_path / "config.json", _attn_implementation_internal=_ATTACK)
+ adapter = DoclingAdapter("owned-fixture")
+ monkeypatch.setattr(adapter, "_resolve_artifacts_path", lambda: None)
+ calls: list[bool] = []
+
+ def converter(ocr_enabled: bool) -> object:
+ config = AutoConfig.from_pretrained(tmp_path, local_files_only=True)
+ assert config._attn_implementation is None
+ calls.append(ocr_enabled)
+ return object()
+
+ monkeypatch.setattr(adapter, "_make_converter", converter)
+ monkeypatch.setattr(adapter, "_convert_bytes", lambda *args, **kwargs: ([], 0, None))
+ adapter.load("cpu")
+ assert adapter._loaded
+ assert calls == [False, True]
+ # The native lazy cache can reconstruct a converter after prewarming. It
+ # still sees the permanent policy, without a request-time installer call.
+ adapter._converters.clear()
+ adapter._get_converter(ocr_enabled=True)
+ assert calls == [False, True, True]
+ kernel_boundary.assert_not_called()
+
+
+@_EXACT_TF4
+@pytest.mark.parametrize("variant", ["pplx", "nemo"])
+def test_guard_precedes_dependent_and_fallback_native_loaders(
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch, variant: str
+) -> None:
+ monkeypatch.setattr("transformers.__version__", "4.57.5")
+ model = Mock(side_effect=AssertionError("Model constructor must not run"))
+ if variant == "pplx":
+ module = importlib.import_module("sie_server.adapters.pplx_decider.adapter")
+ monkeypatch.setattr(module, "checkpoint_config", lambda path: {})
+ monkeypatch.setattr(module, "load_model", model)
+ adapter = module.PerplexityDecisionAdapter(str(tmp_path))
+ else:
+ module = importlib.import_module("sie_server.adapters.nemo_colembed")
+ adapter = module.NemoColEmbedAdapter(str(tmp_path))
+ monkeypatch.setattr(adapter, "_load_v1_dynamic", model)
+ monkeypatch.setattr(transformers.AutoModel, "from_pretrained", model)
+ with pytest.raises(RuntimeError, match=r"requires transformers==4\.57\.6"):
+ adapter.load("cpu")
+ model.assert_not_called()
+
+
+@pytest.mark.parametrize("hub", [False, True])
+@pytest.mark.parametrize("hf_text", [False, True])
+def test_open_clip_guards_available_transformers_before_factory(
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch, hub: bool, hf_text: bool
+) -> None:
+ open_clip = pytest.importorskip("open_clip")
+ module = importlib.import_module("sie_server.adapters.siglip.adapter")
+ text_config = {"context_length": 16, **({"hf_model_name": "owned-tiny"} if hf_text else {})}
+ model_config = {"embed_dim": 8, "vision_cfg": {}, "text_cfg": text_config}
+ selected: list[str] = []
+ events: list[str] = []
+ monkeypatch.setattr(module, "install_transformers_security_fixes", lambda: events.append("guard"))
+
+ def get_config(name: str) -> dict[str, Any]:
+ selected.append(name)
+ raise AssertionError("A separate config preview cannot authorize the later factory read")
+
+ def create(*args: Any, **kwargs: Any) -> tuple[Any, object, object]:
+ assert events == ["guard"]
+ events.append("construct")
+ return SimpleNamespace(to=lambda **kw: None, eval=lambda: None), object(), object()
+
+ monkeypatch.setattr(open_clip, "get_model_config", get_config)
+ monkeypatch.setattr(open_clip, "create_model_and_transforms", create)
+ monkeypatch.setattr(open_clip, "get_tokenizer", lambda name: object())
+ model_id = "ViT/owned" # The real factory normalizes legacy slash names.
+ if hub:
+ model_id = "hf-hub:owned/tiny"
+ (tmp_path / "open_clip_model.safetensors").write_bytes(b"owned-placeholder-not-loaded")
+ (tmp_path / "open_clip_config.json").write_text(
+ json.dumps(
+ {
+ "model_cfg": model_config,
+ "preprocess_cfg": {
+ "mean": [0, 0, 0],
+ "std": [1, 1, 1],
+ "interpolation": "bicubic",
+ "resize_mode": "shortest",
+ },
+ }
+ )
+ )
+ monkeypatch.setattr("huggingface_hub.snapshot_download", lambda **kwargs: str(tmp_path))
+ monkeypatch.setattr(open_clip, "add_model_config", lambda path: None)
+ adapter = SiglipAdapter(
+ "owned", backend="open_clip", open_clip_model_id=model_id, dense_dim=8, revision="a" * 40 if hub else None
+ )
+ adapter.load("cpu")
+ assert events == ["guard", "construct"]
+ assert selected == []
+
+
+@_EXACT_TF4
+def test_open_clip_native_model_refuses_installed_unsupported_transformers(monkeypatch: pytest.MonkeyPatch) -> None:
+ open_clip = pytest.importorskip("open_clip")
+ monkeypatch.setattr("transformers.__version__", "4.57.5")
+ constructor = Mock(side_effect=AssertionError("Factory must not run"))
+ monkeypatch.setattr(open_clip, "create_model_and_transforms", constructor)
+ adapter = SiglipAdapter("owned", backend="open_clip", open_clip_model_id="ViT-B-32", dense_dim=8)
+ with pytest.raises(RuntimeError, match=r"requires transformers==4\.57\.6"):
+ adapter.load("cpu")
+ constructor.assert_not_called()
+
+
+@pytest.mark.parametrize("error", [ImportError("broken discovery"), ValueError("missing module spec")])
+def test_open_clip_does_not_hide_transformers_discovery_errors(
+ monkeypatch: pytest.MonkeyPatch, error: Exception
+) -> None:
+ open_clip = pytest.importorskip("open_clip")
+ constructor = Mock(side_effect=AssertionError("Factory must not run"))
+ monkeypatch.setattr(open_clip, "create_model_and_transforms", constructor)
+ module = importlib.import_module("sie_server.adapters.siglip.adapter")
+ monkeypatch.setattr(module, "find_spec", Mock(side_effect=error))
+ adapter = SiglipAdapter("owned", backend="open_clip", open_clip_model_id="ViT-B-32", dense_dim=8)
+ with pytest.raises(type(error), match=str(error)):
+ adapter.load("cpu")
+ constructor.assert_not_called()
+
+
+def test_open_clip_installer_failure_prevents_factory(monkeypatch: pytest.MonkeyPatch) -> None:
+ open_clip = pytest.importorskip("open_clip")
+ constructor = Mock(side_effect=AssertionError("Factory must not run"))
+ monkeypatch.setattr(open_clip, "create_model_and_transforms", constructor)
+ module = importlib.import_module("sie_server.adapters.siglip.adapter")
+ monkeypatch.setattr(
+ module, "install_transformers_security_fixes", Mock(side_effect=RuntimeError("Owned installation failure"))
+ )
+ adapter = SiglipAdapter("owned", backend="open_clip", open_clip_model_id="ViT-B-32", dense_dim=8)
+ with pytest.raises(RuntimeError, match="Owned installation failure"):
+ adapter.load("cpu")
+ constructor.assert_not_called()
+
+
+@pytest.mark.parametrize("version", ["5.3.0", "5.17.0"])
+def test_open_clip_fixed_version_policy_preserves_native_bindings(
+ monkeypatch: pytest.MonkeyPatch, version: str
+) -> None:
+ open_clip = pytest.importorskip("open_clip")
+ # This is a version-dispatch control, not a claim of loading those wheels.
+ monkeypatch.setattr("transformers.__version__", version)
+ before = (PretrainedConfig.__init__, hub_kernels.is_kernel, modeling_utils.is_kernel)
+ model = SimpleNamespace(to=lambda **kw: None, eval=lambda: None)
+ factory = Mock(return_value=(model, object(), object()))
+ monkeypatch.setattr(open_clip, "create_model_and_transforms", factory)
+ monkeypatch.setattr(open_clip, "get_tokenizer", lambda name: object())
+ adapter = SiglipAdapter("owned", backend="open_clip", open_clip_model_id="owned-native", dense_dim=8)
+ adapter.load("cpu")
+ after = (PretrainedConfig.__init__, hub_kernels.is_kernel, modeling_utils.is_kernel)
+ assert all(left is right for left, right in zip(before, after, strict=True))
+ factory.assert_called_once_with("owned-native")
+ adapter.unload()
+
+
+@pytest.mark.parametrize("kind", ["unknown", "absent", "malformed", "invalid-json", "empty"])
+def test_open_clip_preserves_native_identifier_errors(tmp_path: Path, kind: str) -> None:
+ open_clip = pytest.importorskip("open_clip")
+ identifier = f"local-dir:{tmp_path / 'missing'}"
+ if kind == "unknown":
+ identifier = "owned-unregistered-model"
+ elif kind == "empty":
+ identifier = "local-dir:"
+ elif kind in {"malformed", "invalid-json"}:
+ identifier = f"local-dir:{tmp_path}"
+ (tmp_path / "open_clip_config.json").write_text("{}" if kind == "malformed" else "{invalid-json")
+ with pytest.raises((ValueError, RuntimeError, FileNotFoundError)) as native:
+ open_clip.create_model_and_transforms(identifier)
+ adapter = SiglipAdapter("owned", backend="open_clip", open_clip_model_id=identifier, dense_dim=8)
+ with pytest.raises(type(native.value)) as guarded:
+ adapter.load("cpu")
+ assert str(guarded.value) == str(native.value)
+
+
+@_EXACT_TF4
+def test_actual_open_clip_hf_text_constructor_sees_guard(
+ bert_checkpoints: tuple[Path, Path], monkeypatch: pytest.MonkeyPatch, kernel_boundary: Mock
+) -> None:
+ open_clip = pytest.importorskip("open_clip")
+ from open_clip.model import CustomTextCLIP
+
+ path, _ = bert_checkpoints
+ _update_config(path / "config.json", _attn_implementation_internal=_ATTACK)
+ config = {
+ "embed_dim": 8,
+ "vision_cfg": {"layers": 1, "width": 32, "head_width": 16, "patch_size": 4, "image_size": 8},
+ "text_cfg": {
+ "hf_model_name": str(path),
+ "hf_model_pretrained": True,
+ "hf_proj_type": "linear",
+ "hf_pooler_type": "mean_pooler",
+ },
+ }
+ monkeypatch.setattr(open_clip.factory, "_MODEL_CONFIGS", {**open_clip.factory._MODEL_CONFIGS, "owned-tiny": config})
+ # Tokenizer choice is independent of the HF model loader under test.
+ monkeypatch.setattr(open_clip, "get_tokenizer", lambda name: object())
+ adapter = SiglipAdapter(
+ "owned", backend="open_clip", open_clip_model_id="owned-tiny", dense_dim=8, compute_precision="float32"
+ )
+ # OpenCLIP captured its AutoModel/AutoConfig aliases before installation.
+ assert open_clip.hf_model.AutoModel is transformers.AutoModel
+ assert open_clip.hf_model.AutoConfig is AutoConfig
+ installed = None
+ first_output = None
+ for _ in range(2):
+ try:
+ with torch.random.fork_rng():
+ torch.manual_seed(187)
+ adapter.load("cpu")
+ bindings = (PretrainedConfig.__init__, hub_kernels.is_kernel, modeling_utils.is_kernel)
+ if installed is None:
+ installed = bindings
+ else:
+ assert all(before is after for before, after in zip(installed, bindings, strict=True))
+ assert type(adapter._model) is CustomTextCLIP
+ assert type(adapter._model.text.transformer) is BertModel
+ assert "forward" not in adapter._model.text.transformer.__dict__
+ with torch.inference_mode():
+ output = adapter._model.encode_text(torch.tensor([[2, 5, 6, 3]]))
+ assert output.shape == (1, 8)
+ assert torch.isfinite(output).all()
+ if first_output is None:
+ first_output = output
+ else:
+ assert torch.equal(first_output, output)
+ kernel_boundary.assert_not_called()
+ finally:
+ adapter.unload()
+
+
+@_EXACT_TF4
+@pytest.mark.parametrize("case", ["absolute", "collision", "factory-change"])
+def test_actual_open_clip_local_directory_cannot_bypass_guard(
+ tmp_path: Path,
+ bert_checkpoints: tuple[Path, Path],
+ monkeypatch: pytest.MonkeyPatch,
+ kernel_boundary: Mock,
+ case: str,
+) -> None:
+ open_clip = pytest.importorskip("open_clip", minversion="3.0.0")
+ path, _ = bert_checkpoints
+ tokens = torch.tensor([[2, 5, 6, 3]])
+ expected = BertModel.from_pretrained(path, add_pooling_layer=False).eval()
+ _update_config(path / "config.json", _attn_implementation_internal=_ATTACK)
+ vision = {"layers": 1, "width": 32, "head_width": 16, "patch_size": 4, "image_size": 8}
+ native = {
+ "embed_dim": 8,
+ "vision_cfg": vision,
+ "text_cfg": {"context_length": 8, "vocab_size": 49408, "width": 16, "heads": 2, "layers": 1},
+ }
+ hf = {
+ "embed_dim": 8,
+ "vision_cfg": vision,
+ "text_cfg": {
+ "hf_model_name": str(path),
+ "hf_tokenizer_name": str(path),
+ "hf_model_pretrained": True,
+ "hf_proj_type": "linear",
+ "hf_pooler_type": "mean_pooler",
+ },
+ }
+ selected = tmp_path / "nested" / "selected"
+ selected.mkdir(parents=True)
+ # OpenCLIP 3 loads a local-dir tokenizer from that same directory, even
+ # when text_cfg names a separate model path.
+ _tokenizer(selected)
+ config_path = selected / "open_clip_config.json"
+ config_path.write_text(json.dumps({"model_cfg": hf}))
+ monkeypatch.chdir(tmp_path)
+ identifier = f"local-dir:{selected}" if case == "absolute" else "local-dir:nested/selected"
+ if case == "collision":
+ sibling = tmp_path / "nested-selected"
+ sibling.mkdir()
+ (sibling / "open_clip_config.json").write_text(json.dumps({"model_cfg": native}))
+ elif case == "factory-change":
+ # A file can change after a security preview but before the native
+ # factory reads it. The permanent policy must already be installed.
+ config_path.write_text(json.dumps({"model_cfg": native}))
+ factory = open_clip.create_model_and_transforms
+
+ def changed_factory(*args: Any, **kwargs: Any) -> Any:
+ config_path.write_text(json.dumps({"model_cfg": hf}))
+ return factory(*args, **kwargs)
+
+ monkeypatch.setattr(open_clip, "create_model_and_transforms", changed_factory)
+ adapter = SiglipAdapter("owned", backend="open_clip", open_clip_model_id=identifier, dense_dim=8)
+ try:
+ adapter.load("cpu")
+ assert type(adapter._model.text.transformer) is BertModel
+ assert "forward" not in adapter._model.text.transformer.__dict__
+ assert adapter._model.text.transformer.config._attn_implementation == expected.config._attn_implementation
+ with torch.inference_mode():
+ assert torch.equal(
+ adapter._model.text.transformer(tokens).last_hidden_state, expected(tokens).last_hidden_state
+ )
+ kernel_boundary.assert_not_called()
+ finally:
+ adapter.unload()
+
+
+@pytest.mark.parametrize(
+ "module",
+ ["sie_server.adapters.remote.sie", "sie_server.adapters._transformers_compat"],
+)
+def test_optional_paths_do_not_import_transformers(module: str, tmp_path: Path) -> None:
+ script = """
+import importlib, importlib.abc, sys
+class DenyTransformers(importlib.abc.MetaPathFinder):
+ def find_spec(self, fullname, path=None, target=None):
+ if fullname == 'transformers' or fullname.startswith('transformers.'):
+ raise AssertionError('Optional path imported Transformers')
+sys.meta_path.insert(0, DenyTransformers())
+importlib.import_module(sys.argv[1])
+assert not any(name == 'transformers' or name.startswith('transformers.') for name in sys.modules)
+"""
+ result = subprocess.run( # noqa: S603 — fixed interpreter, inline script and parametrized internal module
+ [sys.executable, "-c", script, module],
+ cwd=tmp_path,
+ env={**os.environ, "PYTHONDONTWRITEBYTECODE": "1"},
+ capture_output=True,
+ text=True,
+ timeout=30,
+ check=False,
+ )
+ assert result.returncode == 0, result.stdout + result.stderr
+
+
+def test_actual_ctranslate2_import_adds_no_model_loading_guard(tmp_path: Path) -> None:
+ # CT2 already imports AutoTokenizer. Its actual adapter must not acquire a
+ # native model-loading policy merely by importing the shared base classes.
+ script = """
+import importlib.abc, sys
+class DenyCompat(importlib.abc.MetaPathFinder):
+ def find_spec(self, fullname, path=None, target=None):
+ if fullname == 'sie_server.adapters._transformers_compat':
+ raise AssertionError('CT2 imported a native model-loading guard')
+sys.meta_path.insert(0, DenyCompat())
+from sie_server.adapters.ctranslate2.generation import CTranslate2GenerationAdapter, AutoTokenizer
+assert CTranslate2GenerationAdapter.__module__ == 'sie_server.adapters.ctranslate2.generation'
+assert AutoTokenizer.__module__ == 'transformers.models.auto.tokenization_auto'
+assert 'sie_server.adapters._transformers_compat' not in sys.modules
+"""
+ result = subprocess.run( # noqa: S603 — fixed interpreter and inline script
+ [sys.executable, "-c", script],
+ cwd=tmp_path,
+ env={**os.environ, "PYTHONDONTWRITEBYTECODE": "1"},
+ capture_output=True,
+ text=True,
+ timeout=30,
+ check=False,
+ )
+ assert result.returncode == 0, result.stdout + result.stderr
+
+
+@pytest.mark.parametrize("local_directory", [False, True])
+def test_actual_native_open_clip_stays_available_without_transformers(tmp_path: Path, local_directory: bool) -> None:
+ pytest.importorskip("open_clip", minversion="3.0.0" if local_directory else None)
+ script = """
+import importlib.machinery, importlib.util, json, pathlib, sys
+# Model an absent optional package through actual import discovery in this
+# subprocess; do not suppress a discoverable-but-broken package's errors.
+class WithoutTransformers(importlib.machinery.PathFinder):
+ @classmethod
+ def find_spec(self, fullname, path=None, target=None):
+ if fullname == 'transformers' or fullname.startswith('transformers.'):
+ return None
+ return super().find_spec(fullname, path, target)
+sys.meta_path[:] = [WithoutTransformers if x is importlib.machinery.PathFinder else x for x in sys.meta_path]
+assert importlib.util.find_spec('transformers') is None
+import open_clip, torch
+from sie_server.adapters.siglip.adapter import SiglipAdapter
+torch.set_num_threads(1)
+config = {
+ 'embed_dim': 8,
+ 'vision_cfg': {'layers': 1, 'width': 32, 'head_width': 16, 'patch_size': 4, 'image_size': 8},
+ 'text_cfg': {'context_length': 8, 'vocab_size': 49408, 'width': 16, 'heads': 2, 'layers': 1},
+}
+identifier = 'owned-native'
+if sys.argv[1] == 'local':
+ directory = pathlib.Path.cwd() / 'nested' / 'native'
+ directory.mkdir(parents=True)
+ (directory / 'open_clip_config.json').write_text(json.dumps({'model_cfg': config}))
+ identifier = f'local-dir:{directory}'
+else:
+ open_clip.factory._MODEL_CONFIGS[identifier] = config
+adapter = SiglipAdapter('owned', backend='open_clip', open_clip_model_id=identifier, dense_dim=8, compute_precision='float32')
+try:
+ adapter.load('cpu')
+ assert type(adapter._model) is open_clip.model.CLIP
+ with torch.inference_mode():
+ result = adapter._model.encode_text(open_clip.tokenize(['hello'], context_length=8))
+ assert result.shape == (1, 8) and torch.isfinite(result).all()
+ assert not any(n == 'transformers' or n.startswith('transformers.') for n in sys.modules)
+finally:
+ adapter.unload()
+"""
+ result = subprocess.run( # noqa: S603 — fixed interpreter and inline offline native fixture
+ [sys.executable, "-c", script, "local" if local_directory else "registered"],
+ cwd=tmp_path,
+ env={**os.environ, "PYTHONDONTWRITEBYTECODE": "1"},
+ capture_output=True,
+ text=True,
+ timeout=30,
+ check=False,
+ )
+ assert result.returncode == 0, result.stdout + result.stderr
diff --git a/packages/sie_server/tests/config/test_native_zerank_catalog.py b/packages/sie_server/tests/config/test_native_zerank_catalog.py
index 600c6cfef..f176886fb 100644
--- a/packages/sie_server/tests/config/test_native_zerank_catalog.py
+++ b/packages/sie_server/tests/config/test_native_zerank_catalog.py
@@ -29,7 +29,7 @@ def test_native_catalog_pin_profile_and_bundle() -> None:
bundle = yaml.safe_load((_SERVER / "bundles/default.yaml").read_text())
assert _ADAPTER in bundle["adapters"]
assert bundle["deps"]["sentence-transformers"] == "==5.7.0"
- assert bundle["deps"]["transformers"] == ">=4.57,<5"
+ assert bundle["deps"]["transformers"] == "==4.57.6"
assert bundle["deps"]["gliner2"] == ">=1.3.2,<=2.0.0"
diff --git a/packages/sie_server/tests/config/test_transformers_dependency_policy.py b/packages/sie_server/tests/config/test_transformers_dependency_policy.py
new file mode 100644
index 000000000..bfcba9a98
--- /dev/null
+++ b/packages/sie_server/tests/config/test_transformers_dependency_policy.py
@@ -0,0 +1,63 @@
+"""Declared deployments match the reviewed Transformers loading boundary."""
+
+import tomllib
+from pathlib import Path
+
+import pytest
+import yaml
+from packaging.requirements import Requirement
+from packaging.specifiers import SpecifierSet
+
+_SERVER = Path(__file__).resolve().parents[2]
+_ROOT = _SERVER.parents[1]
+
+
+def _requirement(values: list[str]) -> Requirement:
+ matches = [Requirement(value) for value in values if Requirement(value).name == "transformers"]
+ assert len(matches) == 1
+ return matches[0]
+
+
+def test_default_local_and_resolved_tf4_are_the_exact_reviewed_version() -> None:
+ project = tomllib.loads((_SERVER / "pyproject.toml").read_text())["project"]
+ default = yaml.safe_load((_SERVER / "bundles/default.yaml").read_text())
+ locked = tomllib.loads((_ROOT / "uv.lock").read_text())
+ runtime = [package for package in locked["package"] if package["name"] == "transformers"]
+ assert len(runtime) == 1
+ assert runtime[0]["version"] == "4.57.6"
+ assert str(_requirement(project["optional-dependencies"]["local"]).specifier) == "==4.57.6"
+ assert default["deps"]["transformers"] == "==4.57.6"
+ assert "4.57.6" in _requirement(project["dependencies"]).specifier
+
+
+@pytest.mark.parametrize("version", ["4.57.0", "4.57.5", "5.0.0", "5.0.9", "5.1.9", "5.2.9", "6.0.0"])
+def test_server_excludes_known_unreviewed_runtime_ranges(version: str) -> None:
+ project = tomllib.loads((_SERVER / "pyproject.toml").read_text())["project"]
+ assert version not in _requirement(project["dependencies"]).specifier
+
+
+def test_server_keeps_fixed_tf5_overlays_and_documents_future_tf4_limit() -> None:
+ text = (_SERVER / "pyproject.toml").read_text()
+ project = tomllib.loads(text)["project"]
+ requirement = _requirement(project["dependencies"])
+ assert requirement.specifier == SpecifierSet(">=4.57.6,<6,!=5.0.*,!=5.1.*,!=5.2.*")
+ for version in ("4.57.6", "5.3.0", "5.17.0"):
+ assert version in requirement.specifier
+ # Metadata cannot represent the disjoint support set. The permanent loader
+ # guard, not this declaration, refuses future unreviewed 4.x versions.
+ assert "4.58.0" in requirement.specifier
+ assert "any future unreviewed 4.x version" in text
+ tf5 = yaml.safe_load((_SERVER / "bundles/transformers5.yaml").read_text())
+ assert tf5["deps"]["transformers"] == ">=5.17,<6"
+
+
+def test_lock_metadata_preserves_the_declared_base_and_local_requirements() -> None:
+ project = tomllib.loads((_SERVER / "pyproject.toml").read_text())["project"]
+ locked = tomllib.loads((_ROOT / "uv.lock").read_text())
+ server = next(package for package in locked["package"] if package["name"] == "sie-server")
+ requirements = [item for item in server["metadata"]["requires-dist"] if item["name"] == "transformers"]
+ base = [item for item in requirements if "marker" not in item]
+ local = [item for item in requirements if item.get("marker") == "extra == 'local'"]
+ assert len(base) == len(local) == 1
+ assert SpecifierSet(base[0]["specifier"]) == _requirement(project["dependencies"]).specifier
+ assert SpecifierSet(local[0]["specifier"]) == _requirement(project["optional-dependencies"]["local"]).specifier
diff --git a/uv.lock b/uv.lock
index 6ca7db56a..1cc73efa8 100644
--- a/uv.lock
+++ b/uv.lock
@@ -4968,8 +4968,8 @@ requires-dist = [
{ name = "torch", marker = "sys_platform == 'linux'", specifier = ">=2.9,<2.10", index = "https://download.pytorch.org/whl/cu129" },
{ name = "torchvision", marker = "sys_platform != 'linux'", specifier = ">=0.18,<1" },
{ name = "torchvision", marker = "sys_platform == 'linux'", specifier = ">=0.18,<1", index = "https://download.pytorch.org/whl/cu129" },
- { name = "transformers", specifier = ">=4.57,<6" },
- { name = "transformers", marker = "extra == 'local'", specifier = ">=4.57,<5" },
+ { name = "transformers", specifier = ">=4.57.6,!=5.0.*,!=5.1.*,!=5.2.*,<6" },
+ { name = "transformers", marker = "extra == 'local'", specifier = "==4.57.6" },
{ name = "typer", specifier = ">=0.14,<1" },
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.32,<1" },
{ name = "uvloop", specifier = ">=0.22" },