renderers 0.1.9.dev10__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.
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/PKG-INFO +2 -2
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/README.md +1 -1
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/docs/renderer-config.md +4 -2
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/renderers/__init__.py +8 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/renderers/_version.py +2 -2
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/renderers/base.py +19 -4
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/renderers/configs.py +59 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/renderers/laguna_xs2.py +74 -19
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/renderers/nemotron3.py +22 -3
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/tests/conftest.py +4 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/tests/test_bridge.py +1 -0
- renderers-0.1.10.dev4/tests/test_laguna_m1.py +232 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/tests/test_nemotron3_parity.py +16 -9
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/tests/test_nemotron3_ultra.py +32 -7
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/tests/test_renderer_config_parity.py +5 -1
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/tests/test_roundtrip.py +3 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/.github/workflows/publish-dev.yml +0 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/.github/workflows/publish.yml +0 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/.github/workflows/style.yml +0 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/.github/workflows/test.yml +0 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/.gitignore +0 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/.pre-commit-config.yaml +0 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/LICENSE +0 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/examples/README.md +0 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/examples/sglang/multiturn_generate_sglang.py +0 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/examples/sglang/online_multiturn_sglang.py +0 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/examples/tinker/multiturn_generate_tinker.py +0 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/examples/transformers/multiturn_generate_transformers.py +0 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/examples/vllm/multiturn_generate_vllm.py +0 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/pyproject.toml +0 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/renderers/client.py +0 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/renderers/deepseek_r1.py +0 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/renderers/deepseek_v3.py +0 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/renderers/default.py +0 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/renderers/glm45.py +0 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/renderers/glm5.py +0 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/renderers/gpt_oss.py +0 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/renderers/hy3.py +0 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/renderers/kimi_k2.py +0 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/renderers/kimi_k25.py +0 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/renderers/llama_3.py +0 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/renderers/minimax_m2.py +0 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/renderers/parsers.py +0 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/renderers/parsing.py +0 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/renderers/prime_qwen3.py +0 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/renderers/qwen3.py +0 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/renderers/qwen35.py +0 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/renderers/qwen36.py +0 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/renderers/qwen3_vl.py +0 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/tests/test_build_helpers.py +0 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/tests/test_client.py +0 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/tests/test_deepseek_r1.py +0 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/tests/test_disabled_thinking_stability.py +0 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/tests/test_glm_tool_name_validation.py +0 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/tests/test_gpt_oss_harmony_parity.py +0 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/tests/test_hy3.py +0 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/tests/test_incremental.py +0 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/tests/test_is_content.py +0 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/tests/test_kimi_k25_tool_schema.py +0 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/tests/test_laguna_xs21.py +0 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/tests/test_llama_3.py +0 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/tests/test_load_tokenizer.py +0 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/tests/test_message_indices.py +0 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/tests/test_message_tool_names.py +0 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/tests/test_multimodal.py +0 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/tests/test_parse_response.py +0 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/tests/test_parse_response_robustness.py +0 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/tests/test_parsers.py +0 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/tests/test_preserve_thinking.py +0 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/tests/test_prime_qwen3_parity.py +0 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/tests/test_qwen35_size_coverage.py +0 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/tests/test_render_ids.py +0 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/tests/test_renderer_config.py +0 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/tests/test_sampled_mask.py +0 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/tests/test_tokens_per_message.py +0 -0
- {renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/tests/test_tool_arg_type_preservation.py +0 -0
- {renderers-0.1.9.dev10 → 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.
|
|
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`, `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`, `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
|
|
|
@@ -37,11 +37,13 @@ chat-template kwargs. Those fields are covered by parity tests against
|
|
|
37
37
|
| Kimi K2 | `KimiK2RendererConfig` | - | `enable_thinking` |
|
|
38
38
|
| Kimi K2.5 / 2.6 | `KimiK25RendererConfig` | `thinking` | `image_cache_max` |
|
|
39
39
|
| Laguna XS.2 | `LagunaXS2RendererConfig` | `enable_thinking`, `render_assistant_messages_raw` | - |
|
|
40
|
+
| Laguna M.1 | `LagunaM1RendererConfig` | `enable_thinking`, `render_assistant_messages_raw` | - |
|
|
40
41
|
| Laguna XS-2.1 | `LagunaXS21RendererConfig` | `enable_thinking` | - |
|
|
41
42
|
| Llama 3 | `Llama3RendererConfig` | `date_string`, `tools_in_user_message` | - |
|
|
42
43
|
| MiniMax M2 | `MiniMaxM2RendererConfig` | `model_identity` | - |
|
|
43
44
|
| Nemotron-3 Nano / Super | `Nemotron3RendererConfig` | `enable_thinking`, `truncate_history_thinking`, `low_effort` | - |
|
|
44
45
|
| Nemotron-3 Ultra | `Nemotron3UltraRendererConfig` | `enable_thinking`, `truncate_history_thinking`, `medium_effort` | - |
|
|
46
|
+
| Nemotron-3.5 Lightning | `Nemotron35RendererConfig` | `enable_thinking`, `truncate_history_thinking` | - |
|
|
45
47
|
| DeepSeek V3 | `DeepSeekV3RendererConfig` | - | - |
|
|
46
48
|
| DeepSeek R1 | `DeepSeekR1RendererConfig` | - | - |
|
|
47
49
|
|
|
@@ -133,10 +135,10 @@ the knobs its template actually exposes:
|
|
|
133
135
|
| gpt-oss | `auto_drop_analysis=False -> all`, else `tool_cycle` |
|
|
134
136
|
| Hy3 | `preserved_thinking=True -> all`, else `tool_cycle` |
|
|
135
137
|
| Kimi K2.5 / 2.6 | `thinking=False -> all`, else `tool_cycle` |
|
|
136
|
-
| 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` |
|
|
137
139
|
| DeepSeek R1 | `template` |
|
|
138
140
|
| MiniMax M2 | `tool_cycle` |
|
|
139
|
-
| DeepSeek V3, Qwen3-VL, Kimi K2, Laguna XS.2 / XS-2.1, Llama 3 | `all` |
|
|
141
|
+
| DeepSeek V3, Qwen3-VL, Kimi K2, Laguna XS.2 / M.1 / XS-2.1, Llama 3 | `all` |
|
|
140
142
|
| PrimeIntellect Qwen3 | `all` |
|
|
141
143
|
|
|
142
144
|
Config construction raises when an explicit template knob directly contradicts
|
|
@@ -54,10 +54,12 @@ from renderers.configs import (
|
|
|
54
54
|
Hy3RendererConfig,
|
|
55
55
|
KimiK25RendererConfig,
|
|
56
56
|
KimiK2RendererConfig,
|
|
57
|
+
LagunaM1RendererConfig,
|
|
57
58
|
LagunaXS2RendererConfig,
|
|
58
59
|
LagunaXS21RendererConfig,
|
|
59
60
|
Llama3RendererConfig,
|
|
60
61
|
MiniMaxM2RendererConfig,
|
|
62
|
+
Nemotron35RendererConfig,
|
|
61
63
|
Nemotron3RendererConfig,
|
|
62
64
|
Nemotron3UltraRendererConfig,
|
|
63
65
|
PrimeQwen3RendererConfig,
|
|
@@ -90,10 +92,12 @@ _LAZY_RENDERERS: dict[str, str] = {
|
|
|
90
92
|
"Hy3Renderer": "renderers.hy3",
|
|
91
93
|
"KimiK25Renderer": "renderers.kimi_k25",
|
|
92
94
|
"KimiK2Renderer": "renderers.kimi_k2",
|
|
95
|
+
"LagunaM1Renderer": "renderers.laguna_xs2",
|
|
93
96
|
"LagunaXS21Renderer": "renderers.laguna_xs2",
|
|
94
97
|
"LagunaXS2Renderer": "renderers.laguna_xs2",
|
|
95
98
|
"Llama3Renderer": "renderers.llama_3",
|
|
96
99
|
"MiniMaxM2Renderer": "renderers.minimax_m2",
|
|
100
|
+
"Nemotron35Renderer": "renderers.nemotron3",
|
|
97
101
|
"Nemotron3Renderer": "renderers.nemotron3",
|
|
98
102
|
"Nemotron3UltraRenderer": "renderers.nemotron3",
|
|
99
103
|
"PrimeQwen3Renderer": "renderers.prime_qwen3",
|
|
@@ -145,6 +149,8 @@ __all__ = [
|
|
|
145
149
|
"KimiK25RendererConfig",
|
|
146
150
|
"KimiK2Renderer",
|
|
147
151
|
"KimiK2RendererConfig",
|
|
152
|
+
"LagunaM1Renderer",
|
|
153
|
+
"LagunaM1RendererConfig",
|
|
148
154
|
"LagunaXS2Renderer",
|
|
149
155
|
"LagunaXS2RendererConfig",
|
|
150
156
|
"LagunaXS21Renderer",
|
|
@@ -158,6 +164,8 @@ __all__ = [
|
|
|
158
164
|
"MiniMaxM2RendererConfig",
|
|
159
165
|
"MultiModalData",
|
|
160
166
|
"MultimodalRenderer",
|
|
167
|
+
"Nemotron35Renderer",
|
|
168
|
+
"Nemotron35RendererConfig",
|
|
161
169
|
"Nemotron3Renderer",
|
|
162
170
|
"Nemotron3RendererConfig",
|
|
163
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.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 1,
|
|
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
|
|
@@ -117,6 +117,7 @@ class Message(TypedDict, total=False):
|
|
|
117
117
|
tool_calls: list[ToolCall]
|
|
118
118
|
tool_call_id: str
|
|
119
119
|
name: str
|
|
120
|
+
reasoning: str
|
|
120
121
|
reasoning_content: str
|
|
121
122
|
|
|
122
123
|
|
|
@@ -1064,6 +1065,9 @@ MODEL_RENDERER_MAP: dict[str, str] = {
|
|
|
1064
1065
|
"nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-BF16": "nemotron-3",
|
|
1065
1066
|
"nvidia/NVIDIA-Nemotron-3-Ultra-550B-A55B-BF16": "nemotron-3-ultra",
|
|
1066
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",
|
|
1067
1071
|
# Llama 3.2 (Instruct). Tested against the gated meta-llama repos and
|
|
1068
1072
|
# the unrestricted unsloth/... mirror, which ships a byte-identical
|
|
1069
1073
|
# chat template. ``Llama3Renderer`` defaults ``date_string`` to
|
|
@@ -1072,9 +1076,10 @@ MODEL_RENDERER_MAP: dict[str, str] = {
|
|
|
1072
1076
|
# construction to pin a different date.
|
|
1073
1077
|
"meta-llama/Llama-3.2-1B-Instruct": "llama-3",
|
|
1074
1078
|
"meta-llama/Llama-3.2-3B-Instruct": "llama-3",
|
|
1075
|
-
# Poolside Laguna.
|
|
1076
|
-
# each mirrored by its own renderer class.
|
|
1079
|
+
# Poolside Laguna. These checkpoints ship distinct chat templates,
|
|
1080
|
+
# each mirrored by its own renderer class/config discriminator.
|
|
1077
1081
|
"poolside/Laguna-XS.2": "laguna-xs.2",
|
|
1082
|
+
"poolside/Laguna-M.1": "laguna-m.1",
|
|
1078
1083
|
"poolside/Laguna-XS-2.1": "laguna-xs-2.1",
|
|
1079
1084
|
# GPT-OSS.
|
|
1080
1085
|
"openai/gpt-oss-20b": "gpt-oss",
|
|
@@ -1323,10 +1328,18 @@ def _populate_registry():
|
|
|
1323
1328
|
from renderers.hy3 import Hy3Renderer
|
|
1324
1329
|
from renderers.kimi_k2 import KimiK2Renderer
|
|
1325
1330
|
from renderers.kimi_k25 import KimiK25Renderer
|
|
1326
|
-
from renderers.laguna_xs2 import
|
|
1331
|
+
from renderers.laguna_xs2 import (
|
|
1332
|
+
LagunaM1Renderer,
|
|
1333
|
+
LagunaXS2Renderer,
|
|
1334
|
+
LagunaXS21Renderer,
|
|
1335
|
+
)
|
|
1327
1336
|
from renderers.llama_3 import Llama3Renderer
|
|
1328
1337
|
from renderers.minimax_m2 import MiniMaxM2Renderer
|
|
1329
|
-
from renderers.nemotron3 import
|
|
1338
|
+
from renderers.nemotron3 import (
|
|
1339
|
+
Nemotron3Renderer,
|
|
1340
|
+
Nemotron3UltraRenderer,
|
|
1341
|
+
Nemotron35Renderer,
|
|
1342
|
+
)
|
|
1330
1343
|
from renderers.prime_qwen3 import PrimeQwen3Renderer
|
|
1331
1344
|
from renderers.qwen3 import Qwen3Renderer
|
|
1332
1345
|
from renderers.qwen3_vl import Qwen3VLRenderer
|
|
@@ -1351,10 +1364,12 @@ def _populate_registry():
|
|
|
1351
1364
|
"kimi-k2": KimiK2Renderer,
|
|
1352
1365
|
"kimi-k2.5": KimiK25Renderer,
|
|
1353
1366
|
"laguna-xs.2": LagunaXS2Renderer,
|
|
1367
|
+
"laguna-m.1": LagunaM1Renderer,
|
|
1354
1368
|
"laguna-xs-2.1": LagunaXS21Renderer,
|
|
1355
1369
|
"llama-3": Llama3Renderer,
|
|
1356
1370
|
"nemotron-3": Nemotron3Renderer,
|
|
1357
1371
|
"nemotron-3-ultra": Nemotron3UltraRenderer,
|
|
1372
|
+
"nemotron-3.5": Nemotron35Renderer,
|
|
1358
1373
|
"gpt-oss": GptOssRenderer,
|
|
1359
1374
|
}
|
|
1360
1375
|
)
|
|
@@ -505,6 +505,28 @@ class LagunaXS2RendererConfig(BaseRendererConfig):
|
|
|
505
505
|
chat template's ``render_assistant_messages_raw`` gate."""
|
|
506
506
|
|
|
507
507
|
|
|
508
|
+
class LagunaM1RendererConfig(BaseRendererConfig):
|
|
509
|
+
"""Laguna M.1 renderer config.
|
|
510
|
+
|
|
511
|
+
Laguna M.1 shares Laguna XS.2's role and tool-call format, but its
|
|
512
|
+
official checkpoint has a distinct chat template: it does not inject
|
|
513
|
+
XS.2's fallback system message and it gives ``message.reasoning``
|
|
514
|
+
precedence over ``message.reasoning_content``. Served by
|
|
515
|
+
:class:`renderers.laguna_xs2.LagunaM1Renderer`.
|
|
516
|
+
"""
|
|
517
|
+
|
|
518
|
+
name: Literal["laguna-m.1"] = "laguna-m.1"
|
|
519
|
+
|
|
520
|
+
enable_thinking: bool = False
|
|
521
|
+
"""When ``True``, the generation prompt includes ``<think>``. Mirrors
|
|
522
|
+
the official template's ``enable_thinking`` kwarg and default."""
|
|
523
|
+
|
|
524
|
+
render_assistant_messages_raw: bool = False
|
|
525
|
+
"""When ``True``, assistant messages use the official template's
|
|
526
|
+
verbatim passthrough branch. See
|
|
527
|
+
:class:`LagunaXS2RendererConfig.render_assistant_messages_raw`."""
|
|
528
|
+
|
|
529
|
+
|
|
508
530
|
class LagunaXS21RendererConfig(BaseRendererConfig):
|
|
509
531
|
"""Laguna XS-2.1 renderer config.
|
|
510
532
|
|
|
@@ -635,6 +657,37 @@ class Nemotron3UltraRendererConfig(BaseRendererConfig):
|
|
|
635
657
|
user message. Mirrors the Ultra chat template's ``medium_effort`` kwarg."""
|
|
636
658
|
|
|
637
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
|
+
|
|
638
691
|
class DeepSeekV3RendererConfig(BaseRendererConfig):
|
|
639
692
|
"""DeepSeek-V3 renderer config (non-reasoning).
|
|
640
693
|
|
|
@@ -680,11 +733,13 @@ RendererConfig = Annotated[
|
|
|
680
733
|
KimiK2RendererConfig,
|
|
681
734
|
KimiK25RendererConfig,
|
|
682
735
|
LagunaXS2RendererConfig,
|
|
736
|
+
LagunaM1RendererConfig,
|
|
683
737
|
LagunaXS21RendererConfig,
|
|
684
738
|
Llama3RendererConfig,
|
|
685
739
|
MiniMaxM2RendererConfig,
|
|
686
740
|
Nemotron3RendererConfig,
|
|
687
741
|
Nemotron3UltraRendererConfig,
|
|
742
|
+
Nemotron35RendererConfig,
|
|
688
743
|
DeepSeekV3RendererConfig,
|
|
689
744
|
DeepSeekR1RendererConfig,
|
|
690
745
|
],
|
|
@@ -720,11 +775,13 @@ _CONFIG_BY_NAME: dict[str, type[BaseRendererConfig]] = {
|
|
|
720
775
|
"kimi-k2": KimiK2RendererConfig,
|
|
721
776
|
"kimi-k2.5": KimiK25RendererConfig,
|
|
722
777
|
"laguna-xs.2": LagunaXS2RendererConfig,
|
|
778
|
+
"laguna-m.1": LagunaM1RendererConfig,
|
|
723
779
|
"laguna-xs-2.1": LagunaXS21RendererConfig,
|
|
724
780
|
"llama-3": Llama3RendererConfig,
|
|
725
781
|
"minimax-m2": MiniMaxM2RendererConfig,
|
|
726
782
|
"nemotron-3": Nemotron3RendererConfig,
|
|
727
783
|
"nemotron-3-ultra": Nemotron3UltraRendererConfig,
|
|
784
|
+
"nemotron-3.5": Nemotron35RendererConfig,
|
|
728
785
|
"deepseek-v3": DeepSeekV3RendererConfig,
|
|
729
786
|
"deepseek-r1": DeepSeekR1RendererConfig,
|
|
730
787
|
}
|
|
@@ -767,10 +824,12 @@ __all__ = [
|
|
|
767
824
|
"Hy3RendererConfig",
|
|
768
825
|
"KimiK25RendererConfig",
|
|
769
826
|
"KimiK2RendererConfig",
|
|
827
|
+
"LagunaM1RendererConfig",
|
|
770
828
|
"LagunaXS2RendererConfig",
|
|
771
829
|
"LagunaXS21RendererConfig",
|
|
772
830
|
"Llama3RendererConfig",
|
|
773
831
|
"MiniMaxM2RendererConfig",
|
|
832
|
+
"Nemotron35RendererConfig",
|
|
774
833
|
"Nemotron3RendererConfig",
|
|
775
834
|
"Nemotron3UltraRendererConfig",
|
|
776
835
|
"PrimeQwen3RendererConfig",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Laguna
|
|
1
|
+
"""Poolside Laguna renderer family.
|
|
2
2
|
|
|
3
3
|
Main properties:
|
|
4
4
|
- Prefix is the single token ``〈|EOS|〉`` (also the EOS / stop token).
|
|
@@ -36,6 +36,11 @@ XS-2.1's template (upstream rev ``575f0f28``) is served by the
|
|
|
36
36
|
section ends at ``</available_tools>``.
|
|
37
37
|
- The ``<system>`` block is emitted whenever there is system content,
|
|
38
38
|
tools, or ``enable_thinking`` — even if that leaves it empty.
|
|
39
|
+
|
|
40
|
+
Laguna M.1's official template (revision ``2bf8a4ab``) is served by
|
|
41
|
+
:class:`LagunaM1Renderer`. It shares XS.2's byte layout, tool syntax, and
|
|
42
|
+
generation prompt, but has no fallback system prompt and reads assistant
|
|
43
|
+
reasoning from ``reasoning`` before falling back to ``reasoning_content``.
|
|
39
44
|
"""
|
|
40
45
|
|
|
41
46
|
from __future__ import annotations
|
|
@@ -56,7 +61,11 @@ from renderers.base import (
|
|
|
56
61
|
resolve_thinking_retention,
|
|
57
62
|
should_rerender_for_thinking_retention,
|
|
58
63
|
)
|
|
59
|
-
from renderers.configs import
|
|
64
|
+
from renderers.configs import (
|
|
65
|
+
LagunaM1RendererConfig,
|
|
66
|
+
LagunaXS2RendererConfig,
|
|
67
|
+
LagunaXS21RendererConfig,
|
|
68
|
+
)
|
|
60
69
|
from renderers.parsing import parse_laguna_xs2
|
|
61
70
|
|
|
62
71
|
_DEFAULT_SYSTEM_MESSAGE = (
|
|
@@ -107,7 +116,12 @@ class LagunaXS2Renderer:
|
|
|
107
116
|
def __init__(
|
|
108
117
|
self,
|
|
109
118
|
tokenizer: PreTrainedTokenizer,
|
|
110
|
-
config:
|
|
119
|
+
config: (
|
|
120
|
+
LagunaXS2RendererConfig
|
|
121
|
+
| LagunaM1RendererConfig
|
|
122
|
+
| LagunaXS21RendererConfig
|
|
123
|
+
| None
|
|
124
|
+
) = None,
|
|
111
125
|
):
|
|
112
126
|
self._tokenizer = tokenizer
|
|
113
127
|
self.config = config or LagunaXS2RendererConfig()
|
|
@@ -481,12 +495,9 @@ class LagunaXS2Renderer:
|
|
|
481
495
|
emit_text,
|
|
482
496
|
emit_text_segments,
|
|
483
497
|
) -> None:
|
|
484
|
-
# Raw passthrough is
|
|
485
|
-
#
|
|
486
|
-
if (
|
|
487
|
-
isinstance(self.config, LagunaXS2RendererConfig)
|
|
488
|
-
and self.config.render_assistant_messages_raw
|
|
489
|
-
):
|
|
498
|
+
# Raw passthrough is shared by XS.2 and M.1; XS-2.1's config does
|
|
499
|
+
# not expose this template gate.
|
|
500
|
+
if getattr(self.config, "render_assistant_messages_raw", False):
|
|
490
501
|
self._render_assistant_raw(
|
|
491
502
|
msg_idx,
|
|
492
503
|
content,
|
|
@@ -495,16 +506,7 @@ class LagunaXS2Renderer:
|
|
|
495
506
|
)
|
|
496
507
|
return
|
|
497
508
|
|
|
498
|
-
reasoning_content =
|
|
499
|
-
if isinstance(msg.get("reasoning_content"), str):
|
|
500
|
-
reasoning_content = msg["reasoning_content"]
|
|
501
|
-
else:
|
|
502
|
-
# When the caller stores reasoning as a ``ThinkingPart`` inside
|
|
503
|
-
# a list-form ``content`` (e.g. after parse_response →
|
|
504
|
-
# reserialize), pull it out here so it survives the re-render.
|
|
505
|
-
part_thinking = self._thinking_text(msg.get("content"))
|
|
506
|
-
if part_thinking:
|
|
507
|
-
reasoning_content = part_thinking
|
|
509
|
+
reasoning_content, content = self._assistant_reasoning_and_content(msg, content)
|
|
508
510
|
|
|
509
511
|
# ``<assistant>\n`` is template-injected scaffolding — the chat
|
|
510
512
|
# template emits these as the generation prompt at inference and
|
|
@@ -567,6 +569,22 @@ class LagunaXS2Renderer:
|
|
|
567
569
|
emit_special(self._assistant_end, msg_idx, is_sampled=True, is_content=True)
|
|
568
570
|
emit_text("\n", msg_idx, is_sampled=False, is_content=False)
|
|
569
571
|
|
|
572
|
+
def _assistant_reasoning_and_content(
|
|
573
|
+
self, msg: Message, content: str
|
|
574
|
+
) -> tuple[str, str]:
|
|
575
|
+
"""Return the reasoning/body pair used by the XS.2 template."""
|
|
576
|
+
reasoning_content = ""
|
|
577
|
+
if isinstance(msg.get("reasoning_content"), str):
|
|
578
|
+
reasoning_content = msg["reasoning_content"]
|
|
579
|
+
else:
|
|
580
|
+
# When the caller stores reasoning as a ``ThinkingPart`` inside
|
|
581
|
+
# a list-form ``content`` (e.g. after parse_response →
|
|
582
|
+
# reserialize), pull it out here so it survives the re-render.
|
|
583
|
+
part_thinking = self._thinking_text(msg.get("content"))
|
|
584
|
+
if part_thinking:
|
|
585
|
+
reasoning_content = part_thinking
|
|
586
|
+
return reasoning_content, content
|
|
587
|
+
|
|
570
588
|
def _render_assistant_raw(
|
|
571
589
|
self,
|
|
572
590
|
msg_idx: int,
|
|
@@ -620,6 +638,43 @@ class LagunaXS2Renderer:
|
|
|
620
638
|
emit_text("\n", msg_idx, is_sampled=False, is_content=False)
|
|
621
639
|
|
|
622
640
|
|
|
641
|
+
class LagunaM1Renderer(LagunaXS2Renderer):
|
|
642
|
+
"""Renderer for the official ``poolside/Laguna-M.1`` checkpoint.
|
|
643
|
+
|
|
644
|
+
The token protocol is shared with XS.2, while the absent fallback
|
|
645
|
+
system prompt and assistant-reasoning precedence are M.1-specific.
|
|
646
|
+
"""
|
|
647
|
+
|
|
648
|
+
def __init__(
|
|
649
|
+
self,
|
|
650
|
+
tokenizer: PreTrainedTokenizer,
|
|
651
|
+
config: LagunaM1RendererConfig | None = None,
|
|
652
|
+
):
|
|
653
|
+
super().__init__(tokenizer, config or LagunaM1RendererConfig())
|
|
654
|
+
self._default_system_message = ""
|
|
655
|
+
|
|
656
|
+
def _assistant_reasoning_and_content(
|
|
657
|
+
self, msg: Message, content: str
|
|
658
|
+
) -> tuple[str, str]:
|
|
659
|
+
# Match the official Jinja exactly: ``reasoning`` wins whenever it
|
|
660
|
+
# is a string (including the empty string), then
|
|
661
|
+
# ``reasoning_content`` is considered. An inline </think> block is
|
|
662
|
+
# removed from content and becomes the fallback reasoning source.
|
|
663
|
+
reasoning_content = ""
|
|
664
|
+
if isinstance(msg.get("reasoning"), str):
|
|
665
|
+
reasoning_content = msg["reasoning"]
|
|
666
|
+
elif isinstance(msg.get("reasoning_content"), str):
|
|
667
|
+
reasoning_content = msg["reasoning_content"]
|
|
668
|
+
|
|
669
|
+
if "</think>" in content:
|
|
670
|
+
if not reasoning_content:
|
|
671
|
+
before_close = content.split("</think>", 1)[0].rstrip("\n")
|
|
672
|
+
reasoning_content = before_close.split("<think>")[-1].lstrip("\n")
|
|
673
|
+
content = content.rsplit("</think>", 1)[-1].lstrip("\n")
|
|
674
|
+
|
|
675
|
+
return reasoning_content, content
|
|
676
|
+
|
|
677
|
+
|
|
623
678
|
class LagunaXS21Renderer(LagunaXS2Renderer):
|
|
624
679
|
"""Laguna-XS-2.1 — mirrors the ``poolside/Laguna-XS-2.1`` chat
|
|
625
680
|
template (upstream rev ``575f0f28``); see the module docstring for
|
|
@@ -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
|
|
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
|
|
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"``)
|
|
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,7 +36,11 @@ 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"),
|
|
43
|
+
("poolside/Laguna-M.1", "auto"),
|
|
40
44
|
# XS-2.1 resolves to `LagunaXS21Renderer` via the model name — its
|
|
41
45
|
# chat template differs from XS.2's (see renderers/laguna_xs2.py).
|
|
42
46
|
("poolside/Laguna-XS-2.1", "auto"),
|
|
@@ -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,232 @@
|
|
|
1
|
+
"""Exact chat-template parity for the official Laguna M.1 checkpoint."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from functools import lru_cache
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
from pydantic import TypeAdapter
|
|
9
|
+
|
|
10
|
+
from renderers import (
|
|
11
|
+
LagunaM1Renderer,
|
|
12
|
+
LagunaM1RendererConfig,
|
|
13
|
+
RendererConfig,
|
|
14
|
+
create_renderer,
|
|
15
|
+
)
|
|
16
|
+
from renderers.base import load_tokenizer
|
|
17
|
+
|
|
18
|
+
_MODEL = "poolside/Laguna-M.1"
|
|
19
|
+
|
|
20
|
+
TOOLS = [
|
|
21
|
+
{
|
|
22
|
+
"type": "function",
|
|
23
|
+
"function": {
|
|
24
|
+
"name": "shell",
|
|
25
|
+
"description": "Run a shell command",
|
|
26
|
+
"parameters": {
|
|
27
|
+
"type": "object",
|
|
28
|
+
"properties": {
|
|
29
|
+
"cmd": {"type": "string"},
|
|
30
|
+
"timeout": {"type": "integer"},
|
|
31
|
+
},
|
|
32
|
+
"required": ["cmd"],
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@lru_cache(maxsize=None)
|
|
40
|
+
def _tok():
|
|
41
|
+
return load_tokenizer(_MODEL)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _renderer(**kwargs) -> LagunaM1Renderer:
|
|
45
|
+
renderer = create_renderer(_tok(), LagunaM1RendererConfig(**kwargs))
|
|
46
|
+
assert isinstance(renderer, LagunaM1Renderer)
|
|
47
|
+
return renderer
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _expected(messages, *, tools=None, add_generation_prompt=False, **kwargs):
|
|
51
|
+
return list(
|
|
52
|
+
_tok().apply_chat_template(
|
|
53
|
+
messages,
|
|
54
|
+
tools=tools,
|
|
55
|
+
add_generation_prompt=add_generation_prompt,
|
|
56
|
+
tokenize=True,
|
|
57
|
+
return_dict=False,
|
|
58
|
+
**kwargs,
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_auto_selection_and_typed_config():
|
|
64
|
+
renderer = create_renderer(_tok())
|
|
65
|
+
assert isinstance(renderer, LagunaM1Renderer)
|
|
66
|
+
assert isinstance(renderer.config, LagunaM1RendererConfig)
|
|
67
|
+
|
|
68
|
+
parsed = TypeAdapter(RendererConfig).validate_python(
|
|
69
|
+
{"name": "laguna-m.1", "enable_thinking": True}
|
|
70
|
+
)
|
|
71
|
+
assert isinstance(parsed, LagunaM1RendererConfig)
|
|
72
|
+
assert parsed.enable_thinking is True
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@pytest.mark.parametrize("enable_thinking", [False, True])
|
|
76
|
+
def test_no_system_and_generation_prompt_parity(enable_thinking):
|
|
77
|
+
messages = [{"role": "user", "content": " preserve me "}]
|
|
78
|
+
renderer = _renderer(enable_thinking=enable_thinking)
|
|
79
|
+
got = renderer.render_ids(messages, add_generation_prompt=True)
|
|
80
|
+
assert got == _expected(
|
|
81
|
+
messages,
|
|
82
|
+
add_generation_prompt=True,
|
|
83
|
+
enable_thinking=enable_thinking,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
text = _tok().decode(got)
|
|
87
|
+
assert text.startswith("〈|EOS|〉<user>\n preserve me \n</user>\n<assistant>\n")
|
|
88
|
+
assert "<system>" not in text
|
|
89
|
+
assert text.endswith("<think>" if enable_thinking else "</think>")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@pytest.mark.parametrize("enable_thinking", [False, True])
|
|
93
|
+
def test_reasoning_content_history_parity_in_both_modes(enable_thinking):
|
|
94
|
+
messages = [
|
|
95
|
+
{"role": "user", "content": "Compute."},
|
|
96
|
+
{
|
|
97
|
+
"role": "assistant",
|
|
98
|
+
"reasoning_content": "\n two steps \n",
|
|
99
|
+
"content": "\n result \n",
|
|
100
|
+
},
|
|
101
|
+
]
|
|
102
|
+
got = _renderer(enable_thinking=enable_thinking).render_ids(messages)
|
|
103
|
+
assert got == _expected(messages, enable_thinking=enable_thinking)
|
|
104
|
+
assert "<think>\ntwo steps\n</think>\nresult\n</assistant>\n" in _tok().decode(got)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_reasoning_field_precedence_and_inline_think_extraction():
|
|
108
|
+
precedence = [
|
|
109
|
+
{"role": "user", "content": "Compute."},
|
|
110
|
+
{
|
|
111
|
+
"role": "assistant",
|
|
112
|
+
"reasoning": "preferred",
|
|
113
|
+
"reasoning_content": "ignored",
|
|
114
|
+
"content": "answer",
|
|
115
|
+
},
|
|
116
|
+
]
|
|
117
|
+
got = _renderer(enable_thinking=True).render_ids(precedence)
|
|
118
|
+
assert got == _expected(precedence, enable_thinking=True)
|
|
119
|
+
text = _tok().decode(got)
|
|
120
|
+
assert "preferred" in text
|
|
121
|
+
assert "ignored" not in text
|
|
122
|
+
|
|
123
|
+
empty_reasoning_wins = [
|
|
124
|
+
{"role": "user", "content": "Compute."},
|
|
125
|
+
{
|
|
126
|
+
"role": "assistant",
|
|
127
|
+
"reasoning": "",
|
|
128
|
+
"reasoning_content": "also ignored",
|
|
129
|
+
"content": "answer",
|
|
130
|
+
},
|
|
131
|
+
]
|
|
132
|
+
got = _renderer(enable_thinking=True).render_ids(empty_reasoning_wins)
|
|
133
|
+
assert got == _expected(empty_reasoning_wins, enable_thinking=True)
|
|
134
|
+
assert "also ignored" not in _tok().decode(got)
|
|
135
|
+
|
|
136
|
+
inline = [
|
|
137
|
+
{"role": "user", "content": "Compute."},
|
|
138
|
+
{
|
|
139
|
+
"role": "assistant",
|
|
140
|
+
"content": "<think>\ninline reason\n</think>\nvisible answer",
|
|
141
|
+
},
|
|
142
|
+
]
|
|
143
|
+
assert _renderer().render_ids(inline) == _expected(inline)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_tools_calls_responses_and_generation_prompt_parity():
|
|
147
|
+
messages = [
|
|
148
|
+
{"role": "system", "content": "Use tools carefully."},
|
|
149
|
+
{"role": "user", "content": "Inspect the machine."},
|
|
150
|
+
{
|
|
151
|
+
"role": "assistant",
|
|
152
|
+
"reasoning": "Need a command.",
|
|
153
|
+
"content": "Running it.",
|
|
154
|
+
"tool_calls": [
|
|
155
|
+
{
|
|
156
|
+
"id": "call_1",
|
|
157
|
+
"type": "function",
|
|
158
|
+
"function": {
|
|
159
|
+
"name": "shell",
|
|
160
|
+
"arguments": {"cmd": "uname -a", "timeout": 5},
|
|
161
|
+
},
|
|
162
|
+
}
|
|
163
|
+
],
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
"role": "tool",
|
|
167
|
+
"tool_call_id": "call_1",
|
|
168
|
+
"content": '{"stdout":"Linux"}',
|
|
169
|
+
},
|
|
170
|
+
]
|
|
171
|
+
renderer = _renderer(enable_thinking=True)
|
|
172
|
+
got = renderer.render_ids(messages, tools=TOOLS, add_generation_prompt=True)
|
|
173
|
+
assert got == _expected(
|
|
174
|
+
messages,
|
|
175
|
+
tools=TOOLS,
|
|
176
|
+
add_generation_prompt=True,
|
|
177
|
+
enable_thinking=True,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
text = _tok().decode(got)
|
|
181
|
+
assert "<available_tools>\n" in text
|
|
182
|
+
assert (
|
|
183
|
+
"<tool_call>shell\n<arg_key>cmd</arg_key>\n"
|
|
184
|
+
"<arg_value>uname -a</arg_value>\n<arg_key>timeout</arg_key>\n"
|
|
185
|
+
"<arg_value>5</arg_value>\n</tool_call>\n"
|
|
186
|
+
) in text
|
|
187
|
+
assert '<tool_response>\n{"stdout":"Linux"}\n</tool_response>\n' in text
|
|
188
|
+
assert text.endswith("<assistant>\n<think>")
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def test_reasoning_content_and_tool_call_round_trip():
|
|
192
|
+
renderer = _renderer(enable_thinking=True)
|
|
193
|
+
prompt = [{"role": "user", "content": "Inspect the machine."}]
|
|
194
|
+
assistant = {
|
|
195
|
+
"role": "assistant",
|
|
196
|
+
"reasoning": "Need a command.",
|
|
197
|
+
"content": "Running it.",
|
|
198
|
+
"tool_calls": [
|
|
199
|
+
{
|
|
200
|
+
"function": {
|
|
201
|
+
"name": "shell",
|
|
202
|
+
"arguments": {"cmd": "uname -a", "timeout": 5},
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
],
|
|
206
|
+
}
|
|
207
|
+
prompt_ids = renderer.render_ids(prompt, add_generation_prompt=True)
|
|
208
|
+
full_ids = renderer.render_ids([*prompt, assistant])
|
|
209
|
+
completion_ids = full_ids[len(prompt_ids) :]
|
|
210
|
+
parsed = renderer.parse_response(completion_ids, tools=TOOLS)
|
|
211
|
+
|
|
212
|
+
assert parsed.reasoning_content == "Need a command."
|
|
213
|
+
assert parsed.content == "Running it."
|
|
214
|
+
assert len(parsed.tool_calls) == 1
|
|
215
|
+
assert parsed.tool_calls[0].name == "shell"
|
|
216
|
+
assert parsed.tool_calls[0].arguments == {"cmd": "uname -a", "timeout": 5}
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def test_raw_assistant_round_trip_parity():
|
|
220
|
+
messages = [
|
|
221
|
+
{"role": "user", "content": "Continue."},
|
|
222
|
+
{
|
|
223
|
+
"role": "assistant",
|
|
224
|
+
"content": "<think>raw reason</think>raw body</assistant>",
|
|
225
|
+
},
|
|
226
|
+
]
|
|
227
|
+
renderer = _renderer(enable_thinking=True, render_assistant_messages_raw=True)
|
|
228
|
+
assert renderer.render_ids(messages) == _expected(
|
|
229
|
+
messages,
|
|
230
|
+
enable_thinking=True,
|
|
231
|
+
render_assistant_messages_raw=True,
|
|
232
|
+
)
|
|
@@ -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
|
|
29
|
-
``low_effort``)
|
|
30
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
652
|
-
if model
|
|
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**.
|
|
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
|
|
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
|
|
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
|
-
|
|
17
|
-
|
|
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
|
|
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
|
-
|
|
56
|
-
#
|
|
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,7 +63,11 @@ _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"),
|
|
70
|
+
("poolside/Laguna-M.1", "auto"),
|
|
67
71
|
("poolside/Laguna-XS-2.1", "auto"),
|
|
68
72
|
("tencent/Hy3", "auto"),
|
|
69
73
|
("openai/gpt-oss-20b", "gpt-oss"),
|
|
@@ -119,7 +123,7 @@ _KWARG_VALUES: dict[str, list[Any]] = {
|
|
|
119
123
|
"You are a helpful assistant. Your name is MiniMax-M2.5 and is built by MiniMax.",
|
|
120
124
|
"You are CustomBot, a research assistant.",
|
|
121
125
|
],
|
|
122
|
-
# Laguna-XS.2 — switches assistant rendering to a verbatim
|
|
126
|
+
# Laguna-XS.2 / Laguna-M.1 — switches assistant rendering to a verbatim
|
|
123
127
|
# passthrough mode. The renderer paths diverge significantly under
|
|
124
128
|
# this flag, so both values are exercised.
|
|
125
129
|
"render_assistant_messages_raw": [True, False],
|
|
@@ -46,7 +46,10 @@ _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"),
|
|
52
|
+
("poolside/Laguna-M.1", "auto"),
|
|
50
53
|
# Laguna-XS-2.1 is deliberately absent: under the default
|
|
51
54
|
# ``enable_thinking=False`` its template drops assistant reasoning at
|
|
52
55
|
# render time, so the reasoning round-trip can't hold by design.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/examples/sglang/multiturn_generate_sglang.py
RENAMED
|
File without changes
|
|
File without changes
|
{renderers-0.1.9.dev10 → renderers-0.1.10.dev4}/examples/tinker/multiturn_generate_tinker.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|