renderers 0.1.11.dev2__tar.gz → 0.1.11.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.11.dev2 → renderers-0.1.11.dev4}/PKG-INFO +9 -7
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/README.md +8 -6
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/docs/renderer-config.md +2 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/pyproject.toml +2 -2
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/renderers/__init__.py +6 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/renderers/_version.py +2 -2
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/renderers/base.py +146 -34
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/renderers/configs.py +44 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/renderers/deepseek_v3.py +5 -2
- renderers-0.1.11.dev4/renderers/deepseek_v4.py +859 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/renderers/gemma4.py +3 -2
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/renderers/glm45.py +5 -2
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/renderers/glm5.py +5 -2
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/renderers/gpt_oss.py +5 -2
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/renderers/hy3.py +43 -11
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/renderers/inkling.py +3 -2
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/renderers/kimi_k2.py +5 -2
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/renderers/kimi_k25.py +4 -3
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/renderers/laguna_xs2.py +53 -7
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/renderers/llama_3.py +5 -2
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/renderers/minimax_m2.py +20 -6
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/renderers/nemotron3.py +5 -2
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/renderers/parsing.py +190 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/renderers/prime_qwen3.py +14 -4
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/renderers/qwen3.py +5 -2
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/renderers/qwen35.py +4 -3
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/renderers/qwen3_vl.py +5 -4
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/tests/conftest.py +5 -0
- renderers-0.1.11.dev4/tests/reference_rendering.py +414 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/tests/test_bridge.py +1 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/tests/test_build_helpers.py +3 -14
- renderers-0.1.11.dev4/tests/test_deepseek_v4.py +565 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/tests/test_load_tokenizer.py +16 -11
- renderers-0.1.11.dev4/tests/test_offsetless_tokenizers.py +223 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/tests/test_render_ids.py +6 -15
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/tests/test_renderer_config_parity.py +22 -26
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/tests/test_roundtrip.py +1 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/.github/workflows/publish-dev.yml +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/.github/workflows/publish.yml +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/.github/workflows/style.yml +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/.github/workflows/test.yml +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/.gitignore +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/.pre-commit-config.yaml +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/LICENSE +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/examples/README.md +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/examples/sglang/multiturn_generate_sglang.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/examples/sglang/online_multiturn_sglang.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/examples/tinker/multiturn_generate_tinker.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/examples/transformers/multiturn_generate_transformers.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/examples/vllm/multiturn_generate_vllm.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/renderers/client.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/renderers/deepseek_r1.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/renderers/default.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/renderers/laguna_s21.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/renderers/parsers.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/renderers/qwen36.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/renderers/qwen38.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/tests/test_client.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/tests/test_deepseek_r1.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/tests/test_disabled_thinking_stability.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/tests/test_gemma4.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/tests/test_glm_tool_name_validation.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/tests/test_gpt_oss_harmony_parity.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/tests/test_hy3.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/tests/test_incremental.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/tests/test_inkling.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/tests/test_is_content.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/tests/test_kimi_k25_tool_schema.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/tests/test_laguna_m1.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/tests/test_laguna_s21.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/tests/test_laguna_xs21.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/tests/test_llama_3.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/tests/test_message_indices.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/tests/test_message_tool_names.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/tests/test_multimodal.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/tests/test_nemotron3_parity.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/tests/test_nemotron3_ultra.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/tests/test_parse_response.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/tests/test_parse_response_robustness.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/tests/test_parsers.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/tests/test_preserve_thinking.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/tests/test_prime_qwen3_parity.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/tests/test_qwen35_size_coverage.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/tests/test_qwen38.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/tests/test_renderer_config.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/tests/test_sampled_mask.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/tests/test_tokens_per_message.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/tests/test_tool_arg_type_preservation.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev4}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: renderers
|
|
3
|
-
Version: 0.1.11.
|
|
3
|
+
Version: 0.1.11.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
|
|
@@ -39,11 +39,13 @@ uv add 'renderers[transformers]'
|
|
|
39
39
|
uv add 'renderers[multimodal]'
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
A BYO tokenizer must expose `encode`, `decode`, `convert_tokens_to_ids`,
|
|
43
|
-
IDs such as `eos_token_id
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
A BYO tokenizer must expose `encode`, `decode`, `convert_tokens_to_ids`, and
|
|
43
|
+
token IDs such as `eos_token_id`. Character offsets are optional: tokenizers
|
|
44
|
+
supporting `return_offsets_mapping=True` also receive precise per-token
|
|
45
|
+
`is_content` attribution; without offsets, renderers return `is_content=[]`.
|
|
46
|
+
`DefaultRenderer` additionally requires `apply_chat_template`. This includes
|
|
47
|
+
text-only Inkling training: `InklingRenderer` loads its Transformers processor
|
|
48
|
+
only when image or audio content is actually rendered.
|
|
47
49
|
|
|
48
50
|
## At a glance
|
|
49
51
|
|
|
@@ -75,7 +77,7 @@ next_prompt_ids = r.bridge_to_next_turn(
|
|
|
75
77
|
)
|
|
76
78
|
```
|
|
77
79
|
|
|
78
|
-
Hand-coded renderers ship for `qwen3`, `qwen3-vl`, `qwen3.5`, `qwen3.6`, `qwen3.8`, `gemma4`, `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`, `inkling` / `inkling-small`, and `prime-qwen3`. Anything else falls back to `DefaultRenderer`, a generic `apply_chat_template` wrapper. `qwen3-vl`, `qwen3.5`, `qwen3.6`, `qwen3.8`, `gemma4`, `kimi-k2.5` / `kimi-k2.6`, and the Inkling checkpoints are multimodal (Inkling handles both image **and** audio).
|
|
80
|
+
Hand-coded renderers ship for `qwen3`, `qwen3-vl`, `qwen3.5`, `qwen3.6`, `qwen3.8`, `gemma4`, `glm-5`, `glm-5.1`, `glm-4.5`, `minimax-m2`, `deepseek-v3`, `deepseek-r1`, `deepseek-v4` (V4 Flash 0731), `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`, `inkling` / `inkling-small`, and `prime-qwen3`. Anything else falls back to `DefaultRenderer`, a generic `apply_chat_template` wrapper. `qwen3-vl`, `qwen3.5`, `qwen3.6`, `qwen3.8`, `gemma4`, `kimi-k2.5` / `kimi-k2.6`, and the Inkling checkpoints are multimodal (Inkling handles both image **and** audio).
|
|
79
81
|
|
|
80
82
|
## API
|
|
81
83
|
|
|
@@ -19,11 +19,13 @@ uv add 'renderers[transformers]'
|
|
|
19
19
|
uv add 'renderers[multimodal]'
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
A BYO tokenizer must expose `encode`, `decode`, `convert_tokens_to_ids`,
|
|
23
|
-
IDs such as `eos_token_id
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
A BYO tokenizer must expose `encode`, `decode`, `convert_tokens_to_ids`, and
|
|
23
|
+
token IDs such as `eos_token_id`. Character offsets are optional: tokenizers
|
|
24
|
+
supporting `return_offsets_mapping=True` also receive precise per-token
|
|
25
|
+
`is_content` attribution; without offsets, renderers return `is_content=[]`.
|
|
26
|
+
`DefaultRenderer` additionally requires `apply_chat_template`. This includes
|
|
27
|
+
text-only Inkling training: `InklingRenderer` loads its Transformers processor
|
|
28
|
+
only when image or audio content is actually rendered.
|
|
27
29
|
|
|
28
30
|
## At a glance
|
|
29
31
|
|
|
@@ -55,7 +57,7 @@ next_prompt_ids = r.bridge_to_next_turn(
|
|
|
55
57
|
)
|
|
56
58
|
```
|
|
57
59
|
|
|
58
|
-
Hand-coded renderers ship for `qwen3`, `qwen3-vl`, `qwen3.5`, `qwen3.6`, `qwen3.8`, `gemma4`, `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`, `inkling` / `inkling-small`, and `prime-qwen3`. Anything else falls back to `DefaultRenderer`, a generic `apply_chat_template` wrapper. `qwen3-vl`, `qwen3.5`, `qwen3.6`, `qwen3.8`, `gemma4`, `kimi-k2.5` / `kimi-k2.6`, and the Inkling checkpoints are multimodal (Inkling handles both image **and** audio).
|
|
60
|
+
Hand-coded renderers ship for `qwen3`, `qwen3-vl`, `qwen3.5`, `qwen3.6`, `qwen3.8`, `gemma4`, `glm-5`, `glm-5.1`, `glm-4.5`, `minimax-m2`, `deepseek-v3`, `deepseek-r1`, `deepseek-v4` (V4 Flash 0731), `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`, `inkling` / `inkling-small`, and `prime-qwen3`. Anything else falls back to `DefaultRenderer`, a generic `apply_chat_template` wrapper. `qwen3-vl`, `qwen3.5`, `qwen3.6`, `qwen3.8`, `gemma4`, `kimi-k2.5` / `kimi-k2.6`, and the Inkling checkpoints are multimodal (Inkling handles both image **and** audio).
|
|
59
61
|
|
|
60
62
|
## API
|
|
61
63
|
|
|
@@ -51,6 +51,7 @@ definition time. Template fields are covered by parity tests against
|
|
|
51
51
|
| Nemotron-3.5 Lightning | `Nemotron35RendererConfig` | `enable_thinking`, `truncate_history_thinking` | - |
|
|
52
52
|
| DeepSeek V3 | `DeepSeekV3RendererConfig` | - | - |
|
|
53
53
|
| DeepSeek R1 | `DeepSeekR1RendererConfig` | - | - |
|
|
54
|
+
| DeepSeek V4 Flash 0731 | `DeepSeekV4RendererConfig` | `enable_thinking`, `drop_thinking`, `reasoning_effort` | - |
|
|
54
55
|
|
|
55
56
|
Configs are frozen value objects. To override a field, construct a new instance
|
|
56
57
|
or call `config.model_copy(update={...})`.
|
|
@@ -145,6 +146,7 @@ the knobs its template actually exposes:
|
|
|
145
146
|
| Kimi K2.5 / 2.6 | `thinking=False -> all`, else `tool_cycle` |
|
|
146
147
|
| Nemotron-3 / 3.5 | `truncate_history_thinking=False -> all`; else `enable_thinking=False -> all`; else `tool_cycle` |
|
|
147
148
|
| DeepSeek R1 | `template` |
|
|
149
|
+
| DeepSeek V4 Flash 0731 | `enable_thinking=False` or `drop_thinking=False -> all`, else `tool_cycle` |
|
|
148
150
|
| MiniMax M2 | `tool_cycle` |
|
|
149
151
|
| DeepSeek V3, Qwen3-VL, Kimi K2, Laguna XS.2 / M.1 / XS-2.1 / S-2.1, Llama 3, Inkling | `all` |
|
|
150
152
|
| PrimeIntellect Qwen3 | `all` |
|
|
@@ -39,8 +39,8 @@ dependencies = [
|
|
|
39
39
|
|
|
40
40
|
[project.optional-dependencies]
|
|
41
41
|
# Tokenizer loading uses Hugging Face. Text-only renderers can instead be
|
|
42
|
-
# constructed with
|
|
43
|
-
# dependency.
|
|
42
|
+
# constructed with a compatible BYO tokenizer and do not import this
|
|
43
|
+
# dependency. Character offsets are optional.
|
|
44
44
|
transformers = [
|
|
45
45
|
# Keep this floor compatible with prime-rl's transformers pin. Inkling's
|
|
46
46
|
# tokenizer and text-only renderer work on older releases; image/audio
|
|
@@ -15,6 +15,7 @@ from renderers.base import (
|
|
|
15
15
|
Message,
|
|
16
16
|
MultiModalData,
|
|
17
17
|
MultimodalRenderer,
|
|
18
|
+
OffsetTokenizer,
|
|
18
19
|
ParsedResponse,
|
|
19
20
|
ParsedToolCall,
|
|
20
21
|
PlaceholderRange,
|
|
@@ -47,6 +48,7 @@ from renderers.configs import (
|
|
|
47
48
|
DefaultRendererConfig,
|
|
48
49
|
DeepSeekR1RendererConfig,
|
|
49
50
|
DeepSeekV3RendererConfig,
|
|
51
|
+
DeepSeekV4RendererConfig,
|
|
50
52
|
GLM45RendererConfig,
|
|
51
53
|
GLM51RendererConfig,
|
|
52
54
|
GLM5RendererConfig,
|
|
@@ -84,6 +86,7 @@ from renderers.configs import (
|
|
|
84
86
|
_LAZY_RENDERERS: dict[str, str] = {
|
|
85
87
|
"DeepSeekR1Renderer": "renderers.deepseek_r1",
|
|
86
88
|
"DeepSeekV3Renderer": "renderers.deepseek_v3",
|
|
89
|
+
"DeepSeekV4Renderer": "renderers.deepseek_v4",
|
|
87
90
|
"DefaultRenderer": "renderers.default",
|
|
88
91
|
"GLM45Renderer": "renderers.glm45",
|
|
89
92
|
"GLM51Renderer": "renderers.glm5",
|
|
@@ -137,6 +140,8 @@ __all__ = [
|
|
|
137
140
|
"DeepSeekR1RendererConfig",
|
|
138
141
|
"DeepSeekV3Renderer",
|
|
139
142
|
"DeepSeekV3RendererConfig",
|
|
143
|
+
"DeepSeekV4Renderer",
|
|
144
|
+
"DeepSeekV4RendererConfig",
|
|
140
145
|
"DefaultRenderer",
|
|
141
146
|
"DefaultRendererConfig",
|
|
142
147
|
"GLM45Renderer",
|
|
@@ -181,6 +186,7 @@ __all__ = [
|
|
|
181
186
|
"Nemotron3RendererConfig",
|
|
182
187
|
"Nemotron3UltraRenderer",
|
|
183
188
|
"Nemotron3UltraRendererConfig",
|
|
189
|
+
"OffsetTokenizer",
|
|
184
190
|
"OverlongPromptError",
|
|
185
191
|
"ParsedResponse",
|
|
186
192
|
"ParsedToolCall",
|
|
@@ -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.11.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 1, 11, '
|
|
21
|
+
__version__ = version = '0.1.11.dev4'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 11, 'dev4')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -11,6 +11,7 @@ from typing import (
|
|
|
11
11
|
Literal,
|
|
12
12
|
Protocol,
|
|
13
13
|
TypedDict,
|
|
14
|
+
cast,
|
|
14
15
|
runtime_checkable,
|
|
15
16
|
)
|
|
16
17
|
|
|
@@ -243,7 +244,9 @@ class RenderedTokens:
|
|
|
243
244
|
Empty ``sampled_mask`` (``[]``) means the renderer doesn't provide
|
|
244
245
|
this signal — consumers should fall back to attribution-only
|
|
245
246
|
masking. ``DefaultRenderer`` leaves it empty because the Jinja
|
|
246
|
-
template is opaque
|
|
247
|
+
template is opaque. Hand-coded renderers normally populate it; a
|
|
248
|
+
renderer whose sampled/scaffold boundary depends on character
|
|
249
|
+
attribution may leave it empty for an offsetless tokenizer.
|
|
247
250
|
|
|
248
251
|
``is_content`` is a per-token signal generalizing the "scaffold vs
|
|
249
252
|
body" distinction across all roles: ``True`` iff the token was
|
|
@@ -269,7 +272,8 @@ class RenderedTokens:
|
|
|
269
272
|
|
|
270
273
|
Empty ``is_content`` (``[]``) — like ``sampled_mask`` — means the
|
|
271
274
|
renderer doesn't provide the signal. ``DefaultRenderer`` leaves it
|
|
272
|
-
empty
|
|
275
|
+
empty because its Jinja template is opaque; all renderers leave it
|
|
276
|
+
empty when the supplied tokenizer cannot return character offsets.
|
|
273
277
|
|
|
274
278
|
``message_tool_names`` is the per-message tool function name list,
|
|
275
279
|
parallel to ``message_roles`` (same length). For tool-role
|
|
@@ -476,7 +480,7 @@ class RenderedTokens:
|
|
|
476
480
|
|
|
477
481
|
Returns an empty dict when :attr:`is_content` or
|
|
478
482
|
:attr:`message_roles` is empty (renderer didn't populate the
|
|
479
|
-
signal — e.g. ``DefaultRenderer``).
|
|
483
|
+
signal — e.g. ``DefaultRenderer`` or an offsetless tokenizer).
|
|
480
484
|
|
|
481
485
|
Intended for selective loss masking: SFT on tool response
|
|
482
486
|
bodies while RL acts only on assistant turns is the canonical
|
|
@@ -667,8 +671,8 @@ class Tokenizer(Protocol):
|
|
|
667
671
|
Hugging Face tokenizers satisfy this protocol, as can lightweight BYO
|
|
668
672
|
adapters around ``tokenizers.Tokenizer`` or another tokenizer backend.
|
|
669
673
|
Keeping the renderer-facing contract here makes ``transformers`` optional
|
|
670
|
-
for text rendering.
|
|
671
|
-
:
|
|
674
|
+
for text rendering. Character offsets are a separate optional capability;
|
|
675
|
+
see :class:`OffsetTokenizer`.
|
|
672
676
|
"""
|
|
673
677
|
|
|
674
678
|
name_or_path: str
|
|
@@ -681,6 +685,17 @@ class Tokenizer(Protocol):
|
|
|
681
685
|
|
|
682
686
|
def convert_tokens_to_ids(self, tokens: Any) -> Any: ...
|
|
683
687
|
|
|
688
|
+
|
|
689
|
+
@runtime_checkable
|
|
690
|
+
class OffsetTokenizer(Tokenizer, Protocol):
|
|
691
|
+
"""Tokenizer that can return character offsets alongside token IDs.
|
|
692
|
+
|
|
693
|
+
Hand-coded renderers use this optional capability to distinguish caller
|
|
694
|
+
content from adjacent template scaffold without changing the underlying
|
|
695
|
+
BPE pass. A basic :class:`Tokenizer` remains sufficient for rendering token
|
|
696
|
+
IDs; when offsets are unavailable, renderers leave ``is_content`` empty.
|
|
697
|
+
"""
|
|
698
|
+
|
|
684
699
|
def __call__(self, *args: Any, **kwargs: Any) -> Any: ...
|
|
685
700
|
|
|
686
701
|
|
|
@@ -967,6 +982,9 @@ MODEL_RENDERER_MAP: dict[str, str] = {
|
|
|
967
982
|
# DeepSeek R1 (reasoning).
|
|
968
983
|
"deepseek-ai/DeepSeek-R1": "deepseek-r1",
|
|
969
984
|
"deepseek-ai/DeepSeek-R1-0528": "deepseek-r1",
|
|
985
|
+
# DeepSeek V4 Flash 0731 uses the repository's Python DSML encoder (the
|
|
986
|
+
# tokenizer intentionally ships no Jinja chat_template).
|
|
987
|
+
"deepseek-ai/DeepSeek-V4-Flash-0731": "deepseek-v4",
|
|
970
988
|
# Kimi K2 (K2.5 and K2.6 share the K2.5 template, distinct from K2).
|
|
971
989
|
"moonshotai/Kimi-K2-Instruct": "kimi-k2",
|
|
972
990
|
"moonshotai/Kimi-K2.5": "kimi-k2.5",
|
|
@@ -1064,7 +1082,7 @@ _TRANSFORMERS_INSTALL_HINT = (
|
|
|
1064
1082
|
"Install the optional dependency with "
|
|
1065
1083
|
"`pip install 'renderers[transformers]'` (or "
|
|
1066
1084
|
"`uv add 'renderers[transformers]'`). Text-only renderers work without "
|
|
1067
|
-
"it when constructed with
|
|
1085
|
+
"it when constructed with a compatible tokenizer object."
|
|
1068
1086
|
)
|
|
1069
1087
|
|
|
1070
1088
|
|
|
@@ -1284,6 +1302,7 @@ def _populate_registry():
|
|
|
1284
1302
|
return
|
|
1285
1303
|
from renderers.deepseek_r1 import DeepSeekR1Renderer
|
|
1286
1304
|
from renderers.deepseek_v3 import DeepSeekV3Renderer
|
|
1305
|
+
from renderers.deepseek_v4 import DeepSeekV4Renderer
|
|
1287
1306
|
from renderers.default import DefaultRenderer
|
|
1288
1307
|
from renderers.glm5 import GLM5Renderer, GLM51Renderer
|
|
1289
1308
|
from renderers.glm45 import GLM45Renderer
|
|
@@ -1329,6 +1348,7 @@ def _populate_registry():
|
|
|
1329
1348
|
"minimax-m2": MiniMaxM2Renderer,
|
|
1330
1349
|
"deepseek-v3": DeepSeekV3Renderer,
|
|
1331
1350
|
"deepseek-r1": DeepSeekR1Renderer,
|
|
1351
|
+
"deepseek-v4": DeepSeekV4Renderer,
|
|
1332
1352
|
"hy3": Hy3Renderer,
|
|
1333
1353
|
"inkling": InklingRenderer,
|
|
1334
1354
|
"kimi-k2": KimiK2Renderer,
|
|
@@ -1759,36 +1779,122 @@ def trim_to_turn_close(
|
|
|
1759
1779
|
return previous_ids
|
|
1760
1780
|
|
|
1761
1781
|
|
|
1762
|
-
|
|
1763
|
-
"""
|
|
1782
|
+
class AttributedTextSegments(list[tuple[int, bool]]):
|
|
1783
|
+
"""Token/content pairs with an explicit attribution-availability flag."""
|
|
1784
|
+
|
|
1785
|
+
def __init__(
|
|
1786
|
+
self,
|
|
1787
|
+
values=(),
|
|
1788
|
+
*,
|
|
1789
|
+
has_content_attribution: bool,
|
|
1790
|
+
) -> None:
|
|
1791
|
+
super().__init__(values)
|
|
1792
|
+
self.has_content_attribution = has_content_attribution
|
|
1793
|
+
|
|
1794
|
+
|
|
1795
|
+
def _get_offset_tokenizer(tokenizer: Tokenizer) -> OffsetTokenizer | None:
|
|
1796
|
+
"""Return ``tokenizer`` when it supports character offsets, else ``None``.
|
|
1764
1797
|
|
|
1765
1798
|
Hand-coded renderers concatenate scaffold + body in one BPE pass to
|
|
1766
1799
|
preserve cross-boundary merges, then attribute each resulting token
|
|
1767
1800
|
back to its source segment via the fast tokenizer's
|
|
1768
|
-
``offset_mapping`` (see :func:`attribute_text_segments`).
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
``PreTrainedTokenizerFast`` instances that satisfy this trivially.
|
|
1801
|
+
``offset_mapping`` (see :func:`attribute_text_segments`). Tokenizers
|
|
1802
|
+
loaded via :func:`load_tokenizer` are ``PreTrainedTokenizerFast``
|
|
1803
|
+
instances that satisfy this capability, but BYO tokenizers need not.
|
|
1772
1804
|
"""
|
|
1805
|
+
call = getattr(tokenizer, "__call__", None)
|
|
1806
|
+
if not callable(call):
|
|
1807
|
+
return None
|
|
1773
1808
|
try:
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1809
|
+
encoding = call("a", add_special_tokens=False, return_offsets_mapping=True)
|
|
1810
|
+
encoding["input_ids"]
|
|
1811
|
+
encoding["offset_mapping"]
|
|
1812
|
+
except (KeyError, NotImplementedError, TypeError, ValueError):
|
|
1813
|
+
return None
|
|
1814
|
+
return cast(OffsetTokenizer, tokenizer)
|
|
1815
|
+
|
|
1816
|
+
|
|
1817
|
+
def _infer_offsets_from_decode(
|
|
1818
|
+
tokenizer: Tokenizer,
|
|
1819
|
+
token_ids: list[int],
|
|
1820
|
+
text: str,
|
|
1821
|
+
) -> list[tuple[int, int]] | None:
|
|
1822
|
+
"""Recover token character spans from an exact decoder round-trip.
|
|
1823
|
+
|
|
1824
|
+
This is a narrow fallback for metadata that does not require exposing
|
|
1825
|
+
content attribution. Some renderers join text from multiple messages in a
|
|
1826
|
+
single BPE pass, so they still need to associate the resulting tokens with
|
|
1827
|
+
the right message when a BYO tokenizer has no native offset mapping.
|
|
1828
|
+
|
|
1829
|
+
Decoding individual tokens is linear and exact for the common BPE/SentencePiece
|
|
1830
|
+
backends. Byte-fallback tokenizers can require multiple tokens before text
|
|
1831
|
+
becomes valid, so a validated cumulative-prefix pass handles that case.
|
|
1832
|
+
If either strategy cannot reconstruct ``text`` exactly, callers must use a
|
|
1833
|
+
conservative renderer-specific message-index fallback. This helper never
|
|
1834
|
+
upgrades the tokenizer's content-attribution capability: ``is_content``
|
|
1835
|
+
remains unavailable without native offsets.
|
|
1836
|
+
"""
|
|
1837
|
+
|
|
1838
|
+
def decode(ids: list[int]) -> str | None:
|
|
1839
|
+
variants = (
|
|
1840
|
+
{"skip_special_tokens": False, "clean_up_tokenization_spaces": False},
|
|
1841
|
+
{"skip_special_tokens": False},
|
|
1842
|
+
{},
|
|
1843
|
+
)
|
|
1844
|
+
for kwargs in variants:
|
|
1845
|
+
try:
|
|
1846
|
+
decoded = tokenizer.decode(ids, **kwargs)
|
|
1847
|
+
except TypeError:
|
|
1848
|
+
continue
|
|
1849
|
+
except (KeyError, NotImplementedError, UnicodeError, ValueError):
|
|
1850
|
+
return None
|
|
1851
|
+
return decoded if isinstance(decoded, str) else None
|
|
1852
|
+
return None
|
|
1853
|
+
|
|
1854
|
+
pieces: list[str] = []
|
|
1855
|
+
for token_id in token_ids:
|
|
1856
|
+
piece = decode([token_id])
|
|
1857
|
+
if piece is None:
|
|
1858
|
+
break
|
|
1859
|
+
pieces.append(piece)
|
|
1860
|
+
if len(pieces) == len(token_ids) and "".join(pieces) == text:
|
|
1861
|
+
offsets: list[tuple[int, int]] = []
|
|
1862
|
+
position = 0
|
|
1863
|
+
for piece in pieces:
|
|
1864
|
+
end = position + len(piece)
|
|
1865
|
+
offsets.append((position, end))
|
|
1866
|
+
position = end
|
|
1867
|
+
return offsets
|
|
1868
|
+
|
|
1869
|
+
offsets = []
|
|
1870
|
+
previous_end = 0
|
|
1871
|
+
for end_index in range(1, len(token_ids) + 1):
|
|
1872
|
+
prefix = decode(token_ids[:end_index])
|
|
1873
|
+
if prefix is None or len(prefix) < previous_end or not text.startswith(prefix):
|
|
1874
|
+
return None
|
|
1875
|
+
current_end = len(prefix)
|
|
1876
|
+
offsets.append((previous_end, current_end))
|
|
1877
|
+
previous_end = current_end
|
|
1878
|
+
if previous_end != len(text):
|
|
1879
|
+
return None
|
|
1880
|
+
return offsets
|
|
1881
|
+
|
|
1882
|
+
|
|
1883
|
+
def _content_mask_or_empty(
|
|
1884
|
+
tokenizer: Tokenizer, content_mask: list[bool]
|
|
1885
|
+
) -> list[bool]:
|
|
1886
|
+
"""Return exact content attribution, or the empty-list unavailable sentinel."""
|
|
1887
|
+
if _get_offset_tokenizer(tokenizer) is None:
|
|
1888
|
+
return []
|
|
1889
|
+
return content_mask
|
|
1784
1890
|
|
|
1785
1891
|
|
|
1786
1892
|
def attribute_text_segments(
|
|
1787
|
-
tokenizer,
|
|
1893
|
+
tokenizer: Tokenizer,
|
|
1788
1894
|
segments: "list[tuple[str, bool]]",
|
|
1789
1895
|
*,
|
|
1790
1896
|
overlap_is_content: bool = False,
|
|
1791
|
-
) ->
|
|
1897
|
+
) -> AttributedTextSegments:
|
|
1792
1898
|
"""Tokenize concatenated segments as a single BPE pass and return
|
|
1793
1899
|
``(token_id, is_content)`` pairs.
|
|
1794
1900
|
|
|
@@ -1816,23 +1922,29 @@ def attribute_text_segments(
|
|
|
1816
1922
|
every body byte inside the ``is_content=True`` run at the cost of a
|
|
1817
1923
|
few adjacent wrap bytes.
|
|
1818
1924
|
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1925
|
+
When ``tokenizer`` implements :class:`OffsetTokenizer`, the result's
|
|
1926
|
+
``has_content_attribution`` flag is true and each bool is exact. For a
|
|
1927
|
+
basic :class:`Tokenizer`, the joined text is still encoded in one pass so
|
|
1928
|
+
token IDs remain identical, but the bools are placeholders and
|
|
1929
|
+
``has_content_attribution`` is false. Renderers propagate that state as an
|
|
1930
|
+
empty ``RenderedTokens.is_content`` list rather than exposing a partial or
|
|
1931
|
+
inaccurate mask.
|
|
1826
1932
|
|
|
1827
1933
|
Empty input or empty joined text returns an empty list.
|
|
1828
1934
|
"""
|
|
1829
1935
|
if not segments:
|
|
1830
|
-
return []
|
|
1936
|
+
return AttributedTextSegments([], has_content_attribution=True)
|
|
1831
1937
|
full_text = "".join(text for text, _ in segments)
|
|
1832
1938
|
if not full_text:
|
|
1833
|
-
return []
|
|
1939
|
+
return AttributedTextSegments([], has_content_attribution=True)
|
|
1834
1940
|
|
|
1835
1941
|
offset_tokenizer = _get_offset_tokenizer(tokenizer)
|
|
1942
|
+
if offset_tokenizer is None:
|
|
1943
|
+
token_ids = tokenizer.encode(full_text, add_special_tokens=False)
|
|
1944
|
+
return AttributedTextSegments(
|
|
1945
|
+
((token_id, False) for token_id in token_ids),
|
|
1946
|
+
has_content_attribution=False,
|
|
1947
|
+
)
|
|
1836
1948
|
encoding = offset_tokenizer(
|
|
1837
1949
|
full_text,
|
|
1838
1950
|
add_special_tokens=False,
|
|
@@ -1887,7 +1999,7 @@ def attribute_text_segments(
|
|
|
1887
1999
|
# the last non-empty segment's bit.
|
|
1888
2000
|
pass
|
|
1889
2001
|
out.append((tok_id, is_content))
|
|
1890
|
-
return out
|
|
2002
|
+
return AttributedTextSegments(out, has_content_attribution=True)
|
|
1891
2003
|
|
|
1892
2004
|
|
|
1893
2005
|
def reject_assistant_in_extension(new_messages: list[Message]) -> bool:
|
|
@@ -930,6 +930,47 @@ class DeepSeekR1RendererConfig(BaseRendererConfig):
|
|
|
930
930
|
_template_fields = frozenset()
|
|
931
931
|
|
|
932
932
|
|
|
933
|
+
class DeepSeekV4RendererConfig(BaseRendererConfig):
|
|
934
|
+
"""DeepSeek-V4-Flash-0731 reference-encoder configuration.
|
|
935
|
+
|
|
936
|
+
The checkpoint ships a Python encoder rather than a Jinja template. These
|
|
937
|
+
fields mirror its public controls: chat vs thinking mode, historical
|
|
938
|
+
reasoning dropping, and the opt-in thinking-effort prefix.
|
|
939
|
+
"""
|
|
940
|
+
|
|
941
|
+
name: Literal["deepseek-v4"] = "deepseek-v4"
|
|
942
|
+
_template_fields = frozenset(
|
|
943
|
+
{"enable_thinking", "drop_thinking", "reasoning_effort"}
|
|
944
|
+
)
|
|
945
|
+
|
|
946
|
+
enable_thinking: bool = False
|
|
947
|
+
"""Select thinking mode. ``False`` matches the official inference script."""
|
|
948
|
+
|
|
949
|
+
drop_thinking: bool = True
|
|
950
|
+
"""Drop reasoning before the latest user query when no tools are present.
|
|
951
|
+
|
|
952
|
+
The reference encoder automatically preserves all reasoning whenever tools
|
|
953
|
+
are supplied, regardless of this value.
|
|
954
|
+
"""
|
|
955
|
+
|
|
956
|
+
reasoning_effort: Literal["low", "high", "max"] = "low"
|
|
957
|
+
"""Thinking-only effort prefix; ``low`` adds no text.
|
|
958
|
+
|
|
959
|
+
``low`` is the checkpoint Python encoder's default. DeepSeek's hosted API
|
|
960
|
+
independently defaults its thinking effort to ``high``.
|
|
961
|
+
"""
|
|
962
|
+
|
|
963
|
+
@model_validator(mode="after")
|
|
964
|
+
def _check_thinking_retention(self):
|
|
965
|
+
_reject_thinking_retention_conflict(
|
|
966
|
+
self,
|
|
967
|
+
"drop_thinking",
|
|
968
|
+
true_implies="tool_cycle",
|
|
969
|
+
false_implies="all",
|
|
970
|
+
)
|
|
971
|
+
return self
|
|
972
|
+
|
|
973
|
+
|
|
933
974
|
RendererConfig = Annotated[
|
|
934
975
|
Union[
|
|
935
976
|
AutoRendererConfig,
|
|
@@ -960,6 +1001,7 @@ RendererConfig = Annotated[
|
|
|
960
1001
|
Nemotron35RendererConfig,
|
|
961
1002
|
DeepSeekV3RendererConfig,
|
|
962
1003
|
DeepSeekR1RendererConfig,
|
|
1004
|
+
DeepSeekV4RendererConfig,
|
|
963
1005
|
],
|
|
964
1006
|
Field(discriminator="name"),
|
|
965
1007
|
]
|
|
@@ -1006,6 +1048,7 @@ _CONFIG_BY_NAME: dict[str, type[BaseRendererConfig]] = {
|
|
|
1006
1048
|
"nemotron-3.5": Nemotron35RendererConfig,
|
|
1007
1049
|
"deepseek-v3": DeepSeekV3RendererConfig,
|
|
1008
1050
|
"deepseek-r1": DeepSeekR1RendererConfig,
|
|
1051
|
+
"deepseek-v4": DeepSeekV4RendererConfig,
|
|
1009
1052
|
}
|
|
1010
1053
|
|
|
1011
1054
|
|
|
@@ -1039,6 +1082,7 @@ __all__ = [
|
|
|
1039
1082
|
"DefaultRendererConfig",
|
|
1040
1083
|
"DeepSeekR1RendererConfig",
|
|
1041
1084
|
"DeepSeekV3RendererConfig",
|
|
1085
|
+
"DeepSeekV4RendererConfig",
|
|
1042
1086
|
"GLM45RendererConfig",
|
|
1043
1087
|
"GLM51RendererConfig",
|
|
1044
1088
|
"GLM5RendererConfig",
|
|
@@ -20,6 +20,7 @@ from renderers.base import (
|
|
|
20
20
|
RenderedTokens,
|
|
21
21
|
ToolSpec,
|
|
22
22
|
Tokenizer,
|
|
23
|
+
_content_mask_or_empty,
|
|
23
24
|
attribute_text_segments,
|
|
24
25
|
extract_message_tool_names,
|
|
25
26
|
reject_assistant_in_extension,
|
|
@@ -259,7 +260,7 @@ class DeepSeekV3Renderer:
|
|
|
259
260
|
token_ids=tokens,
|
|
260
261
|
message_indices=indices,
|
|
261
262
|
sampled_mask=sampled,
|
|
262
|
-
is_content=content_mask,
|
|
263
|
+
is_content=_content_mask_or_empty(self._tokenizer, content_mask),
|
|
263
264
|
message_roles=[m.get("role") or "" for m in messages],
|
|
264
265
|
message_tool_names=extract_message_tool_names(messages),
|
|
265
266
|
)
|
|
@@ -408,7 +409,9 @@ class DeepSeekV3Renderer:
|
|
|
408
409
|
token_ids=previous_ids + ext,
|
|
409
410
|
message_indices=[-1] * len(previous_ids) + ext_indices,
|
|
410
411
|
sampled_mask=[False] * total_len,
|
|
411
|
-
is_content=
|
|
412
|
+
is_content=_content_mask_or_empty(
|
|
413
|
+
self._tokenizer, [False] * len(previous_ids) + ext_content
|
|
414
|
+
),
|
|
412
415
|
message_roles=[m.get("role") or "" for m in new_messages],
|
|
413
416
|
message_tool_names=extract_message_tool_names(new_messages),
|
|
414
417
|
)
|