renderers 0.1.13.dev21__tar.gz → 0.2.1.dev1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. renderers-0.2.1.dev1/.github/workflows/test.yml +69 -0
  2. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/PKG-INFO +1 -1
  3. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/pyproject.toml +6 -0
  4. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/renderers/_version.py +2 -2
  5. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/renderers/base.py +7 -1
  6. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/tests/conftest.py +7 -3
  7. renderers-0.2.1.dev1/tests/model_assets.py +94 -0
  8. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/tests/test_bridge.py +9 -4
  9. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/tests/test_client.py +8 -6
  10. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/tests/test_custom.py +2 -1
  11. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/tests/test_deepseek_r1.py +5 -1
  12. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/tests/test_deepseek_v4.py +5 -1
  13. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/tests/test_disabled_thinking_stability.py +5 -1
  14. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/tests/test_gemma4.py +11 -2
  15. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/tests/test_glm53_flash.py +3 -1
  16. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/tests/test_glm_tool_name_validation.py +5 -1
  17. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/tests/test_hy3.py +4 -1
  18. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/tests/test_inkling.py +4 -1
  19. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/tests/test_laguna_m1.py +4 -1
  20. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/tests/test_laguna_xs21.py +3 -1
  21. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/tests/test_llama_3.py +4 -2
  22. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/tests/test_load_tokenizer.py +25 -9
  23. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/tests/test_message_indices.py +3 -2
  24. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/tests/test_multimodal.py +47 -63
  25. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/tests/test_muse_glimmer.py +5 -2
  26. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/tests/test_nemotron35_vlm.py +2 -1
  27. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/tests/test_offsetless_tokenizers.py +3 -1
  28. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/tests/test_parity.py +5 -2
  29. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/tests/test_parse_response.py +5 -1
  30. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/tests/test_parsers.py +4 -1
  31. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/tests/test_qwen35_size_coverage.py +3 -2
  32. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/tests/test_qwen38.py +5 -2
  33. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/tests/test_reasoning_boundaries.py +5 -1
  34. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/tests/test_roundtrip.py +4 -1
  35. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/tests/test_tagged_rollout.py +4 -1
  36. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/tests/test_tool_arg_type_preservation.py +3 -1
  37. renderers-0.1.13.dev21/.github/workflows/test.yml +0 -41
  38. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/.github/workflows/publish-dev.yml +0 -0
  39. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/.github/workflows/publish.yml +0 -0
  40. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/.github/workflows/style.yml +0 -0
  41. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/.gitignore +0 -0
  42. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/.pre-commit-config.yaml +0 -0
  43. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/LICENSE +0 -0
  44. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/README.md +0 -0
  45. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/docs/renderer-config.md +0 -0
  46. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/examples/README.md +0 -0
  47. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/examples/sglang/multiturn_generate_sglang.py +0 -0
  48. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/examples/sglang/online_multiturn_sglang.py +0 -0
  49. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/examples/tinker/multiturn_generate_tinker.py +0 -0
  50. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/examples/transformers/multiturn_generate_transformers.py +0 -0
  51. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/examples/vllm/multiturn_generate_vllm.py +0 -0
  52. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/renderers/__init__.py +0 -0
  53. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/renderers/client.py +0 -0
  54. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/renderers/configs.py +0 -0
  55. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/renderers/custom.py +0 -0
  56. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/renderers/deepseek_r1.py +0 -0
  57. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/renderers/deepseek_v3.py +0 -0
  58. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/renderers/deepseek_v4.py +0 -0
  59. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/renderers/default.py +0 -0
  60. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/renderers/gemma4.py +0 -0
  61. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/renderers/glm45.py +0 -0
  62. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/renderers/glm5.py +0 -0
  63. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/renderers/gpt_oss.py +0 -0
  64. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/renderers/hy3.py +0 -0
  65. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/renderers/inkling.py +0 -0
  66. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/renderers/kimi_k2.py +0 -0
  67. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/renderers/kimi_k25.py +0 -0
  68. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/renderers/laguna_s21.py +0 -0
  69. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/renderers/laguna_xs2.py +0 -0
  70. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/renderers/llama_3.py +0 -0
  71. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/renderers/minimax_m2.py +0 -0
  72. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/renderers/muse_glimmer.py +0 -0
  73. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/renderers/nemotron3.py +0 -0
  74. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/renderers/parsers.py +0 -0
  75. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/renderers/parsing.py +0 -0
  76. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/renderers/prime_qwen3.py +0 -0
  77. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/renderers/qwen3.py +0 -0
  78. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/renderers/qwen35.py +0 -0
  79. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/renderers/qwen36.py +0 -0
  80. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/renderers/qwen38.py +0 -0
  81. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/renderers/qwen3_vl.py +0 -0
  82. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/renderers/reasoning.py +0 -0
  83. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/renderers/tagged.py +0 -0
  84. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/tests/audit_prefix_stability.py +0 -0
  85. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/tests/fixtures/prefix_stability.json +0 -0
  86. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/tests/parity.py +0 -0
  87. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/tests/reference_continuations.py +0 -0
  88. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/tests/reference_rendering.py +0 -0
  89. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/tests/replay.py +0 -0
  90. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/tests/test_build_helpers.py +0 -0
  91. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/tests/test_incremental.py +0 -0
  92. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/tests/test_is_content.py +0 -0
  93. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/tests/test_kimi_k25_tool_schema.py +0 -0
  94. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/tests/test_message_tool_names.py +0 -0
  95. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/tests/test_nemotron3_ultra.py +0 -0
  96. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/tests/test_parse_response_robustness.py +0 -0
  97. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/tests/test_preserve_thinking.py +0 -0
  98. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/tests/test_renderer_config.py +0 -0
  99. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/tests/test_sampled_mask.py +0 -0
  100. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/tests/test_tokens_per_message.py +0 -0
  101. {renderers-0.1.13.dev21 → renderers-0.2.1.dev1}/uv.lock +0 -0
