renderers 0.1.10.dev1__tar.gz → 0.1.10.dev10__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 (79) hide show
  1. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/PKG-INFO +2 -2
  2. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/README.md +1 -1
  3. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/docs/renderer-config.md +4 -2
  4. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/renderers/__init__.py +8 -0
  5. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/renderers/_version.py +2 -2
  6. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/renderers/base.py +14 -3
  7. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/renderers/configs.py +68 -0
  8. renderers-0.1.10.dev10/renderers/laguna_s21.py +33 -0
  9. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/renderers/laguna_xs2.py +14 -2
  10. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/renderers/nemotron3.py +22 -3
  11. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/tests/conftest.py +7 -0
  12. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/tests/test_bridge.py +1 -0
  13. renderers-0.1.10.dev10/tests/test_laguna_s21.py +128 -0
  14. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/tests/test_nemotron3_parity.py +16 -9
  15. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/tests/test_nemotron3_ultra.py +32 -7
  16. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/tests/test_renderer_config_parity.py +4 -0
  17. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/tests/test_roundtrip.py +2 -0
  18. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/.github/workflows/publish-dev.yml +0 -0
  19. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/.github/workflows/publish.yml +0 -0
  20. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/.github/workflows/style.yml +0 -0
  21. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/.github/workflows/test.yml +0 -0
  22. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/.gitignore +0 -0
  23. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/.pre-commit-config.yaml +0 -0
  24. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/LICENSE +0 -0
  25. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/examples/README.md +0 -0
  26. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/examples/sglang/multiturn_generate_sglang.py +0 -0
  27. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/examples/sglang/online_multiturn_sglang.py +0 -0
  28. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/examples/tinker/multiturn_generate_tinker.py +0 -0
  29. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/examples/transformers/multiturn_generate_transformers.py +0 -0
  30. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/examples/vllm/multiturn_generate_vllm.py +0 -0
  31. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/pyproject.toml +0 -0
  32. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/renderers/client.py +0 -0
  33. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/renderers/deepseek_r1.py +0 -0
  34. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/renderers/deepseek_v3.py +0 -0
  35. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/renderers/default.py +0 -0
  36. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/renderers/glm45.py +0 -0
  37. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/renderers/glm5.py +0 -0
  38. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/renderers/gpt_oss.py +0 -0
  39. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/renderers/hy3.py +0 -0
  40. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/renderers/kimi_k2.py +0 -0
  41. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/renderers/kimi_k25.py +0 -0
  42. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/renderers/llama_3.py +0 -0
  43. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/renderers/minimax_m2.py +0 -0
  44. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/renderers/parsers.py +0 -0
  45. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/renderers/parsing.py +0 -0
  46. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/renderers/prime_qwen3.py +0 -0
  47. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/renderers/qwen3.py +0 -0
  48. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/renderers/qwen35.py +0 -0
  49. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/renderers/qwen36.py +0 -0
  50. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/renderers/qwen3_vl.py +0 -0
  51. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/tests/test_build_helpers.py +0 -0
  52. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/tests/test_client.py +0 -0
  53. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/tests/test_deepseek_r1.py +0 -0
  54. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/tests/test_disabled_thinking_stability.py +0 -0
  55. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/tests/test_glm_tool_name_validation.py +0 -0
  56. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/tests/test_gpt_oss_harmony_parity.py +0 -0
  57. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/tests/test_hy3.py +0 -0
  58. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/tests/test_incremental.py +0 -0
  59. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/tests/test_is_content.py +0 -0
  60. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/tests/test_kimi_k25_tool_schema.py +0 -0
  61. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/tests/test_laguna_m1.py +0 -0
  62. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/tests/test_laguna_xs21.py +0 -0
  63. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/tests/test_llama_3.py +0 -0
  64. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/tests/test_load_tokenizer.py +0 -0
  65. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/tests/test_message_indices.py +0 -0
  66. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/tests/test_message_tool_names.py +0 -0
  67. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/tests/test_multimodal.py +0 -0
  68. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/tests/test_parse_response.py +0 -0
  69. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/tests/test_parse_response_robustness.py +0 -0
  70. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/tests/test_parsers.py +0 -0
  71. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/tests/test_preserve_thinking.py +0 -0
  72. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/tests/test_prime_qwen3_parity.py +0 -0
  73. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/tests/test_qwen35_size_coverage.py +0 -0
  74. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/tests/test_render_ids.py +0 -0
  75. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/tests/test_renderer_config.py +0 -0
  76. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/tests/test_sampled_mask.py +0 -0
  77. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/tests/test_tokens_per_message.py +0 -0
  78. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/tests/test_tool_arg_type_preservation.py +0 -0
  79. {renderers-0.1.10.dev1 → renderers-0.1.10.dev10}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: renderers
3
- Version: 0.1.10.dev1
3
+ Version: 0.1.10.dev10
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
@@ -56,7 +56,7 @@ next_prompt_ids = r.bridge_to_next_turn(
56
56
  )
57
57
  ```
58
58
 
59
- Hand-coded renderers ship for `qwen3`, `qwen3-vl`, `qwen3.5`, `qwen3.6`, `glm-5`, `glm-5.1`, `glm-4.5`, `minimax-m2`, `deepseek-v3`, `deepseek-r1`, `kimi-k2`, `kimi-k2.5` / `kimi-k2.6`, `laguna-xs.2`, `laguna-xs-2.1`, `laguna-m.1`, `nemotron-3`, `nemotron-3-ultra`, `llama-3`, `gpt-oss`, `hy3`, and `prime-qwen3`. Anything else falls back to `DefaultRenderer`, a generic `apply_chat_template` wrapper.
59
+ Hand-coded renderers ship for `qwen3`, `qwen3-vl`, `qwen3.5`, `qwen3.6`, `glm-5`, `glm-5.1`, `glm-4.5`, `minimax-m2`, `deepseek-v3`, `deepseek-r1`, `kimi-k2`, `kimi-k2.5` / `kimi-k2.6`, `laguna-xs.2`, `laguna-xs-2.1`, `laguna-s-2.1`, `laguna-m.1`, `nemotron-3`, `nemotron-3-ultra`, `nemotron-3.5`, `llama-3`, `gpt-oss`, `hy3`, and `prime-qwen3`. Anything else falls back to `DefaultRenderer`, a generic `apply_chat_template` wrapper.
60
60
 
61
61
  ## API
62
62
 
@@ -40,7 +40,7 @@ next_prompt_ids = r.bridge_to_next_turn(
40
40
  )
41
41
  ```
