diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 490ba27e..34e81487 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,36 +6,64 @@ on: paths: - "**.py" - "pyproject.toml" + - "uv.lock" - ".github/workflows/test.yml" pull_request: branches: [main] paths: - "**.py" - "pyproject.toml" + - "uv.lock" - ".github/workflows/test.yml" +permissions: + contents: read + jobs: - test: - name: Renderers + unit: + name: Offline (Python ${{ matrix.python-version }}) runs-on: ubuntu-latest + env: + HF_HUB_OFFLINE: "1" + TRANSFORMERS_OFFLINE: "1" strategy: fail-fast: false matrix: python-version: ["3.11", "3.12", "3.13", "3.14"] - steps: - uses: actions/checkout@v4 - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 + - uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} + - uses: astral-sh/setup-uv@v7 + - run: uv sync --locked + - run: uv run --no-sync pytest tests/ -m "not model" -v - - name: Install uv - uses: astral-sh/setup-uv@v7 - - - name: Install dependencies - run: uv sync - - - name: Run tests - run: uv run pytest tests/ -v + model: + name: Pinned ${{ matrix.suite }} parity + runs-on: ubuntu-latest + timeout-minutes: 45 + strategy: + fail-fast: false + matrix: + include: + - suite: text + selection: "model and not multimodal" + - suite: multimodal + selection: "model and multimodal" + env: + HF_HUB_DISABLE_PROGRESS_BARS: "1" + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v6 + with: + python-version: "3.13" + - uses: astral-sh/setup-uv@v7 + - uses: actions/cache@v4 + with: + path: | + ~/.cache/huggingface/hub + ~/.cache/huggingface/modules + key: hf-renderers-${{ runner.os }}-${{ matrix.suite }}-${{ hashFiles('tests/model_assets.py') }} + - run: uv sync --locked + - run: uv run --no-sync pytest tests/ -m "${{ matrix.selection }}" -v diff --git a/pyproject.toml b/pyproject.toml index 09daae21..3fac356b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -139,3 +139,9 @@ invalid-key = "warn" unsupported-operator = "warn" invalid-return-type = "warn" invalid-assignment = "warn" + +[tool.pytest.ini_options] +markers = [ + "model: requires real tokenizer or processor assets at pinned revisions", + "multimodal: exercises real image/audio/video processors or model sidecars", +] diff --git a/renderers/base.py b/renderers/base.py index f3418841..d7ce49ad 100644 --- a/renderers/base.py +++ b/renderers/base.py @@ -1313,13 +1313,14 @@ def _load_tokenizer_via_auto(model_name_or_path: str, **kwargs) -> Any: return tok -def load_tokenizer(model_name_or_path: str): +def load_tokenizer(model_name_or_path: str, *, revision: str | None = None): """Load a tokenizer with the renderers-package security policy. Default ``trust_remote_code=False``. Models listed in ``TRUSTED_REVISIONS`` (Moonshot Kimi-K2 family) load with ``trust_remote_code=True`` AND a pinned ``revision=`` so transformers only executes the reviewed commit's tokenizer Python. + ``revision`` pins ordinary assets without overriding reviewed remote code. ``AutoTokenizer.from_pretrained`` eagerly builds the model config to resolve the tokenizer class. If that construction raises on a @@ -1338,6 +1339,11 @@ def load_tokenizer(model_name_or_path: str): _require_transformers("Loading a tokenizer") load_name_or_path = _tokenizer_source_for(model_name_or_path) kwargs = _tokenizer_load_kwargs(load_name_or_path) + if revision is not None: + trusted_revision = kwargs.get("revision") + if trusted_revision is not None and revision != trusted_revision: + raise ValueError("Cannot override the reviewed remote-code revision") + kwargs["revision"] = revision tok = _load_tokenizer_via_auto(load_name_or_path, **kwargs) return _preserve_requested_tokenizer_name( tok, diff --git a/tests/conftest.py b/tests/conftest.py index 7b9f9732..cce8b596 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,9 +12,10 @@ import pytest from parity import models_for + from renderers import create_renderer -from renderers.base import load_tokenizer from renderers.configs import config_from_name +from tests.model_assets import load_test_tokenizer # Backwards-compatible view used by documentation and a few invariant tests. RENDERER_MODELS = [(case.model, case.renderer) for case in models_for("shared")] @@ -33,7 +34,7 @@ def release_shared_models(): def _load(model_name: str, renderer_name: str): key = f"{model_name}:{renderer_name}" if key not in _cache: - tokenizer = load_tokenizer(model_name) + tokenizer = load_test_tokenizer(model_name) renderer = create_renderer(tokenizer, config_from_name(renderer_name)) _cache[key] = (tokenizer, renderer) return _cache[key] @@ -54,7 +55,10 @@ def pytest_generate_tests(metafunc): cases = models_for(suite) metafunc.parametrize( "model_name,renderer_name", - [(case.model, case.renderer) for case in cases], + [ + pytest.param(case.model, case.renderer, marks=pytest.mark.model) + for case in cases + ], ids=[case.model for case in cases], ) diff --git a/tests/model_assets.py b/tests/model_assets.py new file mode 100644 index 00000000..bdac1f6c --- /dev/null +++ b/tests/model_assets.py @@ -0,0 +1,94 @@ +"""Immutable test assets; Llama revisions refer to the audited tokenizer mirrors.""" + +from pathlib import Path + +from renderers.base import load_tokenizer + +MODEL_REVISIONS = { + "MiniMaxAI/MiniMax-M2.5": "2515a3ea0afec1dd22d0c7794b3a91557804d195", + "PrimeIntellect/Qwen3-0.6B": "15aba67c8ec68aeac96f57ba7e31d373564ee03c", + "PrimeIntellect/Qwen3-1.7B": "61a79cd101170642a653d1d44d18f69f88f54e08", + "Qwen/Qwen2.5-0.5B-Instruct": "7ae557604adf67be50417f59c2c2f167def9a775", + "Qwen/Qwen3-0.6B": "c1899de289a04d12100db370d81485cdf75e47ca", + "Qwen/Qwen3-4B": "1cfa9a7208912126459214e8b04321603b3df60c", + "Qwen/Qwen3-8B": "b968826d9c46dd6066d109eabc6255188de91218", + "Qwen/Qwen3-VL-30B-A3B-Instruct": "9c4b90e1e4ba969fd3b5378b57d966d725f1b86c", + "Qwen/Qwen3-VL-4B-Instruct": "ebb281ec70b05090aa6165b016eac8ec08e71b17", + "Qwen/Qwen3-VL-8B-Instruct": "0c351dd01ed87e9c1b53cbc748cba10e6187ff3b", + "Qwen/Qwen3.5-0.8B": "2fc06364715b967f1860aea9cf38778875588b17", + "Qwen/Qwen3.5-122B-A10B": "dc4d348443bc740c68e2d77492492c11606384d5", + "Qwen/Qwen3.5-2B": "15852e8c16360a2fea060d615a32b45270f8a8fc", + "Qwen/Qwen3.5-35B-A3B": "59d61f3ce65a6d9863b86d2e96597125219dc754", + "Qwen/Qwen3.5-397B-A17B": "8472618112abcbd45acbcdc58436aff4233c23f7", + "Qwen/Qwen3.5-4B": "851bf6e806efd8d0a36b00ddf55e13ccb7b8cd0a", + "Qwen/Qwen3.5-9B": "c202236235762e1c871ad0ccb60c8ee5ba337b9a", + "Qwen/Qwen3.6-35B-A3B": "995ad96eacd98c81ed38be0c5b274b04031597b0", + "Qwen/Qwen3.8-27B": "1d4bf0f2ff6012fd82039f2fa52739d0dd7c60c0", + "Qwen/Qwen3.8-Flash-Next": "de4b8e4d43b917e7706784d8bb445c9af86a3540", + "THUDM/GLM-4.5-Air": "a24ceef6ce4f3536971efe9b778bdaa1bab18daa", + "deepseek-ai/DeepSeek-R1": "56d4cbbb4d29f4355bab4b9a39ccb717a14ad5ad", + "deepseek-ai/DeepSeek-V3": "e815299b0bcbac849fa540c768ef21845365c9eb", + "deepseek-ai/DeepSeek-V4-Flash-0731": "7872f01b1d1fe23eabc4c98b48bffcef5a386062", + "google/gemma-4-26B-A4B-it": "4d7ae4984b7db7de8f8457170b3f1a419ee76d52", + "google/gemma-4-31B-it": "842da3794eaa0b77d5f08bae87a17459d91ff475", + "google/gemma-4-E2B-it": "3e22461f65e89153144f8adb70e3b8c2cc9845a7", + "google/gemma-4-E4B-it": "ee0ef6023621cff504d758262d4e04895a5af4a2", + "meta-llama/Llama-3.2-1B-Instruct": "5a8abab4a5d6f164389b1079fb721cfab8d7126c", + "meta-llama/Llama-3.2-3B-Instruct": "006f5dcd1393c3add266de40994ba96225e9689d", + "meta-models/Muse-Glimmer-30B": "cec94fcfd6512f2cfe708138c32533b346ea4f69", + "moonshotai/Kimi-K2-Instruct": "fd1984e2b7a3350dbf7305fe73a4ede25c14de50", + "moonshotai/Kimi-K2.5": "4d01dfe0332d63057c186e0b262165819efb6611", + "moonshotai/Kimi-K2.6": "2755962d07cb42aa2d988a35bcb65cd4a9c2de82", + "nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16": "bf77c3174f68ad409e1c2aa60daeb46e32d1c606", + "nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-BF16": "2dc98e2afe4face0e4ce40972a915c45368bd34a", + "nvidia/NVIDIA-Nemotron-3-Ultra-550B-A55B-BF16": "77df655d5e9f8362164ed14dd8b48f8bce657498", + "nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-BF16": "a9904d24bcc1d289a1950fa9d2b978c47cf903b9", + "openai/gpt-oss-120b": "b5c939de8f754692c1647ca79fbf85e8c1e70f8a", + "openai/gpt-oss-20b": "6cee5e81ee83917806bbde320786a8fb61efebee", + "poolside/Laguna-M.1": "2bf8a4ab6d2e8e1e0d92a41318532a0fee1640d6", + "poolside/Laguna-S-2.1": "0f573140834b11cfac0c2af97a101a7a69a13e22", + "poolside/Laguna-XS-2.1": "c5f36269bbdbd3f27fddc9a9f9dbae0cf2cf57db", + "poolside/Laguna-XS.2": "a397bde04501591d0ac3d74a5d9d0a4ec298ffb8", + "tencent/Hy3": "a960ebc3da325ba167f069f76c41eb62c9280d22", + "thinkingmachines/Inkling": "828496eeae4c243ff1a22f7f28ff83694f2f7bc9", + "thinkingmachines/Inkling-Small": "8cc5877b44d343f88b92086aa1fb72897950f06a", + "zai-org/GLM-4.7-Flash": "7dd20894a642a0aa287e9827cb1a1f7f91386b67", + "zai-org/GLM-5": "c183ef8c61faee82855eca1ed9bb3a9a7ce3b0b2", + "zai-org/GLM-5.1": "26e1bd6e011feb778d25ae34b09b07074139d92d", + "zai-org/GLM-5.3": "ba32f8c19c8738e2c57c71798c601d477a7bf844", + "zai-org/GLM-5.3-BF16": "abf42e6c0e53bc29a875511301e7b7fcd220315b", + "zai-org/GLM-5.3-Flash-BF16": "f9fed3bbc82291073373d67452e9f25317ef7577", +} + + +def model_revision(model: str) -> str: + # The optional private VLM fixture can only resolve an existing local snapshot. + if model == "nvidia/NVIDIA-Nemotron-3.5-Super-EA-09112026": + from huggingface_hub import try_to_load_from_cache + + path = try_to_load_from_cache(model, "tokenizer_config.json") + if isinstance(path, str): + return Path(path).parent.name + try: + return MODEL_REVISIONS[model] + except KeyError as exc: + raise KeyError(f"No immutable test revision registered for {model!r}") from exc + + +def load_test_tokenizer(model: str): + return load_tokenizer(model, revision=model_revision(model)) + + +def load_test_processor(model: str): + from transformers import AutoProcessor + + return AutoProcessor.from_pretrained( + model, + revision=model_revision(model), + trust_remote_code=model + in { + "moonshotai/Kimi-K2.5", + "moonshotai/Kimi-K2.6", + "nvidia/NVIDIA-Nemotron-3.5-Super-EA-09112026", + }, + ) diff --git a/tests/test_bridge.py b/tests/test_bridge.py index af5eaf99..a49374e1 100644 --- a/tests/test_bridge.py +++ b/tests/test_bridge.py @@ -27,8 +27,8 @@ @lru_cache(maxsize=None) def _load(model_name: str, renderer_name: str): from renderers import create_renderer - from renderers.base import load_tokenizer from renderers.configs import config_from_name + from tests.model_assets import load_test_tokenizer as load_tokenizer tok = load_tokenizer(model_name) return tok, create_renderer(tok, config_from_name(renderer_name)) @@ -38,7 +38,10 @@ def pytest_generate_tests(metafunc): if "br_model" in metafunc.fixturenames: metafunc.parametrize( "br_model,br_renderer_name", - _BRIDGE_MODELS, + [ + pytest.param(model, renderer, marks=pytest.mark.model) + for model, renderer in _BRIDGE_MODELS + ], ids=[m for m, _ in _BRIDGE_MODELS], ) @@ -223,6 +226,7 @@ def test_qwen3_renders_openai_text_content_parts(br_renderer, br_tokenizer, br_m assert text in br_tokenizer.decode(list_ids, skip_special_tokens=False) +@pytest.mark.model def test_bridge_declines_across_user_query_when_template_drops_thinking(): """Qwen3's template drops a past block's thinking once a new user turn arrives. The resolved ``tool_cycle`` bridge policy therefore treats a @@ -237,8 +241,8 @@ def test_bridge_declines_across_user_query_when_template_drops_thinking(): - no prior thinking + new user query -> decline; no marker lookback. """ from renderers import create_renderer - from renderers.base import load_tokenizer from renderers.configs import Qwen3RendererConfig + from tests.model_assets import load_test_tokenizer as load_tokenizer tok = load_tokenizer("Qwen/Qwen3-8B") im_end = tok.convert_tokens_to_ids("<|im_end|>") @@ -326,14 +330,15 @@ def test_bridge_declines_across_user_turn_when_thinking_present(br_renderer, br_ assert extended is not None, f"{br_model}: should still bridge within a tool cycle" +@pytest.mark.model def test_bridge_keeps_thinking_when_history_kwarg_disables_truncation(): """GLM ``clear_thinking=False`` / Nemotron ``truncate_history_thinking= False`` keep all past thinking (the template doesn't strip it), so the bridge must NOT decline across a user turn — declining would re-render and re-tokenize model-sampled thinking bytes.""" from renderers import create_renderer - from renderers.base import load_tokenizer from renderers.configs import GLM5RendererConfig, Nemotron3RendererConfig + from tests.model_assets import load_test_tokenizer as load_tokenizer asst = [ {"role": "assistant", "reasoning_content": "Let me think.", "content": "Hi"} diff --git a/tests/test_client.py b/tests/test_client.py index 3b53fe25..b85a6962 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -738,6 +738,8 @@ def test_generate_threads_prompt_attribution_through_prebuilt_prompt_path(): # --------------------------------------------------------------------------- +@pytest.mark.model +@pytest.mark.multimodal @pytest.mark.parametrize( "model_id,renderer_class_path", [ @@ -762,11 +764,8 @@ def test_generate_serializes_multimodal_features_for_qwen_vl_family( import torch as _torch - from renderers.base import ( - MultiModalData, - PlaceholderRange, - load_tokenizer, - ) + from renderers.base import MultiModalData, PlaceholderRange + from tests.model_assets import load_test_tokenizer as load_tokenizer mod_name, cls_name = renderer_class_path.split(":") renderer_cls = getattr(importlib.import_module(mod_name), cls_name) @@ -832,6 +831,8 @@ def test_generate_serializes_multimodal_features_for_qwen_vl_family( assert isinstance(item, str) and len(item) > 0 +@pytest.mark.model +@pytest.mark.multimodal def test_generate_serializes_multimodal_features_for_gemma4(): """Gemma 4's HF image positions are translated to vLLM's field name.""" pytest.importorskip("torch") @@ -839,8 +840,9 @@ def test_generate_serializes_multimodal_features_for_gemma4(): import torch as _torch - from renderers.base import MultiModalData, PlaceholderRange, load_tokenizer + from renderers.base import MultiModalData, PlaceholderRange from renderers.gemma4 import Gemma4Renderer + from tests.model_assets import load_test_tokenizer as load_tokenizer renderer = Gemma4Renderer(load_tokenizer("google/gemma-4-31B-it")) mm_data = MultiModalData( diff --git a/tests/test_custom.py b/tests/test_custom.py index c07da1b9..bc755207 100644 --- a/tests/test_custom.py +++ b/tests/test_custom.py @@ -169,8 +169,9 @@ def _emit_generation_prompt(self, emit_special) -> None: ) +@pytest.mark.model def test_readme_example_prefills_the_think_block(tmp_path): - from renderers.base import load_tokenizer + from tests.model_assets import load_test_tokenizer as load_tokenizer path = tmp_path / "glm53_no_thinking.py" path.write_text(GLM53_NO_THINKING_SOURCE) diff --git a/tests/test_deepseek_r1.py b/tests/test_deepseek_r1.py index 41d03e51..d3b992f7 100644 --- a/tests/test_deepseek_r1.py +++ b/tests/test_deepseek_r1.py @@ -7,12 +7,16 @@ from functools import lru_cache +import pytest + from renderers import ( DeepSeekR1Renderer, DeepSeekV3Renderer, create_renderer, ) -from renderers.base import load_tokenizer +from tests.model_assets import load_test_tokenizer as load_tokenizer + +pytestmark = pytest.mark.model @lru_cache diff --git a/tests/test_deepseek_v4.py b/tests/test_deepseek_v4.py index 4a6b4138..f4207a3f 100644 --- a/tests/test_deepseek_v4.py +++ b/tests/test_deepseek_v4.py @@ -6,6 +6,7 @@ import pytest from pydantic import TypeAdapter, ValidationError + from renderers import ( DeepSeekV4Renderer, DeepSeekV4RendererConfig, @@ -13,9 +14,12 @@ ToolCallParseStatus, create_renderer, ) -from renderers.base import MODEL_RENDERER_MAP, load_tokenizer +from renderers.base import MODEL_RENDERER_MAP +from tests.model_assets import load_test_tokenizer as load_tokenizer from tests.reference_rendering import render_reference +pytestmark = pytest.mark.model + MODEL = "deepseek-ai/DeepSeek-V4-Flash-0731" BOS = "<|begin▁of▁sentence|>" EOS = "<|end▁of▁sentence|>" diff --git a/tests/test_disabled_thinking_stability.py b/tests/test_disabled_thinking_stability.py index c6663ecd..dfd8b063 100644 --- a/tests/test_disabled_thinking_stability.py +++ b/tests/test_disabled_thinking_stability.py @@ -24,10 +24,14 @@ from functools import lru_cache +import pytest from parity import models_for + from renderers import create_renderer -from renderers.base import load_tokenizer from renderers.configs import _config_class_for +from tests.model_assets import load_test_tokenizer as load_tokenizer + +pytestmark = pytest.mark.model # One representative model per affected renderer family, each with # thinking explicitly disabled. diff --git a/tests/test_gemma4.py b/tests/test_gemma4.py index 3360e07f..7d0edf3b 100644 --- a/tests/test_gemma4.py +++ b/tests/test_gemma4.py @@ -6,8 +6,12 @@ from parity import models_for from renderers import Gemma4Renderer, create_renderer -from renderers.base import MODEL_RENDERER_MAP, MULTIMODAL_MODELS, load_tokenizer +from renderers.base import MODEL_RENDERER_MAP, MULTIMODAL_MODELS from renderers.configs import Gemma4RendererConfig +from tests.model_assets import load_test_processor +from tests.model_assets import load_test_tokenizer as load_tokenizer + +pytestmark = pytest.mark.model _MODELS = tuple(case.model for case in models_for("gemma-checkpoints")) @@ -298,7 +302,9 @@ def test_parser_recovers_prompt_opened_post_tool_reasoning(): def _image_renderer(): """A Gemma 4 renderer with a live ``Gemma4Processor``, or a skip.""" tokenizer, _ = _gemma4() - renderer = Gemma4Renderer(tokenizer) + renderer = Gemma4Renderer( + tokenizer, processor=load_test_processor("google/gemma-4-31B-it") + ) try: renderer._get_processor() except (RuntimeError, OSError) as exc: # pragma: no cover - env dependent @@ -312,6 +318,7 @@ def _tiny_image(): return Image.new("RGB", (224, 224), color=(128, 192, 255)) +@pytest.mark.multimodal @pytest.mark.parametrize("size", [(224, 224), (448, 224), (224, 448)]) def test_real_processor_keeps_one_batched_row_per_image(size): """Guard the ``MultiModalFieldConfig.batched('image')`` contract with @@ -332,6 +339,7 @@ def test_real_processor_keeps_one_batched_row_per_image(size): assert placeholder.length > 0 +@pytest.mark.multimodal def test_schema_unified_image_parts_still_expand_image_tokens(): """``Dataset.from_list`` unifies the Arrow schema across a content list, so an image part round-tripped through a dataset carries ``text: None`` @@ -374,6 +382,7 @@ def test_schema_unified_image_parts_still_expand_image_tokens(): ) +@pytest.mark.multimodal def test_schema_unified_tool_response_image_parts_survive(): """Same hazard on the tool-response path, which also has to keep accepting untyped text parts.""" diff --git a/tests/test_glm53_flash.py b/tests/test_glm53_flash.py index bf94b4a2..514c7cf3 100644 --- a/tests/test_glm53_flash.py +++ b/tests/test_glm53_flash.py @@ -19,6 +19,8 @@ from renderers.client import generate +pytestmark = pytest.mark.model + MODEL = "zai-org/GLM-5.3-Flash-BF16" GLM53_MODELS = ["zai-org/GLM-5.3", MODEL] @@ -45,7 +47,7 @@ @lru_cache(maxsize=None) def _load(model=MODEL): from renderers import create_renderer - from renderers.base import load_tokenizer + from tests.model_assets import load_test_tokenizer as load_tokenizer tok = load_tokenizer(model) return tok, create_renderer(tok) diff --git a/tests/test_glm_tool_name_validation.py b/tests/test_glm_tool_name_validation.py index 176db262..ed97efdf 100644 --- a/tests/test_glm_tool_name_validation.py +++ b/tests/test_glm_tool_name_validation.py @@ -25,9 +25,13 @@ from functools import lru_cache +import pytest from parity import models_for + from renderers.base import ToolCallParseStatus +pytestmark = pytest.mark.model + # Both GLM renderers share ``parse_glm`` and are served by the same # strict vLLM parser (the ``glm45`` and ``glm47`` aliases both resolve # to ``Glm47MoeModelToolParser`` in vLLM ≥ 0.24). @@ -55,7 +59,7 @@ @lru_cache(maxsize=None) def _load(model: str, renderer_name: str): from renderers import config_from_name, create_renderer - from renderers.base import load_tokenizer + from tests.model_assets import load_test_tokenizer as load_tokenizer tok = load_tokenizer(model) return tok, create_renderer(tok, config_from_name(renderer_name)) diff --git a/tests/test_hy3.py b/tests/test_hy3.py index db8b0668..8f38abad 100644 --- a/tests/test_hy3.py +++ b/tests/test_hy3.py @@ -15,7 +15,10 @@ import pytest from renderers import Hy3RendererConfig, create_renderer -from renderers.base import ToolCallParseStatus, load_tokenizer +from renderers.base import ToolCallParseStatus +from tests.model_assets import load_test_tokenizer as load_tokenizer + +pytestmark = pytest.mark.model _MODEL = "tencent/Hy3" diff --git a/tests/test_inkling.py b/tests/test_inkling.py index 01caaca9..60948e2d 100644 --- a/tests/test_inkling.py +++ b/tests/test_inkling.py @@ -22,9 +22,12 @@ import pytest from renderers import create_renderer -from renderers.base import ToolCallParseStatus, load_tokenizer +from renderers.base import ToolCallParseStatus from renderers.configs import InklingRendererConfig from renderers.inkling import InklingRenderer +from tests.model_assets import load_test_tokenizer as load_tokenizer + +pytestmark = pytest.mark.model _MODEL = "thinkingmachines/Inkling" _SMALL_MODEL = "thinkingmachines/Inkling-Small" diff --git a/tests/test_laguna_m1.py b/tests/test_laguna_m1.py index dd9503fc..f91bb876 100644 --- a/tests/test_laguna_m1.py +++ b/tests/test_laguna_m1.py @@ -4,6 +4,7 @@ from functools import lru_cache +import pytest from pydantic import TypeAdapter from renderers import ( @@ -12,7 +13,9 @@ RendererConfig, create_renderer, ) -from renderers.base import load_tokenizer +from tests.model_assets import load_test_tokenizer as load_tokenizer + +pytestmark = pytest.mark.model _MODEL = "poolside/Laguna-M.1" diff --git a/tests/test_laguna_xs21.py b/tests/test_laguna_xs21.py index add22649..f9dbb130 100644 --- a/tests/test_laguna_xs21.py +++ b/tests/test_laguna_xs21.py @@ -22,9 +22,11 @@ import pytest from renderers import create_renderer -from renderers.base import load_tokenizer from renderers.configs import LagunaXS21RendererConfig, config_from_name from renderers.laguna_xs2 import LagunaXS21Renderer +from tests.model_assets import load_test_tokenizer as load_tokenizer + +pytestmark = pytest.mark.model _MODEL = "poolside/Laguna-XS-2.1" diff --git a/tests/test_llama_3.py b/tests/test_llama_3.py index 86959774..a9950937 100644 --- a/tests/test_llama_3.py +++ b/tests/test_llama_3.py @@ -15,11 +15,13 @@ from renderers import Llama3Renderer, Llama3RendererConfig, create_renderer from renderers.base import ( MODEL_RENDERER_MAP, - ParsedResponse, TOKENIZER_SOURCE_OVERRIDES, + ParsedResponse, ToolCallParseStatus, - load_tokenizer, ) +from tests.model_assets import load_test_tokenizer as load_tokenizer + +pytestmark = pytest.mark.model # Pinned date for byte-parity tests. Matches the chat template's # strftime fallback so we don't have to override on the apply side. diff --git a/tests/test_load_tokenizer.py b/tests/test_load_tokenizer.py index aa9855e2..fbfb2430 100644 --- a/tests/test_load_tokenizer.py +++ b/tests/test_load_tokenizer.py @@ -12,9 +12,11 @@ from types import SimpleNamespace from unittest.mock import patch +import pytest + from renderers import base from renderers.base import TOKENIZER_SOURCE_OVERRIDES, TRUSTED_REVISIONS, load_tokenizer - +from tests.model_assets import load_test_tokenizer # --------------------------------------------------------------------------- # Allow-list shape @@ -50,20 +52,32 @@ def test_trusted_revisions_are_full_shas(): # --------------------------------------------------------------------------- +@pytest.mark.parametrize("revision", [None, "a" * 40]) @patch("transformers.AutoTokenizer.from_pretrained") -def test_unlisted_model_loads_without_remote_code(mock_from_pretrained): - """Default path: trust_remote_code=False, no revision pin.""" - load_tokenizer("Qwen/Qwen3-0.6B") +def test_unlisted_model_loads_without_remote_code(mock_from_pretrained, revision): + """Ordinary assets can be pinned without enabling remote code.""" + load_tokenizer("Qwen/Qwen3-0.6B", revision=revision) args, kwargs = mock_from_pretrained.call_args assert args == ("Qwen/Qwen3-0.6B",) - assert kwargs == {"trust_remote_code": False} + expected = {"trust_remote_code": False} + if revision is not None: + expected["revision"] = revision + assert kwargs == expected +@pytest.mark.parametrize( + "revision", [None, TRUSTED_REVISIONS["moonshotai/Kimi-K2.5"], "a" * 40] +) @patch("transformers.AutoTokenizer.from_pretrained") -def test_kimi_loads_with_pinned_revision(mock_from_pretrained): +def test_kimi_loads_with_pinned_revision(mock_from_pretrained, revision): """Kimi-K2 family: trust_remote_code=True, revision pinned to the sha listed in TRUSTED_REVISIONS — never a branch name.""" - load_tokenizer("moonshotai/Kimi-K2.5") + if revision is not None and revision != TRUSTED_REVISIONS["moonshotai/Kimi-K2.5"]: + with pytest.raises(ValueError, match="reviewed remote-code revision"): + load_tokenizer("moonshotai/Kimi-K2.5", revision=revision) + mock_from_pretrained.assert_not_called() + return + load_tokenizer("moonshotai/Kimi-K2.5", revision=revision) args, kwargs = mock_from_pretrained.call_args assert args == ("moonshotai/Kimi-K2.5",) assert kwargs == { @@ -150,22 +164,24 @@ def __call__(self, *args, **kwargs): # --------------------------------------------------------------------------- +@pytest.mark.model def test_load_tokenizer_real_qwen_works_without_remote_code(): """End-to-end: an unlisted model loads successfully without trust_remote_code. Qwen tokenizers don't ship custom Python.""" - tok = load_tokenizer("Qwen/Qwen3-0.6B") + tok = load_test_tokenizer("Qwen/Qwen3-0.6B") assert tok is not None # Smoke: the tokenizer can encode a basic string. ids = tok.encode("hello", add_special_tokens=False) assert len(ids) > 0 +@pytest.mark.model def test_load_tokenizer_real_kimi_uses_pinned_revision(): """End-to-end: Kimi-K2.5 loads via the pinned-revision path. The parity tests already exercise this path — this test pins the contract that ``load_tokenizer`` is the only sanctioned entry point for the trusted-revision allow-list.""" - tok = load_tokenizer("moonshotai/Kimi-K2.5") + tok = load_test_tokenizer("moonshotai/Kimi-K2.5") assert tok is not None ids = tok.encode("hello", add_special_tokens=False) assert len(ids) > 0 diff --git a/tests/test_message_indices.py b/tests/test_message_indices.py index d6471e37..8f38217c 100644 --- a/tests/test_message_indices.py +++ b/tests/test_message_indices.py @@ -43,8 +43,8 @@ import pytest from renderers import build_training_sample -from renderers.glm45 import GLM45Renderer from renderers.glm5 import GLM5Renderer +from renderers.glm45 import GLM45Renderer from tests.reference_rendering import render_reference @@ -140,6 +140,7 @@ def test_message_indices_in_range(model_name, renderer): ) +@pytest.mark.model def test_kimi_k2_unknown_role_message_indices(): """Regression for the ``i`` vs ``oi`` mistake in ``KimiK2Renderer.render``'s unknown-role fallback. To surface, we @@ -159,7 +160,7 @@ def test_kimi_k2_unknown_role_message_indices(): caller list of length 2. """ from renderers import create_renderer - from renderers.base import load_tokenizer + from tests.model_assets import load_test_tokenizer as load_tokenizer tok = load_tokenizer("moonshotai/Kimi-K2-Instruct") renderer = create_renderer(tok) diff --git a/tests/test_multimodal.py b/tests/test_multimodal.py index aadcf152..3d8aad79 100644 --- a/tests/test_multimodal.py +++ b/tests/test_multimodal.py @@ -15,7 +15,7 @@ the combined message list. Tests skip per-pair when: -- The HF snapshot isn't cached locally (network-free CI mode). +- Optional private model metadata is unavailable locally. - The model lists a modality the renderer doesn't yet support (``NotImplementedError`` in ``render``). - ``Pillow`` / ``torch`` are missing. @@ -23,9 +23,6 @@ from __future__ import annotations -import os -from pathlib import Path - import pytest from renderers import ( @@ -33,8 +30,10 @@ Qwen3VLRenderer, create_renderer, ) -from renderers.base import MODEL_RENDERER_MAP, load_tokenizer +from renderers.base import MODEL_RENDERER_MAP from renderers.configs import _config_class_for +from tests.model_assets import load_test_processor, model_revision +from tests.model_assets import load_test_tokenizer as load_tokenizer def _config_for_model(model_name: str, **kwargs): @@ -55,28 +54,25 @@ def _config_with_add_vision_id(model_name: str, add_vision_id: bool): import numpy as np # noqa: E402 from PIL import Image # noqa: E402 +pytestmark = [pytest.mark.model, pytest.mark.multimodal] + # --------------------------------------------------------------------------- -# Local-snapshot gating — skip when the HF cache doesn't have the model. +# Optional private metadata stays cache-only; public assets are pinned. # --------------------------------------------------------------------------- -def _hf_snapshot_cached(model_name: str) -> bool: - """True iff the HF hub cache has at least one snapshot for ``model_name``. +def _model_assets_available(model_name: str) -> bool: + """Download pinned public assets; keep optional private metadata cache-only.""" + if model_name != "nvidia/NVIDIA-Nemotron-3.5-Super-EA-09112026": + model_revision(model_name) + return True - Avoids pulling weights / configs over the network during test runs. - Mirrors the convention used elsewhere in this repo (test_qwen35_size_coverage) - of relying on the user having pre-fetched relevant models. - """ - cache = ( - Path(os.environ.get("HF_HOME") or Path.home() / ".cache" / "huggingface") - / "hub" - ) - safe = "models--" + model_name.replace("/", "--") - snapshots = cache / safe / "snapshots" - if not snapshots.is_dir(): + try: + model_revision(model_name) + except KeyError: return False - return any(p.is_dir() for p in snapshots.iterdir()) + return True # --------------------------------------------------------------------------- @@ -104,36 +100,12 @@ def _modality_cases(): _loaded: dict[str, tuple] = {} -# Models whose processors need ``trust_remote_code=True`` (custom Python -# in the repo) and still use a pinned revision. Nemotron 3.5 is handled -# separately below so its selected model revision is not overridden. -_PROCESSOR_TRUSTED_REVISIONS: dict[str, str] = { - "moonshotai/Kimi-K2.5": "4d01dfe0332d63057c186e0b262165819efb6611", - "moonshotai/Kimi-K2.6": "2755962d07cb42aa2d988a35bcb65cd4a9c2de82", -} - - def _load_processor_and_renderer(model_name: str): if model_name not in _loaded: - from transformers import AutoProcessor - tokenizer = load_tokenizer(model_name) - revision = _PROCESSOR_TRUSTED_REVISIONS.get(model_name) - if model_name.startswith("nvidia/NVIDIA-Nemotron-3.5"): - processor = AutoProcessor.from_pretrained( - model_name, - trust_remote_code=True, - ) - elif revision is not None: - processor = AutoProcessor.from_pretrained( - model_name, - trust_remote_code=True, - revision=revision, - ) - else: - processor = AutoProcessor.from_pretrained(model_name) + processor = load_test_processor(model_name) renderer = create_renderer(tokenizer) - # Inject processor so the renderer doesn't try to fetch it lazily. + # Inject the pinned processor instead of lazily resolving the model again. if hasattr(renderer, "_processor") and renderer._processor is None: renderer._processor = processor _loaded[model_name] = (tokenizer, processor, renderer) @@ -626,8 +598,10 @@ def test_multimodal_byte_parity_vs_processor(mm_model_name, modality, tiny_image token ids regardless of whether the prompt is templated server-side (MITO) or rendered client-side via this package. """ - if not _hf_snapshot_cached(mm_model_name): - pytest.skip(f"{mm_model_name}: HF snapshot not cached locally") + if not _model_assets_available(mm_model_name): + pytest.skip( + f"{mm_model_name}: optional private model metadata not cached locally" + ) kit = _modality_kit(modality, mm_model_name) tokenizer, processor, renderer = _load_processor_and_renderer(mm_model_name) @@ -659,8 +633,10 @@ def test_multimodal_byte_parity_vs_processor(mm_model_name, modality, tiny_image ) def test_multimodal_placeholders_match_pad_runs(mm_model_name, modality, tiny_image): """``mm_placeholders`` exactly cover the runs of the modality's pad token.""" - if not _hf_snapshot_cached(mm_model_name): - pytest.skip(f"{mm_model_name}: HF snapshot not cached locally") + if not _model_assets_available(mm_model_name): + pytest.skip( + f"{mm_model_name}: optional private model metadata not cached locally" + ) kit = _modality_kit(modality, mm_model_name) tokenizer, _, renderer = _load_processor_and_renderer(mm_model_name) @@ -723,8 +699,10 @@ def test_multimodal_bridge_extends_and_carries_mm_data( the new turn's image, plus its placeholder is recorded with an absolute offset inside the bridged sequence. """ - if not _hf_snapshot_cached(mm_model_name): - pytest.skip(f"{mm_model_name}: HF snapshot not cached locally") + if not _model_assets_available(mm_model_name): + pytest.skip( + f"{mm_model_name}: optional private model metadata not cached locally" + ) kit = _modality_kit(modality, mm_model_name) tokenizer, processor, _ = _load_processor_and_renderer(mm_model_name) @@ -849,8 +827,8 @@ def test_inkling_bridge_does_not_mutate_prior_mm_data(tiny_image): the merge copies the per-modality lists, not just the outer dict, so a carried-forward image doesn't grow the prior turn's list in place.""" model = "thinkingmachines/Inkling" - if not _hf_snapshot_cached(model): - pytest.skip(f"{model}: HF snapshot not cached locally") + if not _model_assets_available(model): + pytest.skip(f"{model}: optional private model metadata not cached locally") _, processor, renderer = _load_processor_and_renderer(model) initial = [ @@ -899,7 +877,7 @@ def test_modality_registry_models_route_to_renderer(): via ``create_renderer(renderer='auto')``. Guards against drift between the multimodal registry and ``MODEL_RENDERER_MAP``.""" for model_name in MULTIMODAL_MODELS: - if not _hf_snapshot_cached(model_name): + if not _model_assets_available(model_name): continue tokenizer = load_tokenizer(model_name) renderer = create_renderer(tokenizer) @@ -925,8 +903,10 @@ def test_tool_response_image_byte_parity(mm_model_name, modality, tiny_image): """ if modality != "image": pytest.skip("Tool-response media path is image-only for now.") - if not _hf_snapshot_cached(mm_model_name): - pytest.skip(f"{mm_model_name}: HF snapshot not cached locally") + if not _model_assets_available(mm_model_name): + pytest.skip( + f"{mm_model_name}: optional private model metadata not cached locally" + ) kit = _modality_kit(modality, mm_model_name) tokenizer, processor, renderer = _load_processor_and_renderer(mm_model_name) @@ -1011,8 +991,10 @@ def test_add_vision_id_parity_vs_processor( ``processor.apply_chat_template(messages, add_vision_id=)`` token-for-token after image expansion. """ - if not _hf_snapshot_cached(mm_model_name): - pytest.skip(f"{mm_model_name}: HF snapshot not cached locally") + if not _model_assets_available(mm_model_name): + pytest.skip( + f"{mm_model_name}: optional private model metadata not cached locally" + ) kit = _modality_kit(modality, mm_model_name) tokenizer, processor, _ = _load_processor_and_renderer(mm_model_name) @@ -1062,8 +1044,10 @@ def test_bridge_refuses_when_add_vision_id_loses_prior_count( ``apply_chat_template`` and a full re-render. The bridge must refuse (return None) so the caller falls back to a full re-render. """ - if not _hf_snapshot_cached(mm_model_name): - pytest.skip(f"{mm_model_name}: HF snapshot not cached locally") + if not _model_assets_available(mm_model_name): + pytest.skip( + f"{mm_model_name}: optional private model metadata not cached locally" + ) kit = _modality_kit(modality, mm_model_name) tokenizer, processor, _ = _load_processor_and_renderer(mm_model_name) @@ -1137,8 +1121,8 @@ def test_qwen3_vl_renderer_exposes_image_modality(): registry → load → create_renderer(auto) → expected concrete class. """ model = "Qwen/Qwen3-VL-4B-Instruct" - if not _hf_snapshot_cached(model): - pytest.skip(f"{model}: HF snapshot not cached locally") + if not _model_assets_available(model): + pytest.skip(f"{model}: optional private model metadata not cached locally") tokenizer = load_tokenizer(model) renderer = create_renderer(tokenizer) assert isinstance(renderer, Qwen3VLRenderer) diff --git a/tests/test_muse_glimmer.py b/tests/test_muse_glimmer.py index 500898ee..41dc0172 100644 --- a/tests/test_muse_glimmer.py +++ b/tests/test_muse_glimmer.py @@ -17,8 +17,11 @@ from functools import lru_cache import pytest + from renderers.base import ToolCallParseStatus +pytestmark = pytest.mark.model + MODEL = "meta-models/Muse-Glimmer-30B" DATE = "2026-10-16" IMAGE = {"type": "image_url", "image_url": {"url": "https://example.com/a.png"}} @@ -70,8 +73,8 @@ def _invoke(name: str, *params: tuple[str, str]) -> str: @lru_cache(maxsize=None) def _load(**config): from renderers import create_renderer - from renderers.base import load_tokenizer from renderers.configs import MuseGlimmerRendererConfig + from tests.model_assets import load_test_tokenizer as load_tokenizer tok = load_tokenizer(MODEL) return tok, create_renderer( @@ -132,7 +135,7 @@ def _token(text: str) -> int: def test_create_renderer_resolves_muse_glimmer(): from renderers import create_renderer - from renderers.base import load_tokenizer + from tests.model_assets import load_test_tokenizer as load_tokenizer renderer = create_renderer(load_tokenizer(MODEL)) assert type(renderer).__name__ == "MuseGlimmerRenderer" diff --git a/tests/test_nemotron35_vlm.py b/tests/test_nemotron35_vlm.py index 76a86cab..f1bfd28d 100644 --- a/tests/test_nemotron35_vlm.py +++ b/tests/test_nemotron35_vlm.py @@ -12,7 +12,6 @@ from renderers.base import MODEL_RENDERER_MAP, MULTIMODAL_MODELS, PlaceholderRange from renderers.nemotron3 import Nemotron35Renderer - MODEL = "nvidia/NVIDIA-Nemotron-3.5-Super-EA-09112026" @@ -277,6 +276,8 @@ def test_nemotron35_loads_approved_remote_processor_without_revision(monkeypatch ] +@pytest.mark.model +@pytest.mark.multimodal def test_real_processor_token_parity_when_metadata_is_available(): metadata = os.environ.get("NEMOTRON35_VLM_METADATA") if metadata is None: diff --git a/tests/test_offsetless_tokenizers.py b/tests/test_offsetless_tokenizers.py index 255c2c68..85174c8b 100644 --- a/tests/test_offsetless_tokenizers.py +++ b/tests/test_offsetless_tokenizers.py @@ -7,7 +7,7 @@ import pytest from renderers import create_renderer -from renderers.base import load_tokenizer +from tests.model_assets import load_test_tokenizer as load_tokenizer class OffsetlessTokenizer: @@ -181,6 +181,7 @@ def test_offsetless_contract_across_renderer_bridges( _assert_offsetless_contract(expected, actual, renderer) +@pytest.mark.model def test_hy3_offsetless_preserves_multiple_system_message_indices() -> None: """Hy3's joined system header must not erase either caller message.""" @@ -199,6 +200,7 @@ def test_hy3_offsetless_preserves_multiple_system_message_indices() -> None: assert {0, 1}.issubset(actual.message_indices) +@pytest.mark.model @pytest.mark.parametrize( "deepseek_model", ["deepseek-ai/DeepSeek-V3", "deepseek-ai/DeepSeek-R1"], diff --git a/tests/test_parity.py b/tests/test_parity.py index d4711c68..928934bd 100644 --- a/tests/test_parity.py +++ b/tests/test_parity.py @@ -16,7 +16,6 @@ from typing import Annotated, Any, Literal, Mapping, Union, cast, get_args, get_origin import pytest - from parity import ( KWARG_VALUES, MODEL_CATALOG, @@ -26,9 +25,11 @@ kwarg_combinations, scenario_is_valid, ) + from renderers import create_renderer -from renderers.base import MODEL_RENDERER_MAP, load_tokenizer +from renderers.base import MODEL_RENDERER_MAP from renderers.configs import RendererConfig, _config_class_for +from tests.model_assets import load_test_tokenizer as load_tokenizer from tests.reference_rendering import ( DEFAULT_REFERENCE_ORACLE, REFERENCE_ORACLES, @@ -38,6 +39,8 @@ render_reference, ) +pytestmark = pytest.mark.model + def _id(case: ModelCase, scenario: Scenario, kwargs: Mapping[str, Any]) -> str: values = ",".join(f"{key}={value!r}" for key, value in kwargs.items()) diff --git a/tests/test_parse_response.py b/tests/test_parse_response.py index be5afbd7..ee9a70ea 100644 --- a/tests/test_parse_response.py +++ b/tests/test_parse_response.py @@ -7,8 +7,12 @@ from functools import lru_cache import pytest + from renderers import create_renderer -from renderers.base import ToolCallParseStatus, load_tokenizer +from renderers.base import ToolCallParseStatus +from tests.model_assets import load_test_tokenizer as load_tokenizer + +pytestmark = pytest.mark.model @lru_cache diff --git a/tests/test_parsers.py b/tests/test_parsers.py index a81a5eb3..00b34b37 100644 --- a/tests/test_parsers.py +++ b/tests/test_parsers.py @@ -6,7 +6,7 @@ import pytest -from renderers.base import ToolCallParseStatus, load_tokenizer +from renderers.base import ToolCallParseStatus from renderers.parsers import ( REASONING_PARSERS, TOOL_PARSERS, @@ -15,6 +15,9 @@ get_reasoning_parser, get_tool_parser, ) +from tests.model_assets import load_test_tokenizer as load_tokenizer + +pytestmark = pytest.mark.model def test_registries_nonempty(): diff --git a/tests/test_qwen35_size_coverage.py b/tests/test_qwen35_size_coverage.py index b609d14f..76abdc7d 100644 --- a/tests/test_qwen35_size_coverage.py +++ b/tests/test_qwen35_size_coverage.py @@ -18,8 +18,8 @@ from parity import MODEL_CATALOG from renderers import Qwen35Renderer, Qwen35RendererConfig, create_renderer -from renderers.base import MODEL_RENDERER_MAP, load_tokenizer - +from renderers.base import MODEL_RENDERER_MAP +from tests.model_assets import load_test_tokenizer as load_tokenizer _QWEN35_IN_MAP = { case.model for case in MODEL_CATALOG if case.resolved_renderer == "qwen3.5" @@ -53,6 +53,7 @@ def test_no_other_qwen35_sizes_silently_added(): # --------------------------------------------------------------------------- +@pytest.mark.model @pytest.mark.parametrize( "qwen35_model,expected_default", [ diff --git a/tests/test_qwen38.py b/tests/test_qwen38.py index 9fbcacf1..6ccb4e25 100644 --- a/tests/test_qwen38.py +++ b/tests/test_qwen38.py @@ -2,8 +2,8 @@ from __future__ import annotations -from functools import lru_cache from copy import deepcopy +from functools import lru_cache import pytest from pydantic import TypeAdapter @@ -14,7 +14,10 @@ RendererConfig, create_renderer, ) -from renderers.base import MODEL_RENDERER_MAP, MULTIMODAL_MODELS, load_tokenizer +from renderers.base import MODEL_RENDERER_MAP, MULTIMODAL_MODELS +from tests.model_assets import load_test_tokenizer as load_tokenizer + +pytestmark = pytest.mark.model TOOLS = [ diff --git a/tests/test_reasoning_boundaries.py b/tests/test_reasoning_boundaries.py index eecf0281..1cade4cf 100644 --- a/tests/test_reasoning_boundaries.py +++ b/tests/test_reasoning_boundaries.py @@ -4,9 +4,13 @@ import pytest from parity import MODEL_CATALOG + from renderers import create_renderer -from renderers.base import ToolCallParseStatus, load_tokenizer +from renderers.base import ToolCallParseStatus from renderers.configs import config_from_name +from tests.model_assets import load_test_tokenizer as load_tokenizer + +pytestmark = pytest.mark.model # Exercise every registered renderer, including inherited family variants. _CASES = {} diff --git a/tests/test_roundtrip.py b/tests/test_roundtrip.py index c1759b84..56675072 100644 --- a/tests/test_roundtrip.py +++ b/tests/test_roundtrip.py @@ -14,9 +14,12 @@ from replay import assert_evidence, replay_client from renderers import create_renderer -from renderers.base import MODEL_RENDERER_MAP, ToolCallParseStatus, load_tokenizer +from renderers.base import MODEL_RENDERER_MAP, ToolCallParseStatus from renderers.client import generate from renderers.configs import config_from_name +from tests.model_assets import load_test_tokenizer as load_tokenizer + +pytestmark = pytest.mark.model # Both matrices visit checkpoints in groups; reuse within each group only. diff --git a/tests/test_tagged_rollout.py b/tests/test_tagged_rollout.py index 68677a71..c34170e6 100644 --- a/tests/test_tagged_rollout.py +++ b/tests/test_tagged_rollout.py @@ -11,9 +11,12 @@ from replay import assert_evidence, replay_client from renderers import create_renderer -from renderers.base import ToolCallParseStatus, load_tokenizer +from renderers.base import ToolCallParseStatus from renderers.client import generate from renderers.configs import config_from_name +from tests.model_assets import load_test_tokenizer as load_tokenizer + +pytestmark = pytest.mark.model @lru_cache(maxsize=1) diff --git a/tests/test_tool_arg_type_preservation.py b/tests/test_tool_arg_type_preservation.py index ec12a3e2..d8196417 100644 --- a/tests/test_tool_arg_type_preservation.py +++ b/tests/test_tool_arg_type_preservation.py @@ -30,6 +30,8 @@ from renderers.muse_glimmer import MuseGlimmerRenderer +pytestmark = pytest.mark.model + # (HuggingFace model name, renderer name). Two JSON-shaped controls # (string types already preserved by the wire format) + five XML-style @@ -40,7 +42,7 @@ @lru_cache(maxsize=None) def _load(model: str, renderer_name: str): from renderers import config_from_name, create_renderer - from renderers.base import load_tokenizer + from tests.model_assets import load_test_tokenizer as load_tokenizer tok = load_tokenizer(model) return tok, create_renderer(tok, config_from_name(renderer_name))