@@ -0,0 +1,69 @@
1
+ name: Test
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ paths:
7
+ - "**.py"
8
+ - "pyproject.toml"
9
+ - "uv.lock"
10
+ - ".github/workflows/test.yml"
11
+ pull_request:
12
+ branches: [main]
13
+ paths:
14
+ - "**.py"
15
+ - "pyproject.toml"
16
+ - "uv.lock"
17
+ - ".github/workflows/test.yml"
18
+
19
+ permissions:
20
+ contents: read
21
+
22
+ jobs:
23
+ unit:
24
+ name: Offline (Python ${{ matrix.python-version }})
25
+ runs-on: ubuntu-latest
26
+ env:
27
+ HF_HUB_OFFLINE: "1"
28
+ TRANSFORMERS_OFFLINE: "1"
29
+ strategy:
30
+ fail-fast: false
31
+ matrix:
32
+ python-version: ["3.11", "3.12", "3.13", "3.14"]
33
+ steps:
34
+ - uses: actions/checkout@v4
35
+ - uses: actions/setup-python@v6
36
+ with:
37
+ python-version: ${{ matrix.python-version }}
38
+ - uses: astral-sh/setup-uv@v7
39
+ - run: uv sync --locked
40
+ - run: uv run --no-sync pytest tests/ -m "not model" -v
41
+
42
+ model:
43
+ name: Pinned ${{ matrix.suite }} parity
44
+ runs-on: ubuntu-latest
45
+ timeout-minutes: 45
46
+ strategy:
47
+ fail-fast: false
48
+ matrix:
49
+ include:
50
+ - suite: text
51
+ selection: "model and not multimodal"
52
+ - suite: multimodal
53
+ selection: "model and multimodal"
54
+ env:
55
+ HF_HUB_DISABLE_PROGRESS_BARS: "1"
56
+ steps:
57
+ - uses: actions/checkout@v4
58
+ - uses: actions/setup-python@v6
59
+ with:
60
+ python-version: "3.13"
61
+ - uses: astral-sh/setup-uv@v7
62
+ - uses: actions/cache@v4
63
+ with:
64
+ path: |
65
+ ~/.cache/huggingface/hub
66
+ ~/.cache/huggingface/modules
67
+ key: hf-renderers-${{ runner.os }}-${{ matrix.suite }}-${{ hashFiles('tests/model_assets.py') }}
68
+ - run: uv sync --locked
69
+ - run: uv run --no-sync pytest tests/ -m "${{ matrix.selection }}" -v
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: renderers
3
- Version: 0.1.13.dev21
3
+ Version: 0.2.1.dev1
4
4
  Summary: Chat template renderers — deterministic message-to-token conversion for LLM training
5
5
  License-Expression: Apache-2.0
6
6
  License-File: LICENSE
@@ -139,3 +139,9 @@ invalid-key = "warn"
139
139
  unsupported-operator = "warn"
140
140
  invalid-return-type = "warn"
141
141
  invalid-assignment = "warn"
