renderers 0.1.12.dev5__tar.gz → 0.1.12.dev7__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/PKG-INFO +2 -2
  2. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/README.md +1 -1
  3. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/docs/renderer-config.md +2 -0
  4. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/renderers/__init__.py +13 -9
  5. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/renderers/_version.py +2 -2
  6. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/renderers/base.py +5 -2
  7. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/renderers/configs.py +27 -2
  8. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/renderers/glm5.py +198 -14
  9. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/tests/parity.py +8 -4
  10. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/tests/test_renderer_config.py +7 -1
  11. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/uv.lock +3 -3
  12. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/.github/workflows/publish-dev.yml +0 -0
  13. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/.github/workflows/publish.yml +0 -0
  14. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/.github/workflows/style.yml +0 -0
  15. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/.github/workflows/test.yml +0 -0
  16. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/.gitignore +0 -0
  17. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/.pre-commit-config.yaml +0 -0
  18. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/LICENSE +0 -0
  19. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/examples/README.md +0 -0
  20. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/examples/sglang/multiturn_generate_sglang.py +0 -0
  21. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/examples/sglang/online_multiturn_sglang.py +0 -0
  22. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/examples/tinker/multiturn_generate_tinker.py +0 -0
  23. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/examples/transformers/multiturn_generate_transformers.py +0 -0
  24. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/examples/vllm/multiturn_generate_vllm.py +0 -0
  25. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/pyproject.toml +0 -0
  26. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/renderers/client.py +0 -0
  27. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/renderers/deepseek_r1.py +0 -0
  28. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/renderers/deepseek_v3.py +0 -0
  29. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/renderers/deepseek_v4.py +0 -0
  30. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/renderers/default.py +0 -0
  31. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/renderers/gemma4.py +0 -0
  32. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/renderers/glm45.py +0 -0
  33. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/renderers/gpt_oss.py +0 -0
  34. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/renderers/hy3.py +0 -0
  35. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/renderers/inkling.py +0 -0
  36. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/renderers/kimi_k2.py +0 -0
  37. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/renderers/kimi_k25.py +0 -0
  38. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/renderers/laguna_s21.py +0 -0
  39. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/renderers/laguna_xs2.py +0 -0
  40. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/renderers/llama_3.py +0 -0
  41. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/renderers/minimax_m2.py +0 -0
  42. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/renderers/nemotron3.py +0 -0
  43. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/renderers/parsers.py +0 -0
  44. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/renderers/parsing.py +0 -0
  45. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/renderers/prime_qwen3.py +0 -0
  46. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/renderers/qwen3.py +0 -0
  47. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/renderers/qwen35.py +0 -0
  48. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/renderers/qwen36.py +0 -0
  49. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/renderers/qwen38.py +0 -0
  50. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/renderers/qwen3_vl.py +0 -0
  51. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/renderers/reasoning.py +0 -0
  52. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/tests/conftest.py +0 -0
  53. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/tests/reference_rendering.py +0 -0
  54. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/tests/test_bridge.py +0 -0
  55. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/tests/test_build_helpers.py +0 -0
  56. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/tests/test_client.py +0 -0
  57. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/tests/test_deepseek_r1.py +0 -0
  58. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/tests/test_deepseek_v4.py +0 -0
  59. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/tests/test_disabled_thinking_stability.py +0 -0
  60. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/tests/test_gemma4.py +0 -0
  61. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/tests/test_glm_tool_name_validation.py +0 -0
  62. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/tests/test_hy3.py +0 -0
  63. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/tests/test_incremental.py +0 -0
  64. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/tests/test_inkling.py +0 -0
  65. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/tests/test_is_content.py +0 -0
  66. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/tests/test_kimi_k25_tool_schema.py +0 -0
  67. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/tests/test_laguna_m1.py +0 -0
  68. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/tests/test_laguna_xs21.py +0 -0
  69. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/tests/test_llama_3.py +0 -0
  70. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/tests/test_load_tokenizer.py +0 -0
  71. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/tests/test_message_indices.py +0 -0
  72. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/tests/test_message_tool_names.py +0 -0
  73. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/tests/test_multimodal.py +0 -0
  74. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/tests/test_nemotron3_ultra.py +0 -0
  75. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/tests/test_offsetless_tokenizers.py +0 -0
  76. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/tests/test_parity.py +0 -0
  77. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/tests/test_parse_response.py +0 -0
  78. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/tests/test_parse_response_robustness.py +0 -0
  79. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/tests/test_parsers.py +0 -0
  80. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/tests/test_preserve_thinking.py +0 -0
  81. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/tests/test_qwen35_size_coverage.py +0 -0
  82. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/tests/test_qwen38.py +0 -0
  83. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/tests/test_reasoning_boundaries.py +0 -0
  84. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/tests/test_roundtrip.py +0 -0
  85. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/tests/test_sampled_mask.py +0 -0
  86. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/tests/test_tokens_per_message.py +0 -0
  87. {renderers-0.1.12.dev5 → renderers-0.1.12.dev7}/tests/test_tool_arg_type_preservation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: renderers
3
- Version: 0.1.12.dev5
3
+ Version: 0.1.12.dev7
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
@@ -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` |
@@ -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
- GLM5RendererConfig,
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.dev5'
22
- __version_tuple__ = version_tuple = (0, 1, 12, 'dev5')
21
+ __version__ = version = '0.1.12.dev7'
22
+ __version_tuple__ = version_tuple = (0, 1, 12, 'dev7')
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.glm5 import GLM5Renderer, GLM51Renderer
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 GLM5RendererConfig, GLM51RendererConfig
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.enable_thinking:
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
- self._render_tool(
266
- messages,
267
- i,
268
- content,
269
- emit_special=emit_special,
270
- emit_text=emit_text,
271
- emit_text_segments=emit_text_segments,
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.enable_thinking:
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.enable_thinking:
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, field as dataclass_field
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
@@ -33,16 +33,16 @@ wheels = [
33
33
 
34
34
  [[package]]
35
35
  name = "anyio"
36
- version = "4.13.0"
36
+ version = "4.14.2"
37
37
  source = { registry = "https://pypi.org/simple" }
38
38
  dependencies = [
39
39
  { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
40
40
  { name = "idna" },
41
41
  { name = "typing-extensions", marker = "python_full_version < '3.13'" },
42
42
  ]
43
- sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" }
43
+ sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" }
44
44
  wheels = [
45
- { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" },
45
+ { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" },
46
46
  ]
47
47
 
48
48
  [[package]]
File without changes