renderers 0.1.10.dev1__tar.gz → 0.1.10.dev4__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 (77) hide show
  1. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/PKG-INFO +2 -2
  2. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/README.md +1 -1
  3. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/docs/renderer-config.md +2 -1
  4. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/renderers/__init__.py +4 -0
  5. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/renderers/_version.py +2 -2
  6. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/renderers/base.py +9 -1
  7. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/renderers/configs.py +34 -0
  8. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/renderers/nemotron3.py +22 -3
  9. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/tests/conftest.py +3 -0
  10. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/tests/test_bridge.py +1 -0
  11. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/tests/test_nemotron3_parity.py +16 -9
  12. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/tests/test_nemotron3_ultra.py +32 -7
  13. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/tests/test_renderer_config_parity.py +3 -0
  14. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/tests/test_roundtrip.py +2 -0
  15. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/.github/workflows/publish-dev.yml +0 -0
  16. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/.github/workflows/publish.yml +0 -0
  17. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/.github/workflows/style.yml +0 -0
  18. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/.github/workflows/test.yml +0 -0
  19. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/.gitignore +0 -0
  20. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/.pre-commit-config.yaml +0 -0
  21. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/LICENSE +0 -0
  22. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/examples/README.md +0 -0
  23. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/examples/sglang/multiturn_generate_sglang.py +0 -0
  24. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/examples/sglang/online_multiturn_sglang.py +0 -0
  25. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/examples/tinker/multiturn_generate_tinker.py +0 -0
  26. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/examples/transformers/multiturn_generate_transformers.py +0 -0
  27. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/examples/vllm/multiturn_generate_vllm.py +0 -0
  28. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/pyproject.toml +0 -0
  29. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/renderers/client.py +0 -0
  30. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/renderers/deepseek_r1.py +0 -0
  31. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/renderers/deepseek_v3.py +0 -0
  32. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/renderers/default.py +0 -0
  33. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/renderers/glm45.py +0 -0
  34. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/renderers/glm5.py +0 -0
  35. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/renderers/gpt_oss.py +0 -0
  36. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/renderers/hy3.py +0 -0
  37. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/renderers/kimi_k2.py +0 -0
  38. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/renderers/kimi_k25.py +0 -0
  39. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/renderers/laguna_xs2.py +0 -0
  40. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/renderers/llama_3.py +0 -0
  41. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/renderers/minimax_m2.py +0 -0
  42. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/renderers/parsers.py +0 -0
  43. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/renderers/parsing.py +0 -0
  44. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/renderers/prime_qwen3.py +0 -0
  45. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/renderers/qwen3.py +0 -0
  46. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/renderers/qwen35.py +0 -0
  47. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/renderers/qwen36.py +0 -0
  48. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/renderers/qwen3_vl.py +0 -0
  49. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/tests/test_build_helpers.py +0 -0
  50. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/tests/test_client.py +0 -0
  51. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/tests/test_deepseek_r1.py +0 -0
  52. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/tests/test_disabled_thinking_stability.py +0 -0
  53. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/tests/test_glm_tool_name_validation.py +0 -0
  54. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/tests/test_gpt_oss_harmony_parity.py +0 -0
  55. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/tests/test_hy3.py +0 -0
  56. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/tests/test_incremental.py +0 -0
  57. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/tests/test_is_content.py +0 -0
  58. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/tests/test_kimi_k25_tool_schema.py +0 -0
  59. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/tests/test_laguna_m1.py +0 -0
  60. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/tests/test_laguna_xs21.py +0 -0
  61. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/tests/test_llama_3.py +0 -0
  62. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/tests/test_load_tokenizer.py +0 -0
  63. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/tests/test_message_indices.py +0 -0
  64. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/tests/test_message_tool_names.py +0 -0
  65. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/tests/test_multimodal.py +0 -0
  66. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/tests/test_parse_response.py +0 -0
  67. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/tests/test_parse_response_robustness.py +0 -0
  68. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/tests/test_parsers.py +0 -0
  69. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/tests/test_preserve_thinking.py +0 -0
  70. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/tests/test_prime_qwen3_parity.py +0 -0
  71. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/tests/test_qwen35_size_coverage.py +0 -0
  72. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/tests/test_render_ids.py +0 -0
  73. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/tests/test_renderer_config.py +0 -0
  74. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/tests/test_sampled_mask.py +0 -0
  75. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/tests/test_tokens_per_message.py +0 -0
  76. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/tests/test_tool_arg_type_preservation.py +0 -0
  77. {renderers-0.1.10.dev1 → renderers-0.1.10.dev4}/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.dev4
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-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-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
 