142
+
143
+ [tool.pytest.ini_options]
144
+ markers = [
145
+ "model: requires real tokenizer or processor assets at pinned revisions",
146
+ "multimodal: exercises real image/audio/video processors or model sidecars",
147
+ ]
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.1.13.dev21'
22
- __version_tuple__ = version_tuple = (0, 1, 13, 'dev21')
21
+ __version__ = version = '0.2.1.dev1'
22
+ __version_tuple__ = version_tuple = (0, 2, 1, 'dev1')
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -1313,13 +1313,14 @@ def _load_tokenizer_via_auto(model_name_or_path: str, **kwargs) -> Any:
1313
1313
  return tok
1314
1314
 
1315
1315
 
1316
- def load_tokenizer(model_name_or_path: str):
1316
+ def load_tokenizer(model_name_or_path: str, *, revision: str | None = None):
1317
1317
  """Load a tokenizer with the renderers-package security policy.
1318
1318
 
1319
1319
  Default ``trust_remote_code=False``. Models listed in
1320
1320
  ``TRUSTED_REVISIONS`` (Moonshot Kimi-K2 family) load with
1321
1321
  ``trust_remote_code=True`` AND a pinned ``revision=<sha>`` so
1322
1322
  transformers only executes the reviewed commit's tokenizer Python.
1323
+ ``revision`` pins ordinary assets without overriding reviewed remote code.
1323
1324
 
1324
1325
  ``AutoTokenizer.from_pretrained`` eagerly builds the model config to
1325
1326
  resolve the tokenizer class. If that construction raises on a
@@ -1338,6 +1339,11 @@ def load_tokenizer(model_name_or_path: str):
1338
1339
  _require_transformers("Loading a tokenizer")
1339
1340
  load_name_or_path = _tokenizer_source_for(model_name_or_path)
1340
1341
  kwargs = _tokenizer_load_kwargs(load_name_or_path)
1342
+ if revision is not None:
1343
+ trusted_revision = kwargs.get("revision")
1344
+ if trusted_revision is not None and revision != trusted_revision:
1345
+ raise ValueError("Cannot override the reviewed remote-code revision")
1346
+ kwargs["revision"] = revision
1341
1347
  tok = _load_tokenizer_via_auto(load_name_or_path, **kwargs)
1342
1348
  return _preserve_requested_tokenizer_name(
1343
1349
  tok,
@@ -12,9 +12,10 @@ from pathlib import Path
12
12
 
13
13
  import pytest
14
14
  from parity import models_for
15
+
15
16
  from renderers import create_renderer
16
- from renderers.base import load_tokenizer
17
17
  from renderers.configs import config_from_name
18
+ from tests.model_assets import load_test_tokenizer
18
19
 
19
20
  # Backwards-compatible view used by documentation and a few invariant tests.
20
21
  RENDERER_MODELS = [(case.model, case.renderer) for case in models_for("shared")]
@@ -33,7 +34,7 @@ def release_shared_models():
33
34
  def _load(model_name: str, renderer_name: str):
34
35
  key = f"{model_name}:{renderer_name}"
35
36
  if key not in _cache:
36
- tokenizer = load_tokenizer(model_name)
37
+ tokenizer = load_test_tokenizer(model_name)
37
38
  renderer = create_renderer(tokenizer, config_from_name(renderer_name))
38
39
  _cache[key] = (tokenizer, renderer)
39
40
  return _cache[key]
@@ -54,7 +55,10 @@ def pytest_generate_tests(metafunc):
54
55
  cases = models_for(suite)
55
56
  metafunc.parametrize(
56
57
  "model_name,renderer_name",
57
- [(case.model, case.renderer) for case in cases],
58
+ [
59
+ pytest.param(case.model, case.renderer, marks=pytest.mark.model)
60
+ for case in cases
61
+ ],
58
62
  ids=[case.model for case in cases],
59
63
  )
60
64
 
@@ -0,0 +1,94 @@
1
+ """Immutable test assets; Llama revisions refer to the audited tokenizer mirrors."""
2
+
3
+ from pathlib import Path
4
+
5
+ from renderers.base import load_tokenizer
6
+
7
+ MODEL_REVISIONS = {
8
+ "MiniMaxAI/MiniMax-M2.5": "2515a3ea0afec1dd22d0c7794b3a91557804d195",
9
+ "PrimeIntellect/Qwen3-0.6B": "15aba67c8ec68aeac96f57ba7e31d373564ee03c",
10
+ "PrimeIntellect/Qwen3-1.7B": "61a79cd101170642a653d1d44d18f69f88f54e08",
11
+ "Qwen/Qwen2.5-0.5B-Instruct": "7ae557604adf67be50417f59c2c2f167def9a775",
12
+ "Qwen/Qwen3-0.6B": "c1899de289a04d12100db370d81485cdf75e47ca",
13
+ "Qwen/Qwen3-4B": "1cfa9a7208912126459214e8b04321603b3df60c",
14
+ "Qwen/Qwen3-8B": "b968826d9c46dd6066d109eabc6255188de91218",
15
+ "Qwen/Qwen3-VL-30B-A3B-Instruct": "9c4b90e1e4ba969fd3b5378b57d966d725f1b86c",
16
+ "Qwen/Qwen3-VL-4B-Instruct": "ebb281ec70b05090aa6165b016eac8ec08e71b17",
17
+ "Qwen/Qwen3-VL-8B-Instruct": "0c351dd01ed87e9c1b53cbc748cba10e6187ff3b",
18
+ "Qwen/Qwen3.5-0.8B": "2fc06364715b967f1860aea9cf38778875588b17",
19
+ "Qwen/Qwen3.5-122B-A10B": "dc4d348443bc740c68e2d77492492c11606384d5",
20
+ "Qwen/Qwen3.5-2B": "15852e8c16360a2fea060d615a32b45270f8a8fc",
21
+ "Qwen/Qwen3.5-35B-A3B": "59d61f3ce65a6d9863b86d2e96597125219dc754",
22
+ "Qwen/Qwen3.5-397B-A17B": "8472618112abcbd45acbcdc58436aff4233c23f7",
23
+ "Qwen/Qwen3.5-4B": "851bf6e806efd8d0a36b00ddf55e13ccb7b8cd0a",
24
+ "Qwen/Qwen3.5-9B": "c202236235762e1c871ad0ccb60c8ee5ba337b9a",
25
+ "Qwen/Qwen3.6-35B-A3B": "995ad96eacd98c81ed38be0c5b274b04031597b0",
26
+ "Qwen/Qwen3.8-27B": "1d4bf0f2ff6012fd82039f2fa52739d0dd7c60c0",
27
+ "Qwen/Qwen3.8-Flash-Next": "de4b8e4d43b917e7706784d8bb445c9af86a3540",
28
+ "THUDM/GLM-4.5-Air": "a24ceef6ce4f3536971efe9b778bdaa1bab18daa",
29
+ "deepseek-ai/DeepSeek-R1": "56d4cbbb4d29f4355bab4b9a39ccb717a14ad5ad",
30
+ "deepseek-ai/DeepSeek-V3": "e815299b0bcbac849fa540c768ef21845365c9eb",
31
+ "deepseek-ai/DeepSeek-V4-Flash-0731": "7872f01b1d1fe23eabc4c98b48bffcef5a386062",
32
+ "google/gemma-4-26B-A4B-it": "4d7ae4984b7db7de8f8457170b3f1a419ee76d52",
33
+ "google/gemma-4-31B-it": "842da3794eaa0b77d5f08bae87a17459d91ff475",
34
+ "google/gemma-4-E2B-it": "3e22461f65e89153144f8adb70e3b8c2cc9845a7",
35
+ "google/gemma-4-E4B-it": "ee0ef6023621cff504d758262d4e04895a5af4a2",
36
+ "meta-llama/Llama-3.2-1B-Instruct": "5a8abab4a5d6f164389b1079fb721cfab8d7126c",
37
+ "meta-llama/Llama-3.2-3B-Instruct": "006f5dcd1393c3add266de40994ba96225e9689d",
38
+ "meta-models/Muse-Glimmer-30B": "cec94fcfd6512f2cfe708138c32533b346ea4f69",
39
+ "moonshotai/Kimi-K2-Instruct": "fd1984e2b7a3350dbf7305fe73a4ede25c14de50",
40
+ "moonshotai/Kimi-K2.5": "4d01dfe0332d63057c186e0b262165819efb6611",
41
+ "moonshotai/Kimi-K2.6": "2755962d07cb42aa2d988a35bcb65cd4a9c2de82",
42
+ "nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16": "bf77c3174f68ad409e1c2aa60daeb46e32d1c606",
43
+ "nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-BF16": "2dc98e2afe4face0e4ce40972a915c45368bd34a",
44
+ "nvidia/NVIDIA-Nemotron-3-Ultra-550B-A55B-BF16": "77df655d5e9f8362164ed14dd8b48f8bce657498",
45
+ "nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-BF16": "a9904d24bcc1d289a1950fa9d2b978c47cf903b9",
46
+ "openai/gpt-oss-120b": "b5c939de8f754692c1647ca79fbf85e8c1e70f8a",
47
+ "openai/gpt-oss-20b": "6cee5e81ee83917806bbde320786a8fb61efebee",
48
+ "poolside/Laguna-M.1": "2bf8a4ab6d2e8e1e0d92a41318532a0fee1640d6",
49
+ "poolside/Laguna-S-2.1": "0f573140834b11cfac0c2af97a101a7a69a13e22",
50
+ "poolside/Laguna-XS-2.1": "c5f36269bbdbd3f27fddc9a9f9dbae0cf2cf57db",
51
+ "poolside/Laguna-XS.2": "a397bde04501591d0ac3d74a5d9d0a4ec298ffb8",
52
+ "tencent/Hy3": "a960ebc3da325ba167f069f76c41eb62c9280d22",
53
+ "thinkingmachines/Inkling": "828496eeae4c243ff1a22f7f28ff83694f2f7bc9",
54
+ "thinkingmachines/Inkling-Small": "8cc5877b44d343f88b92086aa1fb72897950f06a",
55
+ "zai-org/GLM-4.7-Flash": "7dd20894a642a0aa287e9827cb1a1f7f91386b67",
56
+ "zai-org/GLM-5": "c183ef8c61faee82855eca1ed9bb3a9a7ce3b0b2",
57
+ "zai-org/GLM-5.1": "26e1bd6e011feb778d25ae34b09b07074139d92d",
58
+ "zai-org/GLM-5.3": "ba32f8c19c8738e2c57c71798c601d477a7bf844",
59
+ "zai-org/GLM-5.3-BF16": "abf42e6c0e53bc29a875511301e7b7fcd220315b",
60
+ "zai-org/GLM-5.3-Flash-BF16": "f9fed3bbc82291073373d67452e9f25317ef7577",
61
+ }
62
+
63
+
64
+ def model_revision(model: str) -> str:
65
+ # The optional private VLM fixture can only resolve an existing local snapshot.
66
+ if model == "nvidia/NVIDIA-Nemotron-3.5-Super-EA-09112026":
67
+ from huggingface_hub import try_to_load_from_cache
68
+
69
+ path = try_to_load_from_cache(model, "tokenizer_config.json")
70
+ if isinstance(path, str):
71
+ return Path(path).parent.name
72
+ try:
73
+ return MODEL_REVISIONS[model]
74
+ except KeyError as exc:
75
+ raise KeyError(f"No immutable test revision registered for {model!r}") from exc
76
+
77
+
78
+ def load_test_tokenizer(model: str):
79
+ return load_tokenizer(model, revision=model_revision(model))
80
+
81
+
82
+ def load_test_processor(model: str):
83
+ from transformers import AutoProcessor
84
+
85
+ return AutoProcessor.from_pretrained(
86
+ model,
87
+ revision=model_revision(model),
88
+ trust_remote_code=model
89
+ in {
90
+ "moonshotai/Kimi-K2.5",
91
+ "moonshotai/Kimi-K2.6",
92
+ "nvidia/NVIDIA-Nemotron-3.5-Super-EA-09112026",
93
+ },
94
+ )
@@ -27,8 +27,8 @@ _BRIDGE_MODELS = [(case.model, case.renderer) for case in models_for("bridge")]
27
27
  @lru_cache(maxsize=None)
28
28
  def _load(model_name: str, renderer_name: str):
29
29
  from renderers import create_renderer
30
- from renderers.base import load_tokenizer
31
30
  from renderers.configs import config_from_name
31
+ from tests.model_assets import load_test_tokenizer as load_tokenizer
32
32
 
33
33
  tok = load_tokenizer(model_name)
34
34
  return tok, create_renderer(tok, config_from_name(renderer_name))
@@ -38,7 +38,10 @@ def pytest_generate_tests(metafunc):
38
38
  if "br_model" in metafunc.fixturenames:
39
39
  metafunc.parametrize(
40
40
  "br_model,br_renderer_name",
41
- _BRIDGE_MODELS,
41
+ [
42
+ pytest.param(model, renderer, marks=pytest.mark.model)
43
+ for model, renderer in _BRIDGE_MODELS
44
+ ],
42
45
  ids=[m for m, _ in _BRIDGE_MODELS],
43
46
  )
44
47
 
@@ -223,6 +226,7 @@ def test_qwen3_renders_openai_text_content_parts(br_renderer, br_tokenizer, br_m
223
226
  assert text in br_tokenizer.decode(list_ids, skip_special_tokens=False)
224
227
 
225
228
 
229
+ @pytest.mark.model
226
230
  def test_bridge_declines_across_user_query_when_template_drops_thinking():
227
231
  """Qwen3's template drops a past block's thinking once a new user turn