42
42
 
43
- Hand-coded renderers ship for `qwen3`, `qwen3-vl`, `qwen3.5`, `qwen3.6`, `glm-5`, `glm-5.1`, `glm-4.5`, `minimax-m2`, `deepseek-v3`, `deepseek-r1`, `kimi-k2`, `kimi-k2.5` / `kimi-k2.6`, `laguna-xs.2`, `laguna-xs-2.1`, `laguna-m.1`, `nemotron-3`, `nemotron-3-ultra`, `llama-3`, `gpt-oss`, `hy3`, and `prime-qwen3`. Anything else falls back to `DefaultRenderer`, a generic `apply_chat_template` wrapper.
43
+ Hand-coded renderers ship for `qwen3`, `qwen3-vl`, `qwen3.5`, `qwen3.6`, `glm-5`, `glm-5.1`, `glm-4.5`, `minimax-m2`, `deepseek-v3`, `deepseek-r1`, `kimi-k2`, `kimi-k2.5` / `kimi-k2.6`, `laguna-xs.2`, `laguna-xs-2.1`, `laguna-s-2.1`, `laguna-m.1`, `nemotron-3`, `nemotron-3-ultra`, `nemotron-3.5`, `llama-3`, `gpt-oss`, `hy3`, and `prime-qwen3`. Anything else falls back to `DefaultRenderer`, a generic `apply_chat_template` wrapper.
44
44
 
45
45
  ## API
46
46
 
@@ -39,10 +39,12 @@ chat-template kwargs. Those fields are covered by parity tests against
39
39
  | Laguna XS.2 | `LagunaXS2RendererConfig` | `enable_thinking`, `render_assistant_messages_raw` | - |
40
40
  | Laguna M.1 | `LagunaM1RendererConfig` | `enable_thinking`, `render_assistant_messages_raw` | - |
41
41
  | Laguna XS-2.1 | `LagunaXS21RendererConfig` | `enable_thinking` | - |
42
+ | Laguna S-2.1 | `LagunaS21RendererConfig` | `enable_thinking`, `preserve_thinking` | - |
42
43
  | Llama 3 | `Llama3RendererConfig` | `date_string`, `tools_in_user_message` | - |
43
44
  | MiniMax M2 | `MiniMaxM2RendererConfig` | `model_identity` | - |
44
45
  | Nemotron-3 Nano / Super | `Nemotron3RendererConfig` | `enable_thinking`, `truncate_history_thinking`, `low_effort` | - |
45
46
  | Nemotron-3 Ultra | `Nemotron3UltraRendererConfig` | `enable_thinking`, `truncate_history_thinking`, `medium_effort` | - |
47
+ | Nemotron-3.5 Lightning | `Nemotron35RendererConfig` | `enable_thinking`, `truncate_history_thinking` | - |
46
48
  | DeepSeek V3 | `DeepSeekV3RendererConfig` | - | - |
47
49
  | DeepSeek R1 | `DeepSeekR1RendererConfig` | - | - |
48
50
 
@@ -134,10 +136,10 @@ the knobs its template actually exposes:
134
136
  | gpt-oss | `auto_drop_analysis=False -> all`, else `tool_cycle` |
135
137
  | Hy3 | `preserved_thinking=True -> all`, else `tool_cycle` |
136
138
  | Kimi K2.5 / 2.6 | `thinking=False -> all`, else `tool_cycle` |
137
- | Nemotron-3 | `truncate_history_thinking=False -> all`; else `enable_thinking=False -> all`; else `tool_cycle` |
139
+ | Nemotron-3 / 3.5 | `truncate_history_thinking=False -> all`; else `enable_thinking=False -> all`; else `tool_cycle` |
138
140
  | DeepSeek R1 | `template` |
139
141
  | MiniMax M2 | `tool_cycle` |
140
- | DeepSeek V3, Qwen3-VL, Kimi K2, Laguna XS.2 / M.1 / XS-2.1, Llama 3 | `all` |
142
+ | DeepSeek V3, Qwen3-VL, Kimi K2, Laguna XS.2 / M.1 / XS-2.1 / S-2.1, Llama 3 | `all` |
141
143
  | PrimeIntellect Qwen3 | `all` |
142
144
 
143
145
  Config construction raises when an explicit template knob directly contradicts