@@ -43,6 +43,7 @@ chat-template kwargs. Those fields are covered by parity tests against
43
43
  | MiniMax M2 | `MiniMaxM2RendererConfig` | `model_identity` | - |
44
44
  | Nemotron-3 Nano / Super | `Nemotron3RendererConfig` | `enable_thinking`, `truncate_history_thinking`, `low_effort` | - |
45
45
  | Nemotron-3 Ultra | `Nemotron3UltraRendererConfig` | `enable_thinking`, `truncate_history_thinking`, `medium_effort` | - |
46
+ | Nemotron-3.5 Lightning | `Nemotron35RendererConfig` | `enable_thinking`, `truncate_history_thinking` | - |
46
47
  | DeepSeek V3 | `DeepSeekV3RendererConfig` | - | - |
47
48
  | DeepSeek R1 | `DeepSeekR1RendererConfig` | - | - |
48
49
 
@@ -134,7 +135,7 @@ the knobs its template actually exposes:
134
135
  | gpt-oss | `auto_drop_analysis=False -> all`, else `tool_cycle` |
135
136
  | Hy3 | `preserved_thinking=True -> all`, else `tool_cycle` |
136
137
  | 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` |
138
+ | Nemotron-3 / 3.5 | `truncate_history_thinking=False -> all`; else `enable_thinking=False -> all`; else `tool_cycle` |
138
139
  | DeepSeek R1 | `template` |
139
140
  | MiniMax M2 | `tool_cycle` |
140
141
  | DeepSeek V3, Qwen3-VL, Kimi K2, Laguna XS.2 / M.1 / XS-2.1, Llama 3 | `all` |
@@ -59,6 +59,7 @@ from renderers.configs import (
59
59
  LagunaXS21RendererConfig,
60
60
  Llama3RendererConfig,
61
61
  MiniMaxM2RendererConfig,
62
+ Nemotron35RendererConfig,
62
63
  Nemotron3RendererConfig,
63
64
  Nemotron3UltraRendererConfig,
64
65
  PrimeQwen3RendererConfig,
@@ -96,6 +97,7 @@ _LAZY_RENDERERS: dict[str, str] = {
96
97
  "LagunaXS2Renderer": "renderers.laguna_xs2",
97
98
  "Llama3Renderer": "renderers.llama_3",
98
99
  "MiniMaxM2Renderer": "renderers.minimax_m2",
100
+ "Nemotron35Renderer": "renderers.nemotron3",
99
101
  "Nemotron3Renderer": "renderers.nemotron3",
100
102
  "Nemotron3UltraRenderer": "renderers.nemotron3",
101
103
  "PrimeQwen3Renderer": "renderers.prime_qwen3",
@@ -162,6 +164,8 @@ __all__ = [
162
164
  "MiniMaxM2RendererConfig",
163
165
  "MultiModalData",
164
166
  "MultimodalRenderer",
167
+ "Nemotron35Renderer",
168
+ "Nemotron35RendererConfig",
165
169
  "Nemotron3Renderer",
166
170
  "Nemotron3RendererConfig",
167
171
  "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.dev4'
22
+ __version_tuple__ = version_tuple = (0, 1, 10, 'dev4')
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
@@ -1332,7 +1335,11 @@ def _populate_registry():
1332
1335
  )
1333
1336
  from renderers.llama_3 import Llama3Renderer
1334
1337
  from renderers.minimax_m2 import MiniMaxM2Renderer
1335
- from renderers.nemotron3 import Nemotron3Renderer, Nemotron3UltraRenderer
1338
+ from renderers.nemotron3 import (
1339
+ Nemotron3Renderer,
1340
+ Nemotron3UltraRenderer,
1341
+ Nemotron35Renderer,
1342
+ )
1336
1343
  from renderers.prime_qwen3 import PrimeQwen3Renderer
1337
1344
  from renderers.qwen3 import Qwen3Renderer
1338
1345
  from renderers.qwen3_vl import Qwen3VLRenderer
@@ -1362,6 +1369,7 @@ def _populate_registry():
1362
1369
  "llama-3": Llama3Renderer,
1363
1370
  "nemotron-3": Nemotron3Renderer,
1364
1371
  "nemotron-3-ultra": Nemotron3UltraRenderer,
1372
+ "nemotron-3.5": Nemotron35Renderer,
1365
1373
  "gpt-oss": GptOssRenderer,
1366
1374
  }
1367
1375
  )
@@ -657,6 +657,37 @@ class Nemotron3UltraRendererConfig(BaseRendererConfig):
657
657
  user message. Mirrors the Ultra chat template's ``medium_effort`` kwarg."""
658
658
 
659
659
 
660
+ class Nemotron35RendererConfig(BaseRendererConfig):
661
+ """Nemotron-3.5 (Lightning) renderer config.
662
+
663
+ Nemotron 3.5's chat template is the Ultra variant's minus the effort
664
+ kwarg: same reasoning-block glue (no ``\\n`` around ``</think>``, no
665
+ trim on truncated tool-call history), but its Jinja defines no
666
+ reasoning-effort variable at all. It shares the
667
+ :class:`renderers.nemotron3.Nemotron3Renderer` implementation via the
668
+ :class:`renderers.nemotron3.Nemotron35Renderer` subclass and is reached
669
+ via the ``nemotron-3.5`` discriminator.
670
+ """
671
+
672
+ name: Literal["nemotron-3.5"] = "nemotron-3.5"
673
+
674
+ enable_thinking: bool = True
675
+ """See :class:`Nemotron3RendererConfig.enable_thinking`."""
676
+
677
+ truncate_history_thinking: bool = True
678
+ """See :class:`Nemotron3RendererConfig.truncate_history_thinking`."""
679
+
680
+ @model_validator(mode="after")
681
+ def _check_thinking_retention(self):
682
+ _reject_thinking_retention_conflict(
683
+ self,
684
+ "truncate_history_thinking",
685
+ true_implies="tool_cycle",
686
+ false_implies="all",
687
+ )
688
+ return self
689
+
690
+
660
691
  class DeepSeekV3RendererConfig(BaseRendererConfig):
661
692
  """DeepSeek-V3 renderer config (non-reasoning).
662
693
 
@@ -708,6 +739,7 @@ RendererConfig = Annotated[
708
739
  MiniMaxM2RendererConfig,
709
740
  Nemotron3RendererConfig,
710
741
  Nemotron3UltraRendererConfig,
742
+ Nemotron35RendererConfig,
711
743
  DeepSeekV3RendererConfig,
712
744
  DeepSeekR1RendererConfig,
713
745
  ],
@@ -749,6 +781,7 @@ _CONFIG_BY_NAME: dict[str, type[BaseRendererConfig]] = {
749
781
  "minimax-m2": MiniMaxM2RendererConfig,
750
782
  "nemotron-3": Nemotron3RendererConfig,
751
783
  "nemotron-3-ultra": Nemotron3UltraRendererConfig,
784
+ "nemotron-3.5": Nemotron35RendererConfig,
752
785
  "deepseek-v3": DeepSeekV3RendererConfig,
753
786
  "deepseek-r1": DeepSeekR1RendererConfig,
754
787
  }
@@ -796,6 +829,7 @@ __all__ = [
796
829
  "LagunaXS21RendererConfig",
797
830
  "Llama3RendererConfig",
798
831
  "MiniMaxM2RendererConfig",
832
+ "Nemotron35RendererConfig",
799
833
  "Nemotron3RendererConfig",
800
834
  "Nemotron3UltraRendererConfig",
801
835
  "PrimeQwen3RendererConfig",
@@ -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,6 +36,9 @@ 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
@@ -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"),
@@ -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,6 +63,9 @@ _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"),
@@ -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
File without changes
File without changes