renderers 0.1.12.dev4__tar.gz → 0.1.12.dev6__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.12.dev4 → renderers-0.1.12.dev6}/.github/workflows/test.yml +1 -1
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/PKG-INFO +3 -3
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/README.md +1 -1
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/docs/renderer-config.md +2 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/pyproject.toml +1 -1
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/renderers/__init__.py +13 -9
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/renderers/_version.py +2 -2
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/renderers/base.py +5 -2
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/renderers/configs.py +27 -2
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/renderers/glm5.py +198 -14
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/tests/parity.py +8 -4
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/tests/test_renderer_config.py +7 -1
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/uv.lock +271 -10
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/.github/workflows/publish-dev.yml +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/.github/workflows/publish.yml +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/.github/workflows/style.yml +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/.gitignore +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/.pre-commit-config.yaml +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/LICENSE +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/examples/README.md +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/examples/sglang/multiturn_generate_sglang.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/examples/sglang/online_multiturn_sglang.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/examples/tinker/multiturn_generate_tinker.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/examples/transformers/multiturn_generate_transformers.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/examples/vllm/multiturn_generate_vllm.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/renderers/client.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/renderers/deepseek_r1.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/renderers/deepseek_v3.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/renderers/deepseek_v4.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/renderers/default.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/renderers/gemma4.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/renderers/glm45.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/renderers/gpt_oss.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/renderers/hy3.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/renderers/inkling.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/renderers/kimi_k2.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/renderers/kimi_k25.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/renderers/laguna_s21.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/renderers/laguna_xs2.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/renderers/llama_3.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/renderers/minimax_m2.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/renderers/nemotron3.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/renderers/parsers.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/renderers/parsing.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/renderers/prime_qwen3.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/renderers/qwen3.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/renderers/qwen35.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/renderers/qwen36.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/renderers/qwen38.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/renderers/qwen3_vl.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/renderers/reasoning.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/tests/conftest.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/tests/reference_rendering.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/tests/test_bridge.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/tests/test_build_helpers.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/tests/test_client.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/tests/test_deepseek_r1.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/tests/test_deepseek_v4.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/tests/test_disabled_thinking_stability.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/tests/test_gemma4.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/tests/test_glm_tool_name_validation.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/tests/test_hy3.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/tests/test_incremental.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/tests/test_inkling.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/tests/test_is_content.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/tests/test_kimi_k25_tool_schema.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/tests/test_laguna_m1.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/tests/test_laguna_xs21.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/tests/test_llama_3.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/tests/test_load_tokenizer.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/tests/test_message_indices.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/tests/test_message_tool_names.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/tests/test_multimodal.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/tests/test_nemotron3_ultra.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/tests/test_offsetless_tokenizers.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/tests/test_parity.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/tests/test_parse_response.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/tests/test_parse_response_robustness.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/tests/test_parsers.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/tests/test_preserve_thinking.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/tests/test_qwen35_size_coverage.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/tests/test_qwen38.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/tests/test_reasoning_boundaries.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/tests/test_roundtrip.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/tests/test_sampled_mask.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/tests/test_tokens_per_message.py +0 -0
- {renderers-0.1.12.dev4 → renderers-0.1.12.dev6}/tests/test_tool_arg_type_preservation.py +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: renderers
|
|
3
|
-
Version: 0.1.12.
|
|
3
|
+
Version: 0.1.12.dev6
|
|
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
|
|
7
|
-
Requires-Python: <3.
|
|
7
|
+
Requires-Python: <3.15,>=3.10
|
|
8
8
|
Requires-Dist: jinja2
|
|
9
9
|
Requires-Dist: numpy
|
|
10
10
|
Requires-Dist: openai-harmony>=0.0.4
|
|
@@ -106,7 +106,7 @@ next_prompt_ids = r.bridge_to_next_turn(
|
|
|
106
106
|
)
|
|
107
107
|
```
|
|
108
108
|
|
|
109
|
-
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).
|
|
109
|
+
Hand-coded renderers ship for `qwen3`, `qwen3-vl`, `qwen3.5`, `qwen3.6`, `qwen3.8`, `gemma4`, `glm-5`, `glm-5.1`, `glm-5.3`, `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).
|
|
110
110
|
|
|
111
111
|
## API
|
|
112
112
|
|
|
@@ -86,7 +86,7 @@ next_prompt_ids = r.bridge_to_next_turn(
|
|
|
86
86
|
)
|
|
87
87
|
```
|
|
88
88
|
|
|
89
|
-
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).
|
|
89
|
+
Hand-coded renderers ship for `qwen3`, `qwen3-vl`, `qwen3.5`, `qwen3.6`, `qwen3.8`, `gemma4`, `glm-5`, `glm-5.1`, `glm-5.3`, `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).
|
|
90
90
|
|
|
91
91
|
## API
|
|
92
92
|
|
|
@@ -34,6 +34,7 @@ product in `tests/test_parity.py`.
|
|
|
34
34
|
| Qwen3-VL | `Qwen3VLRendererConfig` | `add_vision_id` | `image_cache_max` |
|
|
35
35
|
| Gemma 4 | `Gemma4RendererConfig` | `enable_thinking`, `preserve_thinking` | `image_cache_max` |
|
|
36
36
|
| GLM-5 / 5.1 | `GLM5RendererConfig` / `GLM51RendererConfig` | `enable_thinking`, `clear_thinking` | - |
|
|
37
|
+
| GLM-5.3 | `GLM53RendererConfig` | `clear_thinking`, `reasoning_effort` | - |
|
|
37
38
|
| GLM-4.5 | `GLM45RendererConfig` | `enable_thinking` | - |
|
|
38
39
|
| gpt-oss | `GptOssRendererConfig` | `reasoning_effort`, `conversation_start_date` | `use_system_prompt`, `knowledge_cutoff`, `model_identity`, `auto_drop_analysis` |
|
|
39
40
|
| Hy3 | `Hy3RendererConfig` | `reasoning_effort`, `preserved_thinking`, `is_training`, `raw_last_assistant`, `fallback_strategy` | - |
|
|
@@ -140,6 +141,7 @@ the knobs its template actually exposes:
|
|
|
140
141
|
| Qwen3.8 | `preserve_thinking=True -> all`; else `enable_thinking=False -> all`; else `tool_cycle` |
|
|
141
142
|
| Gemma 4 | `preserve_thinking=True -> all`; else `enable_thinking=False -> all`; else `tool_cycle` |
|
|
142
143
|
| GLM-5 / 5.1 | `clear_thinking=False -> all`; else `enable_thinking=False -> all`; else `tool_cycle` |
|
|
144
|
+
| GLM-5.3 | `clear_thinking=False -> all`, else `tool_cycle` |
|
|
143
145
|
| GLM-4.5 | `enable_thinking=False -> all`, else `tool_cycle` |
|
|
144
146
|
| gpt-oss | `auto_drop_analysis=False -> all`, else `tool_cycle` |
|
|
145
147
|
| Hy3 | `preserved_thinking=True -> all`, else `tool_cycle` |
|
|
@@ -13,7 +13,7 @@ description = "Chat template renderers — deterministic message-to-token conver
|
|
|
13
13
|
readme = "README.md"
|
|
14
14
|
license = "Apache-2.0"
|
|
15
15
|
license-files = ["LICENSE"]
|
|
16
|
-
requires-python = ">=3.10,<3.
|
|
16
|
+
requires-python = ">=3.10,<3.15"
|
|
17
17
|
dependencies = [
|
|
18
18
|
"numpy",
|
|
19
19
|
"openai>=1.108.1",
|
|
@@ -7,11 +7,11 @@ except ImportError:
|
|
|
7
7
|
__version__ = "0+unknown"
|
|
8
8
|
|
|
9
9
|
from renderers.base import (
|
|
10
|
+
MULTIMODAL_MODELS,
|
|
10
11
|
ChatTemplateTokenizer,
|
|
11
12
|
Content,
|
|
12
13
|
ContentPart,
|
|
13
14
|
ImagePart,
|
|
14
|
-
MULTIMODAL_MODELS,
|
|
15
15
|
Message,
|
|
16
16
|
MultiModalData,
|
|
17
17
|
MultimodalRenderer,
|
|
@@ -44,36 +44,37 @@ from renderers.client import MalformedGenerateResponseError, OverlongPromptError
|
|
|
44
44
|
from renderers.configs import (
|
|
45
45
|
AutoRendererConfig,
|
|
46
46
|
BaseRendererConfig,
|
|
47
|
-
config_from_name,
|
|
48
|
-
DefaultRendererConfig,
|
|
49
47
|
DeepSeekR1RendererConfig,
|
|
50
48
|
DeepSeekV3RendererConfig,
|
|
51
49
|
DeepSeekV4RendererConfig,
|
|
50
|
+
DefaultRendererConfig,
|
|
51
|
+
Gemma4RendererConfig,
|
|
52
|
+
GLM5RendererConfig,
|
|
52
53
|
GLM45RendererConfig,
|
|
53
54
|
GLM51RendererConfig,
|
|
54
|
-
|
|
55
|
-
Gemma4RendererConfig,
|
|
55
|
+
GLM53RendererConfig,
|
|
56
56
|
GptOssRendererConfig,
|
|
57
57
|
Hy3RendererConfig,
|
|
58
58
|
InklingRendererConfig,
|
|
59
|
-
KimiK25RendererConfig,
|
|
60
59
|
KimiK2RendererConfig,
|
|
60
|
+
KimiK25RendererConfig,
|
|
61
61
|
LagunaM1RendererConfig,
|
|
62
62
|
LagunaS21RendererConfig,
|
|
63
63
|
LagunaXS2RendererConfig,
|
|
64
64
|
LagunaXS21RendererConfig,
|
|
65
65
|
Llama3RendererConfig,
|
|
66
66
|
MiniMaxM2RendererConfig,
|
|
67
|
-
Nemotron35RendererConfig,
|
|
68
67
|
Nemotron3RendererConfig,
|
|
69
68
|
Nemotron3UltraRendererConfig,
|
|
69
|
+
Nemotron35RendererConfig,
|
|
70
70
|
PrimeQwen3RendererConfig,
|
|
71
|
+
Qwen3RendererConfig,
|
|
72
|
+
Qwen3VLRendererConfig,
|
|
71
73
|
Qwen35RendererConfig,
|
|
72
74
|
Qwen36RendererConfig,
|
|
73
75
|
Qwen38RendererConfig,
|
|
74
|
-
Qwen3RendererConfig,
|
|
75
|
-
Qwen3VLRendererConfig,
|
|
76
76
|
RendererConfig,
|
|
77
|
+
config_from_name,
|
|
77
78
|
)
|
|
78
79
|
|
|
79
80
|
# Concrete renderer classes are lazy-loaded so that consumers needing only the
|
|
@@ -90,6 +91,7 @@ _LAZY_RENDERERS: dict[str, str] = {
|
|
|
90
91
|
"DefaultRenderer": "renderers.default",
|
|
91
92
|
"GLM45Renderer": "renderers.glm45",
|
|
92
93
|
"GLM51Renderer": "renderers.glm5",
|
|
94
|
+
"GLM53Renderer": "renderers.glm5",
|
|
93
95
|
"GLM5Renderer": "renderers.glm5",
|
|
94
96
|
"GptOssRenderer": "renderers.gpt_oss",
|
|
95
97
|
"Gemma4Renderer": "renderers.gemma4",
|
|
@@ -148,6 +150,8 @@ __all__ = [
|
|
|
148
150
|
"GLM45RendererConfig",
|
|
149
151
|
"GLM51Renderer",
|
|
150
152
|
"GLM51RendererConfig",
|
|
153
|
+
"GLM53Renderer",
|
|
154
|
+
"GLM53RendererConfig",
|
|
151
155
|
"GLM5Renderer",
|
|
152
156
|
"GLM5RendererConfig",
|
|
153
157
|
"Gemma4Renderer",
|
|
@@ -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.12.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 1, 12, '
|
|
21
|
+
__version__ = version = '0.1.12.dev6'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 12, 'dev6')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -984,6 +984,8 @@ MODEL_RENDERER_MAP: dict[str, str] = {
|
|
|
984
984
|
"zai-org/GLM-5-FP8": "glm-5",
|
|
985
985
|
"zai-org/GLM-4.7-Flash": "glm-5",
|
|
986
986
|
"zai-org/GLM-5.1": "glm-5.1",
|
|
987
|
+
"zai-org/GLM-5.3": "glm-5.3",
|
|
988
|
+
"zai-org/GLM-5.3-BF16": "glm-5.3",
|
|
987
989
|
# GLM-4.5.
|
|
988
990
|
"THUDM/GLM-4.5-Air": "glm-4.5",
|
|
989
991
|
"zai-org/GLM-4.5-Air": "glm-4.5",
|
|
@@ -1318,10 +1320,10 @@ def _populate_registry():
|
|
|
1318
1320
|
from renderers.deepseek_v3 import DeepSeekV3Renderer
|
|
1319
1321
|
from renderers.deepseek_v4 import DeepSeekV4Renderer
|
|
1320
1322
|
from renderers.default import DefaultRenderer
|
|
1321
|
-
from renderers.
|
|
1323
|
+
from renderers.gemma4 import Gemma4Renderer
|
|
1324
|
+
from renderers.glm5 import GLM5Renderer, GLM51Renderer, GLM53Renderer
|
|
1322
1325
|
from renderers.glm45 import GLM45Renderer
|
|
1323
1326
|
from renderers.gpt_oss import GptOssRenderer
|
|
1324
|
-
from renderers.gemma4 import Gemma4Renderer
|
|
1325
1327
|
from renderers.hy3 import Hy3Renderer
|
|
1326
1328
|
from renderers.inkling import InklingRenderer
|
|
1327
1329
|
from renderers.kimi_k2 import KimiK2Renderer
|
|
@@ -1358,6 +1360,7 @@ def _populate_registry():
|
|
|
1358
1360
|
"qwen3.8": Qwen38Renderer,
|
|
1359
1361
|
"glm-5": GLM5Renderer,
|
|
1360
1362
|
"glm-5.1": GLM51Renderer,
|
|
1363
|
+
"glm-5.3": GLM53Renderer,
|
|
1361
1364
|
"glm-4.5": GLM45Renderer,
|
|
1362
1365
|
"minimax-m2": MiniMaxM2Renderer,
|
|
1363
1366
|
"deepseek-v3": DeepSeekV3Renderer,
|
|
@@ -411,6 +411,29 @@ class GLM51RendererConfig(BaseRendererConfig):
|
|
|
411
411
|
return self
|
|
412
412
|
|
|
413
413
|
|
|
414
|
+
class GLM53RendererConfig(BaseRendererConfig):
|
|
415
|
+
"""GLM-5.3 renderer config."""
|
|
416
|
+
|
|
417
|
+
name: Literal["glm-5.3"] = "glm-5.3"
|
|
418
|
+
_template_fields = frozenset({"clear_thinking", "reasoning_effort"})
|
|
419
|
+
|
|
420
|
+
clear_thinking: bool = False
|
|
421
|
+
"""Drop reasoning from historical assistant turns when ``True``."""
|
|
422
|
+
|
|
423
|
+
reasoning_effort: Literal["low", "high", "max"] = "max"
|
|
424
|
+
"""Reasoning-effort system preamble emitted by the canonical template."""
|
|
425
|
+
|
|
426
|
+
@model_validator(mode="after")
|
|
427
|
+
def _check_thinking_retention(self):
|
|
428
|
+
_reject_thinking_retention_conflict(
|
|
429
|
+
self,
|
|
430
|
+
"clear_thinking",
|
|
431
|
+
true_implies="tool_cycle",
|
|
432
|
+
false_implies="all",
|
|
433
|
+
)
|
|
434
|
+
return self
|
|
435
|
+
|
|
436
|
+
|
|
414
437
|
class GLM45RendererConfig(BaseRendererConfig):
|
|
415
438
|
"""GLM-4.5 Air renderer config."""
|
|
416
439
|
|
|
@@ -984,6 +1007,7 @@ RendererConfig = Annotated[
|
|
|
984
1007
|
Gemma4RendererConfig,
|
|
985
1008
|
GLM5RendererConfig,
|
|
986
1009
|
GLM51RendererConfig,
|
|
1010
|
+
GLM53RendererConfig,
|
|
987
1011
|
GLM45RendererConfig,
|
|
988
1012
|
GptOssRendererConfig,
|
|
989
1013
|
Hy3RendererConfig,
|
|
@@ -1031,6 +1055,7 @@ _CONFIG_BY_NAME: dict[str, type[BaseRendererConfig]] = {
|
|
|
1031
1055
|
"gemma4": Gemma4RendererConfig,
|
|
1032
1056
|
"glm-5": GLM5RendererConfig,
|
|
1033
1057
|
"glm-5.1": GLM51RendererConfig,
|
|
1058
|
+
"glm-5.3": GLM53RendererConfig,
|
|
1034
1059
|
"glm-4.5": GLM45RendererConfig,
|
|
1035
1060
|
"gpt-oss": GptOssRendererConfig,
|
|
1036
1061
|
"hy3": Hy3RendererConfig,
|
|
@@ -1056,8 +1081,7 @@ def _config_class_for(name: str) -> type[BaseRendererConfig]:
|
|
|
1056
1081
|
cls = _CONFIG_BY_NAME.get(name)
|
|
1057
1082
|
if cls is None:
|
|
1058
1083
|
raise ValueError(
|
|
1059
|
-
f"No renderer config registered for name={name!r}. "
|
|
1060
|
-
f"Known: {sorted(_CONFIG_BY_NAME)}"
|
|
1084
|
+
f"No renderer config registered for name={name!r}. Known: {sorted(_CONFIG_BY_NAME)}"
|
|
1061
1085
|
)
|
|
1062
1086
|
return cls
|
|
1063
1087
|
|
|
@@ -1085,6 +1109,7 @@ __all__ = [
|
|
|
1085
1109
|
"DeepSeekV4RendererConfig",
|
|
1086
1110
|
"GLM45RendererConfig",
|
|
1087
1111
|
"GLM51RendererConfig",
|
|
1112
|
+
"GLM53RendererConfig",
|
|
1088
1113
|
"GLM5RendererConfig",
|
|
1089
1114
|
"Gemma4RendererConfig",
|
|
1090
1115
|
"GptOssRendererConfig",
|
|
@@ -20,8 +20,8 @@ from renderers.base import (
|
|
|
20
20
|
Message,
|
|
21
21
|
ParsedResponse,
|
|
22
22
|
RenderedTokens,
|
|
23
|
-
ToolSpec,
|
|
24
23
|
Tokenizer,
|
|
24
|
+
ToolSpec,
|
|
25
25
|
_content_mask_or_empty,
|
|
26
26
|
attribute_text_segments,
|
|
27
27
|
extract_message_tool_names,
|
|
@@ -29,7 +29,11 @@ from renderers.base import (
|
|
|
29
29
|
resolve_thinking_retention,
|
|
30
30
|
should_rerender_for_thinking_retention,
|
|
31
31
|
)
|
|
32
|
-
from renderers.configs import
|
|
32
|
+
from renderers.configs import (
|
|
33
|
+
GLM5RendererConfig,
|
|
34
|
+
GLM51RendererConfig,
|
|
35
|
+
GLM53RendererConfig,
|
|
36
|
+
)
|
|
33
37
|
from renderers.parsing import parse_glm
|
|
34
38
|
|
|
35
39
|
_TOOLS_HEADER = (
|
|
@@ -73,7 +77,7 @@ class GLM5Renderer:
|
|
|
73
77
|
self.config = config or type(self)._config_cls()
|
|
74
78
|
if not self.config.clear_thinking:
|
|
75
79
|
implied_thinking_retention = "all"
|
|
76
|
-
elif not self.config
|
|
80
|
+
elif not getattr(self.config, "enable_thinking", True):
|
|
77
81
|
implied_thinking_retention = "all"
|
|
78
82
|
else:
|
|
79
83
|
implied_thinking_retention = "tool_cycle"
|
|
@@ -262,14 +266,20 @@ class GLM5Renderer:
|
|
|
262
266
|
)
|
|
263
267
|
|
|
264
268
|
elif role == "tool":
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
269
|
+
if i == 0 or messages[i - 1]["role"] != "tool":
|
|
270
|
+
for position, tool_idx in enumerate(
|
|
271
|
+
self._ordered_tool_indices(messages, i)
|
|
272
|
+
):
|
|
273
|
+
tool_message = messages[tool_idx]
|
|
274
|
+
self._render_tool(
|
|
275
|
+
messages,
|
|
276
|
+
tool_idx,
|
|
277
|
+
self._visible_text(tool_message.get("content")),
|
|
278
|
+
emit_special=emit_special,
|
|
279
|
+
emit_text=emit_text,
|
|
280
|
+
emit_text_segments=emit_text_segments,
|
|
281
|
+
starts_block=position == 0,
|
|
282
|
+
)
|
|
273
283
|
|
|
274
284
|
# ── Generation prompt ───────────────────────────────────────
|
|
275
285
|
# Gen prompt tokens are what the chat template prepends before
|
|
@@ -277,7 +287,7 @@ class GLM5Renderer:
|
|
|
277
287
|
# them. Always is_sampled=False / is_content=False.
|
|
278
288
|
if add_generation_prompt:
|
|
279
289
|
emit_special(self._assistant, -1, is_sampled=False, is_content=False)
|
|
280
|
-
if self.config
|
|
290
|
+
if getattr(self.config, "enable_thinking", True):
|
|
281
291
|
emit_special(self._think, -1, is_sampled=False, is_content=False)
|
|
282
292
|
else:
|
|
283
293
|
emit_special(self._think_end, -1, is_sampled=False, is_content=False)
|
|
@@ -291,6 +301,17 @@ class GLM5Renderer:
|
|
|
291
301
|
message_tool_names=extract_message_tool_names(messages),
|
|
292
302
|
)
|
|
293
303
|
|
|
304
|
+
@staticmethod
|
|
305
|
+
def _ordered_tool_indices(messages: list[Message], block_start: int) -> list[int]:
|
|
306
|
+
"""Return one contiguous tool block in its source order."""
|
|
307
|
+
block_end = block_start
|
|
308
|
+
while (
|
|
309
|
+
block_end + 1 < len(messages)
|
|
310
|
+
and messages[block_end + 1].get("role") == "tool"
|
|
311
|
+
):
|
|
312
|
+
block_end += 1
|
|
313
|
+
return list(range(block_start, block_end + 1))
|
|
314
|
+
|
|
294
315
|
def render_ids(
|
|
295
316
|
self,
|
|
296
317
|
messages: list[Message],
|
|
@@ -478,7 +499,7 @@ class GLM5Renderer:
|
|
|
478
499
|
|
|
479
500
|
# Generation prompt — match the gen-prompt branch of ``render()``.
|
|
480
501
|
emit_special(self._assistant, -1)
|
|
481
|
-
if self.config
|
|
502
|
+
if getattr(self.config, "enable_thinking", True):
|
|
482
503
|
emit_special(self._think, -1)
|
|
483
504
|
else:
|
|
484
505
|
emit_special(self._think_end, -1)
|
|
@@ -632,6 +653,7 @@ class GLM5Renderer:
|
|
|
632
653
|
emit_special,
|
|
633
654
|
emit_text,
|
|
634
655
|
emit_text_segments,
|
|
656
|
+
starts_block: bool = False,
|
|
635
657
|
) -> None:
|
|
636
658
|
# Tool body bytes get ``is_content=True``; the wraps are
|
|
637
659
|
# scaffold. The ``<|observation|>`` role tag is scaffold too
|
|
@@ -644,7 +666,7 @@ class GLM5Renderer:
|
|
|
644
666
|
prev_role = messages[msg_idx - 1]["role"] if msg_idx > 0 else None
|
|
645
667
|
closes_assistant_turn = prev_role == "assistant"
|
|
646
668
|
|
|
647
|
-
if prev_role != "tool":
|
|
669
|
+
if starts_block or prev_role != "tool":
|
|
648
670
|
emit_special(
|
|
649
671
|
self._observation,
|
|
650
672
|
msg_idx,
|
|
@@ -683,3 +705,165 @@ class GLM51Renderer(GLM5Renderer):
|
|
|
683
705
|
spec = tool["function"] if "function" in tool else tool
|
|
684
706
|
spec = {k: v for k, v in spec.items() if k not in ("defer_loading", "strict")}
|
|
685
707
|
return json.dumps(spec, ensure_ascii=False)
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
class GLM53Renderer(GLM5Renderer):
|
|
711
|
+
"""Deterministic message → token renderer for GLM-5.3 models.
|
|
712
|
+
|
|
713
|
+
GLM-5.3 adds a reasoning-effort system preamble and always starts an
|
|
714
|
+
assistant generation with ``<think>``. Its template also orders a tool
|
|
715
|
+
result block by the preceding assistant's tool-call IDs.
|
|
716
|
+
"""
|
|
717
|
+
|
|
718
|
+
_config_cls = GLM53RendererConfig
|
|
719
|
+
|
|
720
|
+
@staticmethod
|
|
721
|
+
def _visible_text(content: Any) -> str:
|
|
722
|
+
# The GLM-5.3 template omits ``None`` content instead of rendering
|
|
723
|
+
# Python's string representation (notably tool-call-only assistants).
|
|
724
|
+
return "" if content is None else GLM5Renderer._visible_text(content)
|
|
725
|
+
|
|
726
|
+
def __init__(
|
|
727
|
+
self,
|
|
728
|
+
tokenizer: Tokenizer,
|
|
729
|
+
config: GLM53RendererConfig | None = None,
|
|
730
|
+
):
|
|
731
|
+
super().__init__(tokenizer, config)
|
|
732
|
+
self.effective_thinking_retention = resolve_thinking_retention(
|
|
733
|
+
self.config,
|
|
734
|
+
"tool_cycle" if self.config.clear_thinking else "all",
|
|
735
|
+
)
|
|
736
|
+
|
|
737
|
+
@staticmethod
|
|
738
|
+
def _format_tool_spec(tool: ToolSpec) -> str:
|
|
739
|
+
spec = tool["function"] if "function" in tool else tool
|
|
740
|
+
spec = {k: v for k, v in spec.items() if k not in ("defer_loading", "strict")}
|
|
741
|
+
return json.dumps(spec, ensure_ascii=False)
|
|
742
|
+
|
|
743
|
+
def _ordered_tool_indices(
|
|
744
|
+
self, messages: list[Message], block_start: int
|
|
745
|
+
) -> list[int]:
|
|
746
|
+
"""Match the template's safe tool-result reordering rule."""
|
|
747
|
+
block_end = block_start
|
|
748
|
+
while (
|
|
749
|
+
block_end + 1 < len(messages)
|
|
750
|
+
and messages[block_end + 1].get("role") == "tool"
|
|
751
|
+
):
|
|
752
|
+
block_end += 1
|
|
753
|
+
|
|
754
|
+
previous = messages[block_start - 1] if block_start else {}
|
|
755
|
+
calls = (
|
|
756
|
+
previous.get("tool_calls") if previous.get("role") == "assistant" else None
|
|
757
|
+
)
|
|
758
|
+
if not calls:
|
|
759
|
+
return list(range(block_start, block_end + 1))
|
|
760
|
+
|
|
761
|
+
call_ids = [call.get("id") or call.get("tool_call_id") for call in calls]
|
|
762
|
+
result_ids = [
|
|
763
|
+
message.get("tool_call_id") or message.get("id")
|
|
764
|
+
for message in messages[block_start : block_end + 1]
|
|
765
|
+
]
|
|
766
|
+
if (
|
|
767
|
+
any(identifier is None for identifier in call_ids + result_ids)
|
|
768
|
+
or len(set(call_ids)) != len(call_ids)
|
|
769
|
+
or len(set(result_ids)) != len(result_ids)
|
|
770
|
+
or set(call_ids) != set(result_ids)
|
|
771
|
+
):
|
|
772
|
+
return list(range(block_start, block_end + 1))
|
|
773
|
+
|
|
774
|
+
by_id = {
|
|
775
|
+
identifier: index
|
|
776
|
+
for identifier, index in zip(result_ids, range(block_start, block_end + 1))
|
|
777
|
+
}
|
|
778
|
+
return [by_id[identifier] for identifier in call_ids]
|
|
779
|
+
|
|
780
|
+
def render(
|
|
781
|
+
self,
|
|
782
|
+
messages: list[Message],
|
|
783
|
+
*,
|
|
784
|
+
tools: list[ToolSpec] | None = None,
|
|
785
|
+
add_generation_prompt: bool = False,
|
|
786
|
+
) -> RenderedTokens:
|
|
787
|
+
rendered = super().render(
|
|
788
|
+
messages, tools=tools, add_generation_prompt=add_generation_prompt
|
|
789
|
+
)
|
|
790
|
+
# GLM-5.3's preamble belongs immediately after ``[gMASK]<sop>``.
|
|
791
|
+
prefix = [self._gmask, self._sop]
|
|
792
|
+
preamble = [self._system] + self._encode(
|
|
793
|
+
f"Reasoning Effort: {self.config.reasoning_effort.capitalize()}"
|
|
794
|
+
)
|
|
795
|
+
assert rendered.token_ids[:2] == prefix
|
|
796
|
+
insertion = len(prefix)
|
|
797
|
+
rendered.token_ids[insertion:insertion] = preamble
|
|
798
|
+
rendered.message_indices[insertion:insertion] = [-1] * len(preamble)
|
|
799
|
+
rendered.sampled_mask[insertion:insertion] = [False] * len(preamble)
|
|
800
|
+
if rendered.is_content:
|
|
801
|
+
rendered.is_content[insertion:insertion] = [False] * len(preamble)
|
|
802
|
+
return rendered
|
|
803
|
+
|
|
804
|
+
def _render_assistant(
|
|
805
|
+
self,
|
|
806
|
+
msg,
|
|
807
|
+
msg_idx,
|
|
808
|
+
content,
|
|
809
|
+
last_user_index,
|
|
810
|
+
*,
|
|
811
|
+
emit_special,
|
|
812
|
+
emit_text,
|
|
813
|
+
emit_text_segments,
|
|
814
|
+
):
|
|
815
|
+
reasoning_content = ""
|
|
816
|
+
has_reasoning = False
|
|
817
|
+
if isinstance(msg.get("reasoning_content"), str):
|
|
818
|
+
reasoning_content = msg["reasoning_content"]
|
|
819
|
+
has_reasoning = True
|
|
820
|
+
elif "</think>" in content:
|
|
821
|
+
before, after = content.split("</think>", 1)
|
|
822
|
+
reasoning_content = before.split("<think>")[-1].strip()
|
|
823
|
+
content = after.strip()
|
|
824
|
+
has_reasoning = True
|
|
825
|
+
|
|
826
|
+
emit_special(self._assistant, msg_idx, is_sampled=False, is_content=False)
|
|
827
|
+
include_thinking = has_reasoning and (
|
|
828
|
+
not self.config.clear_thinking or msg_idx > last_user_index
|
|
829
|
+
)
|
|
830
|
+
emit_special(self._think, msg_idx, is_sampled=False, is_content=False)
|
|
831
|
+
if include_thinking:
|
|
832
|
+
emit_text(reasoning_content, msg_idx, is_sampled=True, is_content=True)
|
|
833
|
+
emit_special(self._think_end, msg_idx, is_sampled=True, is_content=True)
|
|
834
|
+
if content.strip():
|
|
835
|
+
emit_text(content.strip(), msg_idx, is_sampled=True, is_content=True)
|
|
836
|
+
|
|
837
|
+
self._render_tool_calls(
|
|
838
|
+
msg, msg_idx, emit_special=emit_special, emit_text=emit_text
|
|
839
|
+
)
|
|
840
|
+
|
|
841
|
+
def _render_tool_calls(self, msg, msg_idx, *, emit_special, emit_text) -> None:
|
|
842
|
+
for tc in msg.get("tool_calls") or []:
|
|
843
|
+
func = tc.get("function") or tc
|
|
844
|
+
arguments = func.get("arguments", {})
|
|
845
|
+
if isinstance(arguments, str):
|
|
846
|
+
arguments = json.loads(arguments)
|
|
847
|
+
emit_special(self._tool_call_tok, msg_idx, is_sampled=True, is_content=True)
|
|
848
|
+
emit_text(func.get("name", ""), msg_idx, is_sampled=True, is_content=True)
|
|
849
|
+
for arg_name, arg_value in arguments.items():
|
|
850
|
+
emit_special(self._arg_key, msg_idx, is_sampled=True, is_content=True)
|
|
851
|
+
emit_text(arg_name, msg_idx, is_sampled=True, is_content=True)
|
|
852
|
+
emit_special(
|
|
853
|
+
self._arg_key_end, msg_idx, is_sampled=True, is_content=True
|
|
854
|
+
)
|
|
855
|
+
emit_special(self._arg_value, msg_idx, is_sampled=True, is_content=True)
|
|
856
|
+
emit_text(
|
|
857
|
+
arg_value
|
|
858
|
+
if isinstance(arg_value, str)
|
|
859
|
+
else json.dumps(arg_value, ensure_ascii=False),
|
|
860
|
+
msg_idx,
|
|
861
|
+
is_sampled=True,
|
|
862
|
+
is_content=True,
|
|
863
|
+
)
|
|
864
|
+
emit_special(
|
|
865
|
+
self._arg_value_end, msg_idx, is_sampled=True, is_content=True
|
|
866
|
+
)
|
|
867
|
+
emit_special(
|
|
868
|
+
self._tool_call_end_tok, msg_idx, is_sampled=True, is_content=True
|
|
869
|
+
)
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from dataclasses import dataclass
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from dataclasses import field as dataclass_field
|
|
6
7
|
from itertools import product
|
|
7
8
|
from typing import Any, Iterable, Mapping
|
|
8
9
|
|
|
@@ -10,7 +11,6 @@ from renderers.base import MODEL_RENDERER_MAP
|
|
|
10
11
|
from renderers.configs import _config_class_for
|
|
11
12
|
from tests.reference_rendering import reference_oracle_for_renderer
|
|
12
13
|
|
|
13
|
-
|
|
14
14
|
TOOLS = [
|
|
15
15
|
{
|
|
16
16
|
"type": "function",
|
|
@@ -158,6 +158,11 @@ MODEL_CATALOG = (
|
|
|
158
158
|
extra_suites={"tool-arg-types", "glm-tool-names"},
|
|
159
159
|
),
|
|
160
160
|
_model("zai-org/GLM-5.1", bridge=True),
|
|
161
|
+
_model(
|
|
162
|
+
"zai-org/GLM-5.3",
|
|
163
|
+
bridge=True,
|
|
164
|
+
extra_suites={"tool-arg-types", "glm-tool-names"},
|
|
165
|
+
),
|
|
161
166
|
_model("zai-org/GLM-4.7-Flash"),
|
|
162
167
|
_model(
|
|
163
168
|
"THUDM/GLM-4.5-Air",
|
|
@@ -710,8 +715,7 @@ def kwarg_combinations(case: ModelCase) -> tuple[dict[str, Any], ...]:
|
|
|
710
715
|
missing = set(fields) - KWARG_VALUES.keys()
|
|
711
716
|
if missing:
|
|
712
717
|
raise AssertionError(
|
|
713
|
-
f"{case.resolved_renderer} declares uncovered template fields: "
|
|
714
|
-
f"{sorted(missing)}"
|
|
718
|
+
f"{case.resolved_renderer} declares uncovered template fields: {sorted(missing)}"
|
|
715
719
|
)
|
|
716
720
|
if not fields:
|
|
717
721
|
return ({},)
|
|
@@ -7,12 +7,12 @@ from typing import Literal
|
|
|
7
7
|
|
|
8
8
|
import pytest
|
|
9
9
|
from pydantic import TypeAdapter, ValidationError
|
|
10
|
-
|
|
11
10
|
from renderers import (
|
|
12
11
|
AutoRendererConfig,
|
|
13
12
|
BaseRendererConfig,
|
|
14
13
|
DefaultRendererConfig,
|
|
15
14
|
GLM5RendererConfig,
|
|
15
|
+
GLM53RendererConfig,
|
|
16
16
|
GptOssRendererConfig,
|
|
17
17
|
Nemotron3RendererConfig,
|
|
18
18
|
Qwen3RendererConfig,
|
|
@@ -53,6 +53,12 @@ def test_discriminated_union_dispatches_on_name():
|
|
|
53
53
|
assert parsed.clear_thinking is False
|
|
54
54
|
|
|
55
55
|
|
|
56
|
+
def test_glm53_defaults_match_its_template():
|
|
57
|
+
cfg = GLM53RendererConfig()
|
|
58
|
+
assert cfg.clear_thinking is False
|
|
59
|
+
assert cfg.reasoning_effort == "max"
|
|
60
|
+
|
|
61
|
+
|
|
56
62
|
def test_discriminated_union_rejects_wrong_renderer_kwargs():
|
|
57
63
|
"""``add_vision_id`` under ``name="qwen3"`` is invalid at deserialise
|
|
58
64
|
time — the discriminator narrows to ``Qwen3RendererConfig`` whose
|