228
232
  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():
237
241
  - no prior thinking + new user query -> decline; no marker lookback.
238
242
  """
239
243
  from renderers import create_renderer
240
- from renderers.base import load_tokenizer
241
244
  from renderers.configs import Qwen3RendererConfig
245
+ from tests.model_assets import load_test_tokenizer as load_tokenizer
242
246
 
243
247
  tok = load_tokenizer("Qwen/Qwen3-8B")
244
248
  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_
326
330
  assert extended is not None, f"{br_model}: should still bridge within a tool cycle"
327
331
 
328
332
 
333
+ @pytest.mark.model
329
334
  def test_bridge_keeps_thinking_when_history_kwarg_disables_truncation():
330
335
  """GLM ``clear_thinking=False`` / Nemotron ``truncate_history_thinking=
331
336
  False`` keep all past thinking (the template doesn't strip it), so the
332
337
  bridge must NOT decline across a user turn — declining would re-render and
333
338
  re-tokenize model-sampled thinking bytes."""
334
339
  from renderers import create_renderer
335
- from renderers.base import load_tokenizer
336
340
  from renderers.configs import GLM5RendererConfig, Nemotron3RendererConfig
341
+ from tests.model_assets import load_test_tokenizer as load_tokenizer
337
342
 
338
343
  asst = [
339
344
  {"role": "assistant", "reasoning_content": "Let me think.", "content": "Hi"}
@@ -738,6 +738,8 @@ def test_generate_threads_prompt_attribution_through_prebuilt_prompt_path():
738
738
  # ---------------------------------------------------------------------------
739
739
 
740
740
 
741
+ @pytest.mark.model
742
+ @pytest.mark.multimodal
741
743
  @pytest.mark.parametrize(
742
744
  "model_id,renderer_class_path",
743
745
  [
@@ -762,11 +764,8 @@ def test_generate_serializes_multimodal_features_for_qwen_vl_family(
762
764
 
763
765
  import torch as _torch
764
766
 
765
- from renderers.base import (
766
- MultiModalData,
767
- PlaceholderRange,
768
- load_tokenizer,
769
- )
767
+ from renderers.base import MultiModalData, PlaceholderRange
768
+ from tests.model_assets import load_test_tokenizer as load_tokenizer
770
769
 
771
770
  mod_name, cls_name = renderer_class_path.split(":")
772
771
  renderer_cls = getattr(importlib.import_module(mod_name), cls_name)
@@ -832,6 +831,8 @@ def test_generate_serializes_multimodal_features_for_qwen_vl_family(
832
831
  assert isinstance(item, str) and len(item) > 0
833
832
 
834
833
 
834
+ @pytest.mark.model
835
+ @pytest.mark.multimodal
835
836
  def test_generate_serializes_multimodal_features_for_gemma4():
836
837
  """Gemma 4's HF image positions are translated to vLLM's field name."""
837
838
  pytest.importorskip("torch")
@@ -839,8 +840,9 @@ def test_generate_serializes_multimodal_features_for_gemma4():
839
840
 
840
841
  import torch as _torch
841
842
 
842
- from renderers.base import MultiModalData, PlaceholderRange, load_tokenizer
843
+ from renderers.base import MultiModalData, PlaceholderRange
843
844
  from renderers.gemma4 import Gemma4Renderer
845
+ from tests.model_assets import load_test_tokenizer as load_tokenizer
844
846
 
845
847
  renderer = Gemma4Renderer(load_tokenizer("google/gemma-4-31B-it"))
846
848
  mm_data = MultiModalData(
@@ -169,8 +169,9 @@ GLM53_NO_THINKING_SOURCE = textwrap.dedent(
169
169
  )
170
170
 
171
171
 
172
+ @pytest.mark.model
172
173
  def test_readme_example_prefills_the_think_block(tmp_path):
173
- from renderers.base import load_tokenizer
174
+ from tests.model_assets import load_test_tokenizer as load_tokenizer
174
175
 
175
176
  path = tmp_path / "glm53_no_thinking.py"
176
177
  path.write_text(GLM53_NO_THINKING_SOURCE)
@@ -7,12 +7,16 @@ stripping of ``</think>`` from historical assistant turns.
7
7
 
8
8
  from functools import lru_cache
9
9
 
10
+ import pytest
11
+
10
12
  from renderers import (
11
13
  DeepSeekR1Renderer,
12
14
  DeepSeekV3Renderer,
13
15
  create_renderer,
14
16
  )
15
- from renderers.base import load_tokenizer
17
+ from tests.model_assets import load_test_tokenizer as load_tokenizer
18
+
19
+ pytestmark = pytest.mark.model
16
20
 
17
21
 
18
22
  @lru_cache
@@ -6,6 +6,7 @@ from functools import lru_cache
6
6
 
7
7
  import pytest
8
8
  from pydantic import TypeAdapter, ValidationError
9
+
9
10
  from renderers import (
10
11
  DeepSeekV4Renderer,
11
12
  DeepSeekV4RendererConfig,
@@ -13,9 +14,12 @@ from renderers import (
13
14
  ToolCallParseStatus,
14
15
  create_renderer,
15
16
  )
16
- from renderers.base import MODEL_RENDERER_MAP, load_tokenizer
17
+ from renderers.base import MODEL_RENDERER_MAP
18
+ from tests.model_assets import load_test_tokenizer as load_tokenizer
17
19
  from tests.reference_rendering import render_reference
18
20
 
21
+ pytestmark = pytest.mark.model
22
+
19
23
  MODEL = "deepseek-ai/DeepSeek-V4-Flash-0731"
20
24
  BOS = "<|begin▁of▁sentence|>"
21
25
  EOS = "<|end▁of▁sentence|>"
@@ -24,10 +24,14 @@ from __future__ import annotations
24
24
 
25
25
  from functools import lru_cache
26
26
 
27
+ import pytest
27
28
  from parity import models_for
29
+
28
30
  from renderers import create_renderer
29
- from renderers.base import load_tokenizer
30
31
  from renderers.configs import _config_class_for
32
+ from tests.model_assets import load_test_tokenizer as load_tokenizer
33
+
34
+ pytestmark = pytest.mark.model
31
35
 
32
36
  # One representative model per affected renderer family, each with
33
37
  # thinking explicitly disabled.
@@ -6,8 +6,12 @@ import pytest
6
6
  from parity import models_for
7
7
 
8
8
  from renderers import Gemma4Renderer, create_renderer
9
- from renderers.base import MODEL_RENDERER_MAP, MULTIMODAL_MODELS, load_tokenizer
9
+ from renderers.base import MODEL_RENDERER_MAP, MULTIMODAL_MODELS
10
10
  from renderers.configs import Gemma4RendererConfig
11
+ from tests.model_assets import load_test_processor
12
+ from tests.model_assets import load_test_tokenizer as load_tokenizer
13
+
14
+ pytestmark = pytest.mark.model
11
15
 
12
16
 
13
17
  _MODELS = tuple(case.model for case in models_for("gemma-checkpoints"))
@@ -298,7 +302,9 @@ def test_parser_recovers_prompt_opened_post_tool_reasoning():
298
302
  def _image_renderer():
299
303
  """A Gemma 4 renderer with a live ``Gemma4Processor``, or a skip."""
300
304
  tokenizer, _ = _gemma4()
301
- renderer = Gemma4Renderer(tokenizer)
305
+ renderer = Gemma4Renderer(
306
+ tokenizer, processor=load_test_processor("google/gemma-4-31B-it")
307
+ )
302
308
  try:
303
309
  renderer._get_processor()
304
310
  except (RuntimeError, OSError) as exc: # pragma: no cover - env dependent
@@ -312,6 +318,7 @@ def _tiny_image():
312
318
  return Image.new("RGB", (224, 224), color=(128, 192, 255))
313
319
 
314
320
 
321
+ @pytest.mark.multimodal
315
322
  @pytest.mark.parametrize("size", [(224, 224), (448, 224), (224, 448)])
316
323
  def test_real_processor_keeps_one_batched_row_per_image(size):
317
324
  """Guard the ``MultiModalFieldConfig.batched('image')`` contract with
@@ -332,6 +339,7 @@ def test_real_processor_keeps_one_batched_row_per_image(size):
332
339
  assert placeholder.length > 0
333
340
 
334
341
 
342
+ @pytest.mark.multimodal
335
343
  def test_schema_unified_image_parts_still_expand_image_tokens():
336
344
  """``Dataset.from_list`` unifies the Arrow schema across a content list,
337
345
  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():
374
382
  )
375
383
 
376
384
 
385
+ @pytest.mark.multimodal
377
386
  def test_schema_unified_tool_response_image_parts_survive():
378
387
  """Same hazard on the tool-response path, which also has to keep
379
388
  accepting untyped text parts."""
@@ -19,6 +19,8 @@ from openai import AsyncOpenAI
19
19
 
20
20
  from renderers.client import generate
21
21
 
22
+ pytestmark = pytest.mark.model
23
+
22
24
  MODEL = "zai-org/GLM-5.3-Flash-BF16"
23
25
  GLM53_MODELS = ["zai-org/GLM-5.3", MODEL]
24
26
 
@@ -45,7 +47,7 @@ _TOOL_CALL = {
45
47
  @lru_cache(maxsize=None)
46
48
  def _load(model=MODEL):
47
49
  from renderers import create_renderer
48
- from renderers.base import load_tokenizer
50
+ from tests.model_assets import load_test_tokenizer as load_tokenizer
49
51
 
50
52
  tok = load_tokenizer(model)
51
53
  return tok, create_renderer(tok)
@@ -25,9 +25,13 @@ from __future__ import annotations
25
25
 
26
26
  from functools import lru_cache
27
27
 
28
+ import pytest
28
29
  from parity import models_for
30
+
29
31
  from renderers.base import ToolCallParseStatus
30
32
 
33
+ pytestmark = pytest.mark.model
34
+
31
35
  # Both GLM renderers share ``parse_glm`` and are served by the same
32
36
  # strict vLLM parser (the ``glm45`` and ``glm47`` aliases both resolve
33
37
  # to ``Glm47MoeModelToolParser`` in vLLM ≥ 0.24).
@@ -55,7 +59,7 @@ _TOOLS = [
55
59
  @lru_cache(maxsize=None)
56
60
  def _load(model: str, renderer_name: str):
57
61
  from renderers import config_from_name, create_renderer
58
- from renderers.base import load_tokenizer
62
+ from tests.model_assets import load_test_tokenizer as load_tokenizer
59
63
 
60
64
  tok = load_tokenizer(model)
61
65
  return tok, create_renderer(tok, config_from_name(renderer_name))
@@ -15,7 +15,10 @@ from functools import lru_cache
15
15
  import pytest
16
16
 
17
17
  from renderers import Hy3RendererConfig, create_renderer
18
- from renderers.base import ToolCallParseStatus, load_tokenizer
18
+ from renderers.base import ToolCallParseStatus
19
+ from tests.model_assets import load_test_tokenizer as load_tokenizer
20
+
21
+ pytestmark = pytest.mark.model
19
22
 
20
23
  _MODEL = "tencent/Hy3"
21
24
 
@@ -22,9 +22,12 @@ import numpy as np
22
22
  import pytest
23
23
 
24
24
  from renderers import create_renderer
25
- from renderers.base import ToolCallParseStatus, load_tokenizer
25
+ from renderers.base import ToolCallParseStatus
26
26
  from renderers.configs import InklingRendererConfig
27
27
  from renderers.inkling import InklingRenderer
28
+ from tests.model_assets import load_test_tokenizer as load_tokenizer
29
+
30
+ pytestmark = pytest.mark.model
28
31
 
29
32
  _MODEL = "thinkingmachines/Inkling"
30
33
  _SMALL_MODEL = "thinkingmachines/Inkling-Small"
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  from functools import lru_cache
6
6
 
7
+ import pytest
7
8
  from pydantic import TypeAdapter
8
9
 
9
10
  from renderers import (
@@ -12,7 +13,9 @@ from renderers import (
12
13
  RendererConfig,
13
14
  create_renderer,
14
15
  )
15
- from renderers.base import load_tokenizer
16
+ from tests.model_assets import load_test_tokenizer as load_tokenizer
17
+
18
+ pytestmark = pytest.mark.model
16
19
 
17
20
  _MODEL = "poolside/Laguna-M.1"
18
21
 
@@ -22,9 +22,11 @@ from functools import lru_cache
22
22
  import pytest
23
23
 
24
24
  from renderers import create_renderer
25
- from renderers.base import load_tokenizer
26
25
  from renderers.configs import LagunaXS21RendererConfig, config_from_name
27
26
  from renderers.laguna_xs2 import LagunaXS21Renderer
27
+ from tests.model_assets import load_test_tokenizer as load_tokenizer
28
+
29
+ pytestmark = pytest.mark.model
28
30
 
29
31
  _MODEL = "poolside/Laguna-XS-2.1"
30
32
 
@@ -15,11 +15,13 @@ from parity import models_for
15
15
  from renderers import Llama3Renderer, Llama3RendererConfig, create_renderer
16
16
  from renderers.base import (
17
17
  MODEL_RENDERER_MAP,
18
- ParsedResponse,
19
18
  TOKENIZER_SOURCE_OVERRIDES,
19
+ ParsedResponse,
20
20
  ToolCallParseStatus,
21
- load_tokenizer,
22
21
  )
22
+ from tests.model_assets import load_test_tokenizer as load_tokenizer
23
+
24
+ pytestmark = pytest.mark.model
23
25
 
24
26
  # Pinned date for byte-parity tests. Matches the chat template's
25
27
  # strftime fallback so we don't have to override on the apply side.