@@ -55,10 +55,12 @@ from renderers.configs import (
55
55
  KimiK25RendererConfig,
56
56
  KimiK2RendererConfig,
57
57
  LagunaM1RendererConfig,
58
+ LagunaS21RendererConfig,
58
59
  LagunaXS2RendererConfig,
59
60
  LagunaXS21RendererConfig,
60
61
  Llama3RendererConfig,
61
62
  MiniMaxM2RendererConfig,
63
+ Nemotron35RendererConfig,
62
64
  Nemotron3RendererConfig,
63
65
  Nemotron3UltraRendererConfig,
64
66
  PrimeQwen3RendererConfig,
@@ -92,10 +94,12 @@ _LAZY_RENDERERS: dict[str, str] = {
92
94
  "KimiK25Renderer": "renderers.kimi_k25",
93
95
  "KimiK2Renderer": "renderers.kimi_k2",
94
96
  "LagunaM1Renderer": "renderers.laguna_xs2",
97
+ "LagunaS21Renderer": "renderers.laguna_s21",
95
98
  "LagunaXS21Renderer": "renderers.laguna_xs2",
96
99
  "LagunaXS2Renderer": "renderers.laguna_xs2",
97
100
  "Llama3Renderer": "renderers.llama_3",
98
101
  "MiniMaxM2Renderer": "renderers.minimax_m2",
102
+ "Nemotron35Renderer": "renderers.nemotron3",
99
103
  "Nemotron3Renderer": "renderers.nemotron3",
100
104
  "Nemotron3UltraRenderer": "renderers.nemotron3",
101
105
  "PrimeQwen3Renderer": "renderers.prime_qwen3",
@@ -149,6 +153,8 @@ __all__ = [
149
153
  "KimiK2RendererConfig",
150
154
  "LagunaM1Renderer",
151
155
  "LagunaM1RendererConfig",
156
+ "LagunaS21Renderer",
157
+ "LagunaS21RendererConfig",
152
158
  "LagunaXS2Renderer",
153
159
  "LagunaXS2RendererConfig",
154
160
  "LagunaXS21Renderer",
@@ -162,6 +168,8 @@ __all__ = [
162
168
  "MiniMaxM2RendererConfig",
163
169
  "MultiModalData",
164
170
  "MultimodalRenderer",
171
+ "Nemotron35Renderer",
172
+ "Nemotron35RendererConfig",
165
173
  "Nemotron3Renderer",
166
174
  "Nemotron3RendererConfig",
167
175
  "Nemotron3UltraRenderer",
@@ -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.10.dev1'
22
- __version_tuple__ = version_tuple = (0, 1, 10, 'dev1')
21
+ __version__ = version = '0.1.10.dev10'
22
+ __version_tuple__ = version_tuple = (0, 1, 10, 'dev10')
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -1065,6 +1065,9 @@ MODEL_RENDERER_MAP: dict[str, str] = {
1065
1065
  "nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-BF16": "nemotron-3",
1066
1066
  "nvidia/NVIDIA-Nemotron-3-Ultra-550B-A55B-BF16": "nemotron-3-ultra",
1067
1067
  "nvidia/NVIDIA-Nemotron-3-Ultra-550B-A55B-FP8": "nemotron-3-ultra",
1068
+ # Nemotron 3.5 (Lightning). Its template is the Ultra variant's minus the
1069
+ # effort kwarg (``nemotron-3.5``).
1070
+ "nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-BF16": "nemotron-3.5",
1068
1071
  # Llama 3.2 (Instruct). Tested against the gated meta-llama repos and
1069
1072
  # the unrestricted unsloth/... mirror, which ships a byte-identical
1070
1073
  # chat template. ``Llama3Renderer`` defaults ``date_string`` to
@@ -1073,11 +1076,12 @@ MODEL_RENDERER_MAP: dict[str, str] = {
1073
1076
  # construction to pin a different date.
1074
1077
  "meta-llama/Llama-3.2-1B-Instruct": "llama-3",
1075
1078
  "meta-llama/Llama-3.2-3B-Instruct": "llama-3",
1076
- # Poolside Laguna. These checkpoints ship distinct chat templates,
1077
- # each mirrored by its own renderer class/config discriminator.
1079
+ # Poolside Laguna. These checkpoints ship distinct chat templates, each
1080
+ # mirrored by its own renderer class/config discriminator.
1078
1081
  "poolside/Laguna-XS.2": "laguna-xs.2",
1079
1082
  "poolside/Laguna-M.1": "laguna-m.1",
1080
1083
  "poolside/Laguna-XS-2.1": "laguna-xs-2.1",
1084
+ "poolside/Laguna-S-2.1": "laguna-s-2.1",
1081
1085
  # GPT-OSS.
1082
1086
  "openai/gpt-oss-20b": "gpt-oss",
1083
1087
  "openai/gpt-oss-120b": "gpt-oss",
@@ -1325,6 +1329,7 @@ def _populate_registry():
1325
1329
  from renderers.hy3 import Hy3Renderer
1326
1330
  from renderers.kimi_k2 import KimiK2Renderer
1327
1331
  from renderers.kimi_k25 import KimiK25Renderer
1332
+ from renderers.laguna_s21 import LagunaS21Renderer
1328
1333
  from renderers.laguna_xs2 import (
1329
1334
  LagunaM1Renderer,
1330
1335
  LagunaXS2Renderer,
@@ -1332,7 +1337,11 @@ def _populate_registry():
1332
1337
  )
1333
1338
  from renderers.llama_3 import Llama3Renderer
1334
1339
  from renderers.minimax_m2 import MiniMaxM2Renderer
1335
- from renderers.nemotron3 import Nemotron3Renderer, Nemotron3UltraRenderer
1340
+ from renderers.nemotron3 import (
1341
+ Nemotron3Renderer,
1342
+ Nemotron3UltraRenderer,
1343
+ Nemotron35Renderer,
1344
+ )
1336
1345
  from renderers.prime_qwen3 import PrimeQwen3Renderer
1337
1346
  from renderers.qwen3 import Qwen3Renderer
1338
1347
  from renderers.qwen3_vl import Qwen3VLRenderer
@@ -1359,9 +1368,11 @@ def _populate_registry():
1359
1368
  "laguna-xs.2": LagunaXS2Renderer,
1360
1369
  "laguna-m.1": LagunaM1Renderer,
1361
1370
  "laguna-xs-2.1": LagunaXS21Renderer,
1371
+ "laguna-s-2.1": LagunaS21Renderer,
1362
1372
  "llama-3": Llama3Renderer,
1363
1373
  "nemotron-3": Nemotron3Renderer,
1364
1374
  "nemotron-3-ultra": Nemotron3UltraRenderer,
1375
+ "nemotron-3.5": Nemotron35Renderer,
1365
1376
  "gpt-oss": GptOssRenderer,
1366
1377
  }
1367
1378
  )
@@ -546,6 +546,37 @@ class LagunaXS21RendererConfig(BaseRendererConfig):
546
546
  upstream default."""
547
547
 
548
548
 
549
+ class LagunaS21RendererConfig(BaseRendererConfig):
550
+ """Laguna S-2.1 renderer config.
551
+
552
+ S-2.1 is a larger sibling of XS-2.1 sharing its tokenizer (same vocab,
553
+ special tokens, and merges), but its chat template is *not* byte-identical:
554
+ ``enable_thinking`` defaults to ``True`` (XS-2.1 defaults ``False``), and a
555
+ new ``preserve_thinking`` kwarg widens the reasoning-display gate to
556
+ ``enable_thinking or preserve_thinking``. The token format is otherwise
557
+ identical, so this is served by
558
+ :class:`renderers.laguna_s21.LagunaS21Renderer`, a thin subclass of
559
+ ``LagunaXS21Renderer`` that only overrides that gate.
560
+ """
561
+
562
+ name: Literal["laguna-s-2.1"] = "laguna-s-2.1"
563
+
564
+ enable_thinking: bool = True
565
+ """When ``True``, the generation prompt ends with ``<think>`` and every
566
+ assistant turn renders ``<think>{reasoning}</think>``; when ``False``,
567
+ turns open with a bare ``</think>``. Mirrors the template's
568
+ ``enable_thinking`` kwarg — note S-2.1's upstream default is ``True``,
569
+ unlike XS-2.1's ``False``."""
570
+
571
+ preserve_thinking: bool = False
572
+ """When ``True``, assistant turns keep their ``<think>{reasoning}</think>``
573
+ block even while ``enable_thinking`` is ``False`` — the template gates
574
+ reasoning display on ``enable_thinking or preserve_thinking``. With the
575
+ default ``False`` the gate collapses to ``enable_thinking`` and the
576
+ renderer matches XS-2.1 turn-for-turn. Mirrors the template's
577
+ ``preserve_thinking`` kwarg and its upstream default."""
578
+
579
+
549
580
  class Llama3RendererConfig(BaseRendererConfig):
550
581
  """Llama-3.x Instruct renderer config.
551
582
 
@@ -657,6 +688,37 @@ class Nemotron3UltraRendererConfig(BaseRendererConfig):
657
688
  user message. Mirrors the Ultra chat template's ``medium_effort`` kwarg."""
658
689
 
659
690
 
691
+ class Nemotron35RendererConfig(BaseRendererConfig):
692
+ """Nemotron-3.5 (Lightning) renderer config.
693
+
694
+ Nemotron 3.5's chat template is the Ultra variant's minus the effort
695
+ kwarg: same reasoning-block glue (no ``\\n`` around ``</think>``, no
696
+ trim on truncated tool-call history), but its Jinja defines no
697
+ reasoning-effort variable at all. It shares the
698
+ :class:`renderers.nemotron3.Nemotron3Renderer` implementation via the
699
+ :class:`renderers.nemotron3.Nemotron35Renderer` subclass and is reached
700
+ via the ``nemotron-3.5`` discriminator.
701
+ """
702
+
703
+ name: Literal["nemotron-3.5"] = "nemotron-3.5"
704
+
705
+ enable_thinking: bool = True
706
+ """See :class:`Nemotron3RendererConfig.enable_thinking`."""
707
+
708
+ truncate_history_thinking: bool = True
709
+ """See :class:`Nemotron3RendererConfig.truncate_history_thinking`."""
710
+
711
+ @model_validator(mode="after")
712
+ def _check_thinking_retention(self):
713
+ _reject_thinking_retention_conflict(
714
+ self,
715
+ "truncate_history_thinking",
716
+ true_implies="tool_cycle",
717
+ false_implies="all",
718
+ )
719
+ return self
720
+
721
+
660
722
  class DeepSeekV3RendererConfig(BaseRendererConfig):
661
723
  """DeepSeek-V3 renderer config (non-reasoning).
662
724
 
@@ -704,10 +766,12 @@ RendererConfig = Annotated[
704
766
  LagunaXS2RendererConfig,
705
767
  LagunaM1RendererConfig,
706
768
  LagunaXS21RendererConfig,
769
+ LagunaS21RendererConfig,
707
770
  Llama3RendererConfig,
708
771
  MiniMaxM2RendererConfig,
709
772
  Nemotron3RendererConfig,
710
773
  Nemotron3UltraRendererConfig,
774
+ Nemotron35RendererConfig,
711
775
  DeepSeekV3RendererConfig,
712
776
  DeepSeekR1RendererConfig,
713
777
  ],
@@ -745,10 +809,12 @@ _CONFIG_BY_NAME: dict[str, type[BaseRendererConfig]] = {
745
809
  "laguna-xs.2": LagunaXS2RendererConfig,
746
810
  "laguna-m.1": LagunaM1RendererConfig,
747
811
  "laguna-xs-2.1": LagunaXS21RendererConfig,
812
+ "laguna-s-2.1": LagunaS21RendererConfig,
748
813
  "llama-3": Llama3RendererConfig,
749
814
  "minimax-m2": MiniMaxM2RendererConfig,
750
815
  "nemotron-3": Nemotron3RendererConfig,
751
816
  "nemotron-3-ultra": Nemotron3UltraRendererConfig,
817
+ "nemotron-3.5": Nemotron35RendererConfig,
752
818
  "deepseek-v3": DeepSeekV3RendererConfig,
753
819
  "deepseek-r1": DeepSeekR1RendererConfig,
754
820
  }
@@ -792,10 +858,12 @@ __all__ = [
792
858
  "KimiK25RendererConfig",
793
859
  "KimiK2RendererConfig",
794
860
  "LagunaM1RendererConfig",
861
+ "LagunaS21RendererConfig",
795
862
  "LagunaXS2RendererConfig",
796
863
  "LagunaXS21RendererConfig",
797
864
  "Llama3RendererConfig",
798
865
  "MiniMaxM2RendererConfig",
866
+ "Nemotron35RendererConfig",
799
867
  "Nemotron3RendererConfig",
800
868
  "Nemotron3UltraRendererConfig",
801
869
  "PrimeQwen3RendererConfig",
@@ -0,0 +1,33 @@
1
+ """Laguna S-2.1 Renderer — a larger sibling of Laguna XS-2.1.
2
+
3
+ S-2.1 shares XS-2.1's tokenizer and token format, so this is a thin subclass of
4
+ :class:`renderers.laguna_xs2.LagunaXS21Renderer`; see that module for the shared
5
+ format. The template delta is two thinking kwargs: ``enable_thinking`` defaults
6
+ to ``True`` (XS-2.1 defaults ``False``), and ``preserve_thinking`` widens the
7
+ reasoning-display gate to ``enable_thinking or preserve_thinking``.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from transformers.tokenization_utils import PreTrainedTokenizer
13
+
14
+ from renderers.configs import LagunaS21RendererConfig
15
+ from renderers.laguna_xs2 import LagunaXS21Renderer
16
+
17
+
18
+ class LagunaS21Renderer(LagunaXS21Renderer):
19
+ """Mirrors the ``poolside/Laguna-S-2.1`` chat template."""
20
+
21
+ # Narrows the inherited attribute so the S-2.1-only ``preserve_thinking``
22
+ # resolves; ``__init__`` always stores an S-2.1 config.
23
+ config: LagunaS21RendererConfig
24
+
25
+ def __init__(
26
+ self,
27
+ tokenizer: PreTrainedTokenizer,
28
+ config: LagunaS21RendererConfig | None = None,
29
+ ):
30
+ super().__init__(tokenizer, config or LagunaS21RendererConfig())
31
+
32
+ def _render_history_reasoning(self) -> bool:
33
+ return self.config.enable_thinking or self.config.preserve_thinking
@@ -41,6 +41,9 @@ Laguna M.1's official template (revision ``2bf8a4ab``) is served by
41
41
  :class:`LagunaM1Renderer`. It shares XS.2's byte layout, tool syntax, and
42
42
  generation prompt, but has no fallback system prompt and reads assistant
43
43
  reasoning from ``reasoning`` before falling back to ``reasoning_content``.
44
+
45
+ S-2.1 subclasses :class:`LagunaXS21Renderer` from :mod:`renderers.laguna_s21`,
46
+ overriding only the ``_render_history_reasoning`` seam.
44
47
  """
45
48
 
46
49
  from __future__ import annotations
@@ -63,6 +66,7 @@ from renderers.base import (
63
66
  )
64
67
  from renderers.configs import (
65
68
  LagunaM1RendererConfig,
69
+ LagunaS21RendererConfig,
66
70
  LagunaXS2RendererConfig,
67
71
  LagunaXS21RendererConfig,
68
72
  )
@@ -120,6 +124,7 @@ class LagunaXS2Renderer:
120
124
  LagunaXS2RendererConfig
121
125
  | LagunaM1RendererConfig
122
126
  | LagunaXS21RendererConfig
127
+ | LagunaS21RendererConfig
123
128
  | None
124
129
  ) = None,
125
130
  ):
@@ -688,10 +693,17 @@ class LagunaXS21Renderer(LagunaXS2Renderer):
688
693
  def __init__(
689
694
  self,
690
695
  tokenizer: PreTrainedTokenizer,
691
- config: LagunaXS21RendererConfig | None = None,
696
+ config: LagunaXS21RendererConfig | LagunaS21RendererConfig | None = None,
692
697
  ):
693
698
  super().__init__(tokenizer, config or LagunaXS21RendererConfig())
694
699
 
700
+ def _render_history_reasoning(self) -> bool:
701
+ """Whether an assistant turn renders its ``<think>{reasoning}</think>``
702
+ block (vs opening with a bare ``</think>``). Mirrors the template's
703
+ reasoning-display gate; XS-2.1 gates this on ``enable_thinking`` alone.
704
+ """
705
+ return self.config.enable_thinking
706
+
695
707
  def render(
696
708
  self,
697
709
  messages: list[Message],
@@ -975,7 +987,7 @@ class LagunaXS21Renderer(LagunaXS2Renderer):
975
987
  # scaffolding the model never samples.
976
988
  emit_special(self._assistant, msg_idx, is_sampled=False, is_content=False)
977
989
 
978
- if self.config.enable_thinking:
990
+ if self._render_history_reasoning():
979
991
  # ``<think>{reasoning}</think>`` renders verbatim, even when
980
992
  # the reasoning is empty; the opener is the gen-prompt
981
993
  # prefill, the rest the model sampled.
@@ -31,7 +31,11 @@ from renderers.base import (
31
31
  should_rerender_for_thinking_retention,
32
32
  trim_to_turn_close,
33
33
  )
34
- from renderers.configs import Nemotron3RendererConfig, Nemotron3UltraRendererConfig
34
+ from renderers.configs import (
35
+ Nemotron3RendererConfig,
36
+ Nemotron3UltraRendererConfig,
37
+ Nemotron35RendererConfig,
38
+ )
35
39
  from renderers.parsing import parse_qwen35
36
40
 
37
41
  # ---------------------------------------------------------------------------
@@ -76,10 +80,12 @@ def _render_extra_keys(obj: dict[str, Any], handled_keys: set[str]) -> list[str]
76
80
  return lines
77
81
 
78
82
 
79
- # The Nemotron-3 family ships two chat-template variants. Nano / Super share
83
+ # The Nemotron family ships three chat-template variants. Nano / Super share
80
84
  # one (renderer ``Nemotron3Renderer`` / config ``name="nemotron-3"``); Ultra
81
85
  # differs in the reasoning-block glue — no ``\n`` around ``</think>`` — and is
82
- # the ``Nemotron3UltraRenderer`` subclass (``name="nemotron-3-ultra"``). Which
86
+ # the ``Nemotron3UltraRenderer`` subclass (``name="nemotron-3-ultra"``);
87
+ # Nemotron 3.5 (Lightning) uses the Ultra glue but defines no effort kwarg and
88
+ # is the ``Nemotron35Renderer`` subclass (``name="nemotron-3.5"``). Which
83
89
  # variant a checkpoint uses is carried by ``MODEL_RENDERER_MAP``, so the right
84
90
  # renderer class is constructed and the variant is encoded by the class itself.
85
91
 
@@ -852,3 +858,16 @@ class Nemotron3UltraRenderer(Nemotron3Renderer):
852
858
 
853
859
  _config_cls = Nemotron3UltraRendererConfig
854
860
  _ultra = True
861
+
862
+
863
+ class Nemotron35Renderer(Nemotron3Renderer):
864
+ """Renderer for Nemotron **3.5** (Lightning).
865
+
866
+ The 3.5 chat template is the Ultra variant's minus the effort kwarg: same
867
+ reasoning-block glue (carried by the ``_ultra`` class hook), but its Jinja
868
+ defines no reasoning-effort variable, so the config exposes none and the
869
+ effort hint is always empty.
870
+ """
871
+
872
+ _config_cls = Nemotron35RendererConfig
873
+ _ultra = True
@@ -36,11 +36,18 @@ RENDERER_MODELS = [
36
36
  # Ultra resolves to the `nemotron-3-ultra` config variant via the model
37
37
  # name (auto → MODEL_RENDERER_MAP → nemotron-3-ultra).
38
38
  ("nvidia/NVIDIA-Nemotron-3-Ultra-550B-A55B-BF16", "auto"),
39
+ # Nemotron 3.5 (Lightning): Ultra's template variant minus the effort
40
+ # kwarg (auto → MODEL_RENDERER_MAP → nemotron-3.5).
41
+ ("nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-BF16", "auto"),
39
42
  ("poolside/Laguna-XS.2", "auto"),
40
43
  ("poolside/Laguna-M.1", "auto"),
41
44
  # XS-2.1 resolves to `LagunaXS21Renderer` via the model name — its
42
45
  # chat template differs from XS.2's (see renderers/laguna_xs2.py).
43
46
  ("poolside/Laguna-XS-2.1", "auto"),
47
+ # S-2.1 is a larger sibling of XS-2.1 sharing its tokenizer but NOT its
48
+ # chat template (S-2.1 defaults enable_thinking=True and adds a
49
+ # preserve_thinking gate), so it resolves to LagunaS21Renderer.
50
+ ("poolside/Laguna-S-2.1", "auto"),
44
51
  # DeepSeek-V3/R1 are intentionally NOT in this shared barrage: their
45
52
  # chat templates can't render the barrage's tool-call fixtures (the
46
53
  # templates require ``tool['type']`` and a string-serialized
@@ -33,6 +33,7 @@ _BRIDGE_MODELS = [
33
33
  ("moonshotai/Kimi-K2-Instruct", "auto"),
34
34
  ("moonshotai/Kimi-K2.5", "auto"),
35
35
  ("nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16", "auto"),
36
+ ("nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-BF16", "auto"),
36
37
  ("tencent/Hy3", "auto"),
37
38
  ("unsloth/Llama-3.2-1B-Instruct", "llama-3"),
38
39
  ("openai/gpt-oss-20b", "gpt-oss"),
@@ -0,0 +1,128 @@
1
+ """Laguna-S-2.1 focused tests.
2
+
3
+ S-2.1 shares XS-2.1's tokenizer and token format, so the shared matrices
4
+ (conftest / config-parity) already assert byte parity on the common shapes
5
+ via :class:`LagunaS21Renderer`. This file pins the two behaviours that make
6
+ S-2.1 *differ* from XS-2.1 — and that the shared barrage can't reach because
7
+ it never sets the relevant kwargs:
8
+
9
+ - ``enable_thinking`` defaults to ``True`` (XS-2.1 defaults ``False``), so the
10
+ auto-resolved renderer renders reasoning by default, and
11
+ - the new ``preserve_thinking`` kwarg keeps historical ``<think>`` blocks even
12
+ while ``enable_thinking`` is ``False`` — the template gates reasoning display
13
+ on ``enable_thinking or preserve_thinking``.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from functools import lru_cache
19
+ from itertools import product
20
+
21
+ from renderers import create_renderer
22
+ from renderers.base import load_tokenizer
23
+ from renderers.configs import LagunaS21RendererConfig
24
+ from renderers.laguna_s21 import LagunaS21Renderer
25
+
26
+ _MODEL = "poolside/Laguna-S-2.1"
27
+
28
+
29
+ @lru_cache(maxsize=None)
30
+ def _tok():
31
+ return load_tokenizer(_MODEL)
32
+
33
+
34
+ def _renderer(**config_kwargs) -> LagunaS21Renderer:
35
+ renderer = create_renderer(_tok(), LagunaS21RendererConfig(**config_kwargs))
36
+ assert isinstance(renderer, LagunaS21Renderer)
37
+ return renderer
38
+
39
+
40
+ def _expected(msgs, *, add_generation_prompt=False, **template_kwargs):
41
+ return list(
42
+ _tok().apply_chat_template(
43
+ msgs,
44
+ add_generation_prompt=add_generation_prompt,
45
+ tokenize=True,
46
+ return_dict=False,
47
+ **template_kwargs,
48
+ )
49
+ )
50
+
51
+
52
+ def test_auto_resolves_to_s21_renderer_with_thinking_on_by_default():
53
+ """``poolside/Laguna-S-2.1`` resolves to :class:`LagunaS21Renderer`, and
54
+ its config defaults ``enable_thinking=True`` / ``preserve_thinking=False``
55
+ — matching S-2.1's template defaults (unlike XS-2.1's thinking-off)."""
56
+ r = create_renderer(_tok()) # auto-resolve via MODEL_RENDERER_MAP
57
+ assert isinstance(r, LagunaS21Renderer)
58
+ assert r.config.name == "laguna-s-2.1"
59
+ assert r.config.enable_thinking is True
60
+ assert r.config.preserve_thinking is False
61
+
62
+
63
+ def test_default_renders_empty_think_wrapper():
64
+ """With thinking on by default, a reasoning-free assistant turn still
65
+ opens with the empty ``<think></think>`` wrapper (this is exactly the
66
+ render_ids divergence from the XS-2.1 renderer)."""
67
+ msgs = [
68
+ {"role": "user", "content": "What is 2+2?"},
69
+ {"role": "assistant", "content": "4"},
70
+ ]
71
+ r = _renderer() # defaults: enable_thinking=True
72
+ ours = r.render_ids(msgs)
73
+ assert ours == _expected(msgs)
74
+ assert "<assistant><think></think>4</assistant>" in _tok().decode(ours)
75
+
76
+
77
+ def test_preserve_thinking_keeps_history_when_thinking_off():
78
+ """``enable_thinking=False`` alone drops reasoning, but with
79
+ ``preserve_thinking=True`` the historical ``<think>{reasoning}</think>``
80
+ survives — matching the template's ``enable_thinking or preserve_thinking``
81
+ gate."""
82
+ msgs = [
83
+ {"role": "user", "content": "What is 2+2?"},
84
+ {"role": "assistant", "reasoning_content": "Simple arithmetic", "content": "4"},
85
+ ]
86
+ r = _renderer(enable_thinking=False, preserve_thinking=True)
87
+ ours = r.render_ids(msgs)
88
+ assert ours == _expected(msgs, enable_thinking=False, preserve_thinking=True)
89
+ assert "<think>Simple arithmetic</think>" in _tok().decode(ours)
90
+
91
+
92
+ def test_no_preserve_thinking_drops_history_when_thinking_off():
93
+ """With both flags off the gate collapses to ``enable_thinking`` and the
94
+ turn opens with a bare ``</think>``, dropping the reasoning — identical to
95
+ the XS-2.1 renderer's default."""
96
+ msgs = [
97
+ {"role": "user", "content": "What is 2+2?"},
98
+ {"role": "assistant", "reasoning_content": "Simple arithmetic", "content": "4"},
99
+ ]
100
+ r = _renderer(enable_thinking=False, preserve_thinking=False)
101
+ ours = r.render_ids(msgs)
102
+ assert ours == _expected(msgs, enable_thinking=False, preserve_thinking=False)
103
+ assert "Simple arithmetic" not in _tok().decode(ours)
104
+
105
+
106
+ def test_all_thinking_flag_combos_match_template():
107
+ """Byte parity against ``apply_chat_template`` for every
108
+ ``enable_thinking`` × ``preserve_thinking`` combination, over a multi-turn
109
+ conversation carrying historical reasoning."""
110
+ msgs = [
111
+ {"role": "user", "content": "Q1"},
112
+ {"role": "assistant", "reasoning_content": "deduce", "content": "A1"},
113
+ {"role": "user", "content": "Q2"},
114
+ {"role": "assistant", "reasoning_content": "more", "content": "A2"},
115
+ ]
116
+ for enable_thinking, preserve_thinking in product((False, True), repeat=2):
117
+ r = _renderer(
118
+ enable_thinking=enable_thinking, preserve_thinking=preserve_thinking
119
+ )
120
+ for add_generation_prompt in (False, True):
121
+ assert r.render_ids(
122
+ msgs, add_generation_prompt=add_generation_prompt
123
+ ) == _expected(
124
+ msgs,
125
+ add_generation_prompt=add_generation_prompt,
126
+ enable_thinking=enable_thinking,
127
+ preserve_thinking=preserve_thinking,
128
+ ), (enable_thinking, preserve_thinking, add_generation_prompt)
@@ -25,9 +25,10 @@ Every assertion compares ``renderer.render_ids(...)`` to
25
25
  renderer is byte-for-byte faithful for that case. Tokenizers are loaded from
26
26
  the local HF cache (offline); no network.
27
27
 
28
- The variants split across two configs: ``nemotron-3`` (Nano / Super, with
29
- ``low_effort``) and ``nemotron-3-ultra`` (Ultra, with ``medium_effort``). The
30
- helper resolves the right config class per model from ``MODEL_RENDERER_MAP``.
28
+ The variants split across three configs: ``nemotron-3`` (Nano / Super, with
29
+ ``low_effort``), ``nemotron-3-ultra`` (Ultra, with ``medium_effort``), and
30
+ ``nemotron-3.5`` (Lightning Ultra's glue, no effort kwarg). The helper
31
+ resolves the right config class per model from ``MODEL_RENDERER_MAP``.
31
32
  """
32
33
 
33
34
  from __future__ import annotations
@@ -44,7 +45,8 @@ from renderers.configs import _config_class_for
44
45
  NANO = "nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16"
45
46
  SUPER = "nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-BF16"
46
47
  ULTRA = "nvidia/NVIDIA-Nemotron-3-Ultra-550B-A55B-BF16"
47
- MODELS = [NANO, SUPER, ULTRA]
48
+ LIGHTNING = "nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-BF16"
49
+ MODELS = [NANO, SUPER, ULTRA, LIGHTNING]
48
50
 
49
51
 
50
52
  @lru_cache
@@ -107,7 +109,9 @@ def _assert_parity(
107
109
  )
108
110
 
109
111
 
110
- pytestmark = pytest.mark.parametrize("model", MODELS, ids=["nano", "super", "ultra"])
112
+ pytestmark = pytest.mark.parametrize(
113
+ "model", MODELS, ids=["nano", "super", "ultra", "lightning"]
114
+ )
111
115
 
112
116
 
113
117
  TOOLS = [
@@ -648,8 +652,8 @@ _EFFORT_SHAPES = [
648
652
  def test_low_effort_kwarg(model, flag, shape, extra):
649
653
  """``low_effort`` appends ``\\n\\n{reasoning effort: low}`` to the last user
650
654
  message on **Super**; it's a no-op on **Nano** (its template never defines
651
- it). Ultra's config has no such field, so it's skipped."""
652
- if model == ULTRA:
655
+ it). The Ultra / Lightning configs have no such field, so they're skipped."""
656
+ if model in (ULTRA, LIGHTNING):
653
657
  pytest.skip("low_effort is a nemotron-3 (Nano/Super) kwarg")
654
658
  _assert_parity(model, shape, low_effort=flag, **extra)
655
659
 
@@ -660,7 +664,7 @@ def test_low_effort_kwarg(model, flag, shape, extra):
660
664
  )
661
665
  def test_medium_effort_kwarg(model, flag, shape, extra):
662
666
  """``medium_effort`` appends ``\\n\\n{reasoning effort: efficient}`` on
663
- **Ultra**. Nano/Super configs have no such field, so they're skipped."""
667
+ **Ultra**. The other configs have no such field, so they're skipped."""
664
668
  if model != ULTRA:
665
669
  pytest.skip("medium_effort is a nemotron-3-ultra kwarg")
666
670
  _assert_parity(model, shape, medium_effort=flag, **extra)
@@ -668,9 +672,12 @@ def test_medium_effort_kwarg(model, flag, shape, extra):
668
672
 
669
673
  def test_effort_kwarg_lives_on_the_right_variant(model):
670
674
  """Each effort kwarg is declared only on the variant whose template defines
671
- it — the discriminated union rejects the wrong combination at config load."""
675
+ it — the discriminated union rejects the wrong combination at config load.
676
+ Lightning's template defines no effort variable, so its config has neither."""
672
677
  fields = _config_cls(model).template_field_names()
673
678
  if model == ULTRA:
674
679
  assert "medium_effort" in fields and "low_effort" not in fields
680
+ elif model == LIGHTNING:
681
+ assert "low_effort" not in fields and "medium_effort" not in fields
675
682
  else:
676
683
  assert "low_effort" in fields and "medium_effort" not in fields
@@ -1,4 +1,4 @@
1
- """Offline wiring tests for the Nemotron-3 variant split.
1
+ """Offline wiring tests for the Nemotron variant split.
2
2
 
3
3
  Assert the model→renderer mapping, the per-variant typed-config surface, and
4
4
  the name-based ``low_effort`` gating WITHOUT loading any tokenizer (no
@@ -6,15 +6,16 @@ network). This pins the wiring the parity matrix can't reach — in particular
6
6
  the FP8 Ultra entry, which no test loads a tokenizer for — so it can't
7
7
  silently rot.
8
8
 
9
- The two variants:
9
+ The three variants:
10
10
 
11
11
  * ``nemotron-3`` — Nano / Super, shared template. Config exposes ``low_effort``
12
12
  (honoured on Super, a no-op on Nano).
13
13
  * ``nemotron-3-ultra`` — Ultra, distinct ``</think>`` glue. Config exposes
14
14
  ``medium_effort``.
15
+ * ``nemotron-3.5`` — Lightning, Ultra's glue but no effort kwarg at all.
15
16
 
16
- Both route to the one ``Nemotron3Renderer`` class, which selects the variant
17
- from ``config.name``.
17
+ All route to the one ``Nemotron3Renderer`` implementation via per-variant
18
+ subclasses.
18
19
  """
19
20
 
20
21
  from types import SimpleNamespace
@@ -23,9 +24,15 @@ from renderers.base import MODEL_RENDERER_MAP, RENDERER_REGISTRY, _populate_regi
23
24
  from renderers.configs import (
24
25
  Nemotron3RendererConfig,
25
26
  Nemotron3UltraRendererConfig,
27
+ Nemotron35RendererConfig,
26
28
  _config_class_for,
27
29
  )
28
- from renderers.nemotron3 import Nemotron3Renderer, Nemotron3UltraRenderer, _is_super
30
+ from renderers.nemotron3 import (
31
+ Nemotron3Renderer,
32
+ Nemotron3UltraRenderer,
33
+ Nemotron35Renderer,
34
+ _is_super,
35
+ )
29
36
 
30
37
  _ULTRA_REPOS = [
31
38
  "nvidia/NVIDIA-Nemotron-3-Ultra-550B-A55B-BF16",
@@ -35,6 +42,9 @@ _NANO_SUPER_REPOS = [
35
42
  "nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16",
36
43
  "nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-BF16",
37
44
  ]
45
+ _LIGHTNING_REPOS = [
46
+ "nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-BF16",
47
+ ]
38
48
 
39
49
 
40
50
  def _fake_tok(name):
@@ -46,18 +56,23 @@ def test_models_map_to_their_variant():
46
56
  assert MODEL_RENDERER_MAP.get(repo) == "nemotron-3-ultra", repo
47
57
  for repo in _NANO_SUPER_REPOS:
48
58
  assert MODEL_RENDERER_MAP.get(repo) == "nemotron-3", repo
59
+ for repo in _LIGHTNING_REPOS:
60
+ assert MODEL_RENDERER_MAP.get(repo) == "nemotron-3.5", repo
49
61
 
50
62
 
51
63
  def test_each_discriminator_maps_to_its_config_and_renderer_class():
52
64
  # Config discriminator → config class.
53
65
  assert _config_class_for("nemotron-3") is Nemotron3RendererConfig
54
66
  assert _config_class_for("nemotron-3-ultra") is Nemotron3UltraRendererConfig
55
- # Registry → renderer class (Ultra is a sibling subclass, matching the
56
- # GLM-5/5.1 and Qwen3.5/3.6 house style not one class under two names).
67
+ assert _config_class_for("nemotron-3.5") is Nemotron35RendererConfig
68
+ # Registry renderer class (Ultra / 3.5 are sibling subclasses, matching
69
+ # the GLM-5/5.1 and Qwen3.5/3.6 house style — not one class under N names).
57
70
  _populate_registry()
58
71
  assert RENDERER_REGISTRY["nemotron-3"] is Nemotron3Renderer
59
72
  assert RENDERER_REGISTRY["nemotron-3-ultra"] is Nemotron3UltraRenderer
73
+ assert RENDERER_REGISTRY["nemotron-3.5"] is Nemotron35Renderer
60
74
  assert issubclass(Nemotron3UltraRenderer, Nemotron3Renderer)
75
+ assert issubclass(Nemotron35Renderer, Nemotron3Renderer)
61
76
 
62
77
 
63
78
  def test_variant_is_encoded_by_the_class():
@@ -66,8 +81,10 @@ def test_variant_is_encoded_by_the_class():
66
81
  # config.name → class). Default config also follows the class.
67
82
  assert Nemotron3Renderer._ultra is False
68
83
  assert Nemotron3UltraRenderer._ultra is True
84
+ assert Nemotron35Renderer._ultra is True
69
85
  assert Nemotron3Renderer._config_cls is Nemotron3RendererConfig
70
86
  assert Nemotron3UltraRenderer._config_cls is Nemotron3UltraRendererConfig
87
+ assert Nemotron35Renderer._config_cls is Nemotron35RendererConfig
71
88
 
72
89
 
73
90
  def test_template_fields_per_variant():
@@ -80,6 +97,10 @@ def test_template_fields_per_variant():
80
97
  assert Nemotron3UltraRendererConfig.template_field_names() == frozenset(
81
98
  {"enable_thinking", "truncate_history_thinking", "medium_effort"}
82
99
  )
100
+ # Lightning's template defines no effort variable at all.
101
+ assert Nemotron35RendererConfig.template_field_names() == frozenset(
102
+ {"enable_thinking", "truncate_history_thinking"}
103
+ )
83
104
 
84
105
 
85
106
  def test_configs_reject_the_other_variants_effort_kwarg():
@@ -91,6 +112,10 @@ def test_configs_reject_the_other_variants_effort_kwarg():
91
112
  Nemotron3RendererConfig(medium_effort=True) # type: ignore[call-arg]
92
113
  with pytest.raises(ValidationError):
93
114
  Nemotron3UltraRendererConfig(low_effort=True) # type: ignore[call-arg]
115
+ with pytest.raises(ValidationError):
116
+ Nemotron35RendererConfig(low_effort=True) # type: ignore[call-arg]
117
+ with pytest.raises(ValidationError):
118
+ Nemotron35RendererConfig(medium_effort=True) # type: ignore[call-arg]
94
119
  # And the removed ``ultra`` selector is gone entirely.
95
120
  with pytest.raises(ValidationError):
96
121
  Nemotron3RendererConfig(ultra=True) # type: ignore[call-arg]
@@ -63,9 +63,13 @@ _RENDERER_MODELS = [
63
63
  # Ultra: auto-resolves to the ``nemotron-3-ultra`` config via the model
64
64
  # name; parity asserted against the Ultra apply_chat_template (``medium_effort``).
65
65
  ("nvidia/NVIDIA-Nemotron-3-Ultra-550B-A55B-BF16", "auto"),
66
+ # Nemotron 3.5 (Lightning): Ultra's template minus the effort kwarg — its
67
+ # config declares only ``enable_thinking`` / ``truncate_history_thinking``.
68
+ ("nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-BF16", "auto"),
66
69
  ("poolside/Laguna-XS.2", "auto"),
67
70
  ("poolside/Laguna-M.1", "auto"),
68
71
  ("poolside/Laguna-XS-2.1", "auto"),
72
+ ("poolside/Laguna-S-2.1", "auto"),
69
73
  ("tencent/Hy3", "auto"),
70
74
  ("openai/gpt-oss-20b", "gpt-oss"),
71
75
  ]
@@ -46,6 +46,8 @@ _ROUNDTRIP_MODELS = [
46
46
  # Ultra: parse must recover content after a </think> glued directly to it
47
47
  # (no separating newline) — the Ultra-specific glue stresses the round-trip.
48
48
  ("nvidia/NVIDIA-Nemotron-3-Ultra-550B-A55B-BF16", "auto"),
49
+ # Nemotron 3.5 (Lightning): same glue as Ultra, distinct renderer class.
50
+ ("nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-BF16", "auto"),
49
51
  ("poolside/Laguna-XS.2", "auto"),
50
52
  ("poolside/Laguna-M.1", "auto"),
51
53
  # Laguna-XS-2.1 is deliberately absent: under the default