renderers 0.1.13.dev4__tar.gz → 0.1.13.dev5__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 (97) hide show
  1. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/PKG-INFO +2 -2
  2. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/README.md +1 -1
  3. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/docs/renderer-config.md +2 -2
  4. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/renderers/__init__.py +4 -0
  5. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/renderers/_version.py +2 -2
  6. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/renderers/base.py +9 -1
  7. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/renderers/client.py +11 -2
  8. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/renderers/configs.py +9 -0
  9. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/renderers/glm5.py +326 -35
  10. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/tests/parity.py +5 -0
  11. renderers-0.1.13.dev5/tests/test_glm53_flash.py +624 -0
  12. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/tests/test_reasoning_boundaries.py +2 -1
  13. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/.github/workflows/publish-dev.yml +0 -0
  14. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/.github/workflows/publish.yml +0 -0
  15. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/.github/workflows/style.yml +0 -0
  16. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/.github/workflows/test.yml +0 -0
  17. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/.gitignore +0 -0
  18. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/.pre-commit-config.yaml +0 -0
  19. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/LICENSE +0 -0
  20. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/examples/README.md +0 -0
  21. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/examples/sglang/multiturn_generate_sglang.py +0 -0
  22. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/examples/sglang/online_multiturn_sglang.py +0 -0
  23. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/examples/tinker/multiturn_generate_tinker.py +0 -0
  24. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/examples/transformers/multiturn_generate_transformers.py +0 -0
  25. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/examples/vllm/multiturn_generate_vllm.py +0 -0
  26. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/pyproject.toml +0 -0
  27. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/renderers/custom.py +0 -0
  28. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/renderers/deepseek_r1.py +0 -0
  29. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/renderers/deepseek_v3.py +0 -0
  30. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/renderers/deepseek_v4.py +0 -0
  31. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/renderers/default.py +0 -0
  32. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/renderers/gemma4.py +0 -0
  33. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/renderers/glm45.py +0 -0
  34. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/renderers/gpt_oss.py +0 -0
  35. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/renderers/hy3.py +0 -0
  36. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/renderers/inkling.py +0 -0
  37. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/renderers/kimi_k2.py +0 -0
  38. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/renderers/kimi_k25.py +0 -0
  39. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/renderers/laguna_s21.py +0 -0
  40. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/renderers/laguna_xs2.py +0 -0
  41. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/renderers/llama_3.py +0 -0
  42. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/renderers/minimax_m2.py +0 -0
  43. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/renderers/nemotron3.py +0 -0
  44. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/renderers/parsers.py +0 -0
  45. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/renderers/parsing.py +0 -0
  46. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/renderers/prime_qwen3.py +0 -0
  47. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/renderers/qwen3.py +0 -0
  48. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/renderers/qwen35.py +0 -0
  49. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/renderers/qwen36.py +0 -0
  50. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/renderers/qwen38.py +0 -0
  51. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/renderers/qwen3_vl.py +0 -0
  52. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/renderers/reasoning.py +0 -0
  53. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/renderers/tagged.py +0 -0
  54. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/tests/audit_prefix_stability.py +0 -0
  55. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/tests/conftest.py +0 -0
  56. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/tests/fixtures/prefix_stability.json +0 -0
  57. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/tests/reference_continuations.py +0 -0
  58. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/tests/reference_rendering.py +0 -0
  59. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/tests/replay.py +0 -0
  60. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/tests/test_bridge.py +0 -0
  61. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/tests/test_build_helpers.py +0 -0
  62. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/tests/test_client.py +0 -0
  63. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/tests/test_custom.py +0 -0
  64. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/tests/test_deepseek_r1.py +0 -0
  65. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/tests/test_deepseek_v4.py +0 -0
  66. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/tests/test_disabled_thinking_stability.py +0 -0
  67. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/tests/test_gemma4.py +0 -0
  68. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/tests/test_glm_tool_name_validation.py +0 -0
  69. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/tests/test_hy3.py +0 -0
  70. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/tests/test_incremental.py +0 -0
  71. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/tests/test_inkling.py +0 -0
  72. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/tests/test_is_content.py +0 -0
  73. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/tests/test_kimi_k25_tool_schema.py +0 -0
  74. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/tests/test_laguna_m1.py +0 -0
  75. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/tests/test_laguna_xs21.py +0 -0
  76. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/tests/test_llama_3.py +0 -0
  77. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/tests/test_load_tokenizer.py +0 -0
  78. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/tests/test_message_indices.py +0 -0
  79. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/tests/test_message_tool_names.py +0 -0
  80. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/tests/test_multimodal.py +0 -0
  81. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/tests/test_nemotron35_vlm.py +0 -0
  82. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/tests/test_nemotron3_ultra.py +0 -0
  83. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/tests/test_offsetless_tokenizers.py +0 -0
  84. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/tests/test_parity.py +0 -0
  85. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/tests/test_parse_response.py +0 -0
  86. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/tests/test_parse_response_robustness.py +0 -0
  87. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/tests/test_parsers.py +0 -0
  88. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/tests/test_preserve_thinking.py +0 -0
  89. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/tests/test_qwen35_size_coverage.py +0 -0
  90. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/tests/test_qwen38.py +0 -0
  91. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/tests/test_renderer_config.py +0 -0
  92. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/tests/test_roundtrip.py +0 -0
  93. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/tests/test_sampled_mask.py +0 -0
  94. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/tests/test_tagged_rollout.py +0 -0
  95. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/tests/test_tokens_per_message.py +0 -0
  96. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/tests/test_tool_arg_type_preservation.py +0 -0
  97. {renderers-0.1.13.dev4 → renderers-0.1.13.dev5}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: renderers
3
- Version: 0.1.13.dev4
3
+ Version: 0.1.13.dev5
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
@@ -114,7 +114,7 @@ next_prompt_ids = r.bridge_to_next_turn(
114
114
  )
115
115
  ```
116
116
 
117
- 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).
117
+ Hand-coded renderers ship for `qwen3`, `qwen3-vl`, `qwen3.5`, `qwen3.6`, `qwen3.8`, `gemma4`, `glm-5`, `glm-5.1`, `glm-5.3`, `glm-5.3-flash`, `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`, `glm-5.3-flash`, `kimi-k2.5` / `kimi-k2.6`, and the Inkling checkpoints are multimodal (Inkling handles both image **and** audio; `glm-5.3-flash` renders images only with `process_multimodal=False`).
118
118
 
119
119
  ## API
120
120
 
@@ -94,7 +94,7 @@ next_prompt_ids = r.bridge_to_next_turn(
94
94
  )
95
95
  ```
96
96
 
97
- 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).
97
+ Hand-coded renderers ship for `qwen3`, `qwen3-vl`, `qwen3.5`, `qwen3.6`, `qwen3.8`, `gemma4`, `glm-5`, `glm-5.1`, `glm-5.3`, `glm-5.3-flash`, `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`, `glm-5.3-flash`, `kimi-k2.5` / `kimi-k2.6`, and the Inkling checkpoints are multimodal (Inkling handles both image **and** audio; `glm-5.3-flash` renders images only with `process_multimodal=False`).
98
98
 
99
99
  ## API
100
100
 
@@ -34,7 +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
+ | GLM-5.3 / 5.3 Flash | `GLM53RendererConfig` / `GLM53FlashRendererConfig` | `clear_thinking`, `reasoning_effort` | - |
38
38
  | GLM-4.5 | `GLM45RendererConfig` | `enable_thinking` | - |
39
39
  | gpt-oss | `GptOssRendererConfig` | `reasoning_effort`, `conversation_start_date` | `use_system_prompt`, `knowledge_cutoff`, `model_identity`, `auto_drop_analysis` |
40
40
  | Hy3 | `Hy3RendererConfig` | `reasoning_effort`, `preserved_thinking`, `is_training`, `raw_last_assistant`, `fallback_strategy` | - |
@@ -141,7 +141,7 @@ the knobs its template actually exposes:
141
141
  | Qwen3.8 | `preserve_thinking=True -> all`; else `enable_thinking=False -> all`; else `tool_cycle` |
142
142
  | Gemma 4 | `preserve_thinking=True -> all`; else `enable_thinking=False -> all`; else `tool_cycle` |
143
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` |
144
+ | GLM-5.3 / 5.3 Flash | `clear_thinking=False -> all`, else `tool_cycle` |
145
145
  | GLM-4.5 | `enable_thinking=False -> all`, else `tool_cycle` |
146
146
  | gpt-oss | `auto_drop_analysis=False -> all`, else `tool_cycle` |
147
147
  | Hy3 | `preserved_thinking=True -> all`, else `tool_cycle` |
@@ -55,6 +55,7 @@ from renderers.configs import (
55
55
  GLM5RendererConfig,
56
56
  GLM45RendererConfig,
57
57
  GLM51RendererConfig,
58
+ GLM53FlashRendererConfig,
58
59
  GLM53RendererConfig,
59
60
  GptOssRendererConfig,
60
61
  Hy3RendererConfig,
@@ -95,6 +96,7 @@ _LAZY_RENDERERS: dict[str, str] = {
95
96
  "DefaultRenderer": "renderers.default",
96
97
  "GLM45Renderer": "renderers.glm45",
97
98
  "GLM51Renderer": "renderers.glm5",
99
+ "GLM53FlashRenderer": "renderers.glm5",
98
100
  "GLM53Renderer": "renderers.glm5",
99
101
  "GLM5Renderer": "renderers.glm5",
100
102
  "GptOssRenderer": "renderers.gpt_oss",
@@ -155,6 +157,8 @@ __all__ = [
155
157
  "GLM45RendererConfig",
156
158
  "GLM51Renderer",
157
159
  "GLM51RendererConfig",
160
+ "GLM53FlashRenderer",
161
+ "GLM53FlashRendererConfig",
158
162
  "GLM53Renderer",
159
163
  "GLM53RendererConfig",
160
164
  "GLM5Renderer",
@@ -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.13.dev4'
22
- __version_tuple__ = version_tuple = (0, 1, 13, 'dev4')
21
+ __version__ = version = '0.1.13.dev5'
22
+ __version_tuple__ = version_tuple = (0, 1, 13, 'dev5')
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -1012,6 +1012,8 @@ MODEL_RENDERER_MAP: dict[str, str] = {
1012
1012
  "zai-org/GLM-5.1": "glm-5.1",
1013
1013
  "zai-org/GLM-5.3": "glm-5.3",
1014
1014
  "zai-org/GLM-5.3-BF16": "glm-5.3",
1015
+ "zai-org/GLM-5.3-Flash": "glm-5.3-flash",
1016
+ "zai-org/GLM-5.3-Flash-BF16": "glm-5.3-flash",
1015
1017
  # GLM-4.5.
1016
1018
  "THUDM/GLM-4.5-Air": "glm-4.5",
1017
1019
  "zai-org/GLM-4.5-Air": "glm-4.5",
@@ -1353,7 +1355,12 @@ def _populate_registry():
1353
1355
  from renderers.deepseek_v4 import DeepSeekV4Renderer
1354
1356
  from renderers.default import DefaultRenderer
1355
1357
  from renderers.gemma4 import Gemma4Renderer
1356
- from renderers.glm5 import GLM5Renderer, GLM51Renderer, GLM53Renderer
1358
+ from renderers.glm5 import (
1359
+ GLM5Renderer,
1360
+ GLM51Renderer,
1361
+ GLM53FlashRenderer,
1362
+ GLM53Renderer,
1363
+ )
1357
1364
  from renderers.glm45 import GLM45Renderer
1358
1365
  from renderers.gpt_oss import GptOssRenderer
1359
1366
  from renderers.hy3 import Hy3Renderer
@@ -1393,6 +1400,7 @@ def _populate_registry():
1393
1400
  "glm-5": GLM5Renderer,
1394
1401
  "glm-5.1": GLM51Renderer,
1395
1402
  "glm-5.3": GLM53Renderer,
1403
+ "glm-5.3-flash": GLM53FlashRenderer,
1396
1404
  "glm-4.5": GLM45Renderer,
1397
1405
  "minimax-m2": MiniMaxM2Renderer,
1398
1406
  "deepseek-v3": DeepSeekV3Renderer,
@@ -336,7 +336,9 @@ async def generate(
336
336
  "token_ids": prompt_ids,
337
337
  "sampling_params": sp,
338
338
  }
339
- content_parts = _content_parts(messages) if not process_multimodal else None
339
+ content_parts = (
340
+ _content_parts(messages, renderer=renderer) if not process_multimodal else None
341
+ )
340
342
  features = (
341
343
  _build_mm_features(renderer, mm_data)
342
344
  if process_multimodal and mm_data and not mm_data.is_empty()
@@ -454,8 +456,15 @@ _MEDIA_URL_TYPES = {
454
456
  }
455
457
 
456
458
 
457
- def _content_parts(messages: list[Message]) -> list[dict[str, Any]]:
459
+ def _content_parts(
460
+ messages: list[Message], *, renderer: Renderer
461
+ ) -> list[dict[str, Any]]:
458
462
  """Flatten raw media in prompt order for vLLM's token generate endpoint."""
463
+ # Some templates reorder tool results. Use the same order for raw media,
464
+ # including when the caller supplies a prebuilt prompt and skips rendering.
465
+ order_messages = getattr(renderer, "messages_in_prompt_order", None)
466
+ if order_messages is not None:
467
+ messages = order_messages(messages)
459
468
  parts: list[dict[str, Any]] = []
460
469
  for message in messages:
461
470
  content = message.get("content")
@@ -434,6 +434,12 @@ class GLM53RendererConfig(BaseRendererConfig):
434
434
  return self
435
435
 
436
436
 
437
+ class GLM53FlashRendererConfig(GLM53RendererConfig):
438
+ """GLM-5.3 Flash renderer config. GLM-5.3's template surface, plus images."""
439
+
440
+ name: Literal["glm-5.3-flash"] = "glm-5.3-flash"
441
+
442
+
437
443
  class GLM45RendererConfig(BaseRendererConfig):
438
444
  """GLM-4.5 Air renderer config."""
439
445
 
@@ -1042,6 +1048,7 @@ RendererConfig = Annotated[
1042
1048
  GLM5RendererConfig,
1043
1049
  GLM51RendererConfig,
1044
1050
  GLM53RendererConfig,
1051
+ GLM53FlashRendererConfig,
1045
1052
  GLM45RendererConfig,
1046
1053
  GptOssRendererConfig,
1047
1054
  Hy3RendererConfig,
@@ -1091,6 +1098,7 @@ _CONFIG_BY_NAME: dict[str, type[BaseRendererConfig]] = {
1091
1098
  "glm-5": GLM5RendererConfig,
1092
1099
  "glm-5.1": GLM51RendererConfig,
1093
1100
  "glm-5.3": GLM53RendererConfig,
1101
+ "glm-5.3-flash": GLM53FlashRendererConfig,
1094
1102
  "glm-4.5": GLM45RendererConfig,
1095
1103
  "gpt-oss": GptOssRendererConfig,
1096
1104
  "hy3": Hy3RendererConfig,
@@ -1144,6 +1152,7 @@ __all__ = [
1144
1152
  "DeepSeekV4RendererConfig",
1145
1153
  "GLM45RendererConfig",
1146
1154
  "GLM51RendererConfig",
1155
+ "GLM53FlashRendererConfig",
1147
1156
  "GLM53RendererConfig",
1148
1157
  "GLM5RendererConfig",
1149
1158
  "Gemma4RendererConfig",
@@ -12,10 +12,12 @@ Key differences from Qwen family:
12
12
  from __future__ import annotations
13
13
 
14
14
  import json
15
+ from collections.abc import Mapping
15
16
  from typing import Any
16
17
 
17
18
  from renderers.base import (
18
19
  Message,
20
+ MultiModalData,
19
21
  ParsedResponse,
20
22
  RenderedTokens,
21
23
  Tokenizer,
@@ -30,6 +32,7 @@ from renderers.base import (
30
32
  from renderers.configs import (
31
33
  GLM5RendererConfig,
32
34
  GLM51RendererConfig,
35
+ GLM53FlashRendererConfig,
33
36
  GLM53RendererConfig,
34
37
  )
35
38
  from renderers.parsing import parse_glm
@@ -52,6 +55,24 @@ _TOOLS_FOOTER = (
52
55
  )
53
56
 
54
57
 
58
+ def _is_list_of_outputs(content: Any) -> bool:
59
+ return (
60
+ isinstance(content, list)
61
+ and bool(content)
62
+ and isinstance(content[0], dict)
63
+ and "output" in content[0]
64
+ )
65
+
66
+
67
+ def _tool_contents(content: Any) -> list[Any]:
68
+ if _is_list_of_outputs(content):
69
+ return [
70
+ entry.get("output") if isinstance(entry, dict) else None
71
+ for entry in content
72
+ ]
73
+ return [content]
74
+
75
+
55
76
  class GLM5Renderer:
56
77
  """Deterministic message → token renderer for GLM-5 models."""
57
78
 
@@ -138,6 +159,26 @@ class GLM5Renderer:
138
159
  return "".join(parts)
139
160
  return str(content)
140
161
 
162
+ def _emit_body(
163
+ self, content: Any, msg_idx: int, *, emit_special, emit_text
164
+ ) -> None:
165
+ """Emit a system, user or tool message body. Renderers for models that
166
+ read images in the conversation override this to place the media."""
167
+ emit_text(
168
+ self._visible_text(content), msg_idx, is_sampled=False, is_content=True
169
+ )
170
+
171
+ def _emit_tool_body(
172
+ self, content: Any, msg_idx: int, *, tools, emit_special, emit_text
173
+ ) -> None:
174
+ self._emit_body(
175
+ content, msg_idx, emit_special=emit_special, emit_text=emit_text
176
+ )
177
+
178
+ @staticmethod
179
+ def _prompt_tools(tools: list[ToolSpec]) -> list[ToolSpec]:
180
+ return tools
181
+
141
182
  @staticmethod
142
183
  def _last_user_index(messages: list[Message]) -> int:
143
184
  for i in range(len(messages) - 1, -1, -1):
@@ -218,7 +259,7 @@ class GLM5Renderer:
218
259
  if tools:
219
260
  emit_special(self._system, -1, is_sampled=False, is_content=False)
220
261
  tool_text = _TOOLS_HEADER
221
- for tool in tools:
262
+ for tool in self._prompt_tools(tools):
222
263
  tool_text += self._format_tool_spec(tool) + "\n"
223
264
  tool_text += _TOOLS_FOOTER
224
265
  emit_text(tool_text, -1, is_sampled=False, is_content=False)
@@ -237,7 +278,12 @@ class GLM5Renderer:
237
278
 
238
279
  if role == "system":
239
280
  emit_special(self._system, i, is_sampled=False, is_content=False)
240
- emit_text(content, i, is_sampled=False, is_content=True)
281
+ self._emit_body(
282
+ msg.get("content"),
283
+ i,
284
+ emit_special=emit_special,
285
+ emit_text=emit_text,
286
+ )
241
287
 
242
288
  elif role == "user":
243
289
  emit_special(
@@ -246,7 +292,12 @@ class GLM5Renderer:
246
292
  is_sampled=closes_assistant_turn,
247
293
  is_content=closes_assistant_turn,
248
294
  )
249
- emit_text(content, i, is_sampled=False, is_content=True)
295
+ self._emit_body(
296
+ msg.get("content"),
297
+ i,
298
+ emit_special=emit_special,
299
+ emit_text=emit_text,
300
+ )
250
301
 
251
302
  elif role == "assistant":
252
303
  self._render_assistant(
@@ -261,13 +312,13 @@ class GLM5Renderer:
261
312
 
262
313
  elif role == "tool":
263
314
  if i == 0 or messages[i - 1]["role"] != "tool":
264
- for position, tool_idx in enumerate(
265
- self._ordered_tool_indices(messages, i)
315
+ for position, (tool_idx, tool_content) in enumerate(
316
+ self._tool_results(messages, i)
266
317
  ):
267
- tool_message = messages[tool_idx]
268
318
  self._render_tool(
269
319
  tool_idx,
270
- self._visible_text(tool_message.get("content")),
320
+ tool_content,
321
+ tools=tools,
271
322
  emit_special=emit_special,
272
323
  emit_text=emit_text,
273
324
  emit_text_segments=emit_text_segments,
@@ -316,6 +367,14 @@ class GLM5Renderer:
316
367
  block_end += 1
317
368
  return list(range(block_start, block_end + 1))
318
369
 
370
+ def _tool_results(
371
+ self, messages: list[Message], block_start: int
372
+ ) -> list[tuple[int, Any]]:
373
+ return [
374
+ (i, messages[i].get("content"))
375
+ for i in self._ordered_tool_indices(messages, block_start)
376
+ ]
377
+
319
378
  def render_ids(
320
379
  self,
321
380
  messages: list[Message],
@@ -469,15 +528,24 @@ class GLM5Renderer:
469
528
  # *actually* produced this step.
470
529
  for i, msg in enumerate(new_messages):
471
530
  role = msg.get("role")
472
- content = self._visible_text(msg.get("content"))
473
531
  if role == "user":
474
532
  # Dedup: model already emitted <|user|> as its stop token.
475
533
  if not (i == 0 and last_prev == self._user):
476
534
  emit_special(self._user, i)
477
- emit_text(content, i, is_content=True)
535
+ self._emit_body(
536
+ msg.get("content"),
537
+ i,
538
+ emit_special=emit_special,
539
+ emit_text=emit_text,
540
+ )
478
541
  elif role == "system":
479
542
  emit_special(self._system, i)
480
- emit_text(content, i, is_content=True)
543
+ self._emit_body(
544
+ msg.get("content"),
545
+ i,
546
+ emit_special=emit_special,
547
+ emit_text=emit_text,
548
+ )
481
549
  elif role == "tool":
482
550
  prev_is_tool = i > 0 and new_messages[i - 1].get("role") == "tool"
483
551
  if i == 0 and last_prev == self._observation:
@@ -486,7 +554,13 @@ class GLM5Renderer:
486
554
  elif not prev_is_tool:
487
555
  emit_special(self._observation, i)
488
556
  emit_special(self._tool_response_tok, i)
489
- emit_text(content, i, is_content=True)
557
+ self._emit_tool_body(
558
+ msg.get("content"),
559
+ i,
560
+ tools=tools,
561
+ emit_special=emit_special,
562
+ emit_text=emit_text,
563
+ )
490
564
  emit_special(self._tool_response_end_tok, i)
491
565
  else:
492
566
  return None
@@ -637,8 +711,9 @@ class GLM5Renderer:
637
711
  def _render_tool(
638
712
  self,
639
713
  msg_idx: int,
640
- content: str,
714
+ content: Any,
641
715
  *,
716
+ tools: list[ToolSpec] | None = None,
642
717
  emit_special,
643
718
  emit_text,
644
719
  emit_text_segments,
@@ -664,7 +739,13 @@ class GLM5Renderer:
664
739
  emit_special(
665
740
  self._tool_response_tok, msg_idx, is_sampled=False, is_content=False
666
741
  )
667
- emit_text(content, msg_idx, is_sampled=False, is_content=True)
742
+ self._emit_tool_body(
743
+ content,
744
+ msg_idx,
745
+ tools=tools,
746
+ emit_special=emit_special,
747
+ emit_text=emit_text,
748
+ )
668
749
  emit_special(
669
750
  self._tool_response_end_tok, msg_idx, is_sampled=False, is_content=False
670
751
  )
@@ -727,10 +808,45 @@ class GLM53Renderer(GLM5Renderer):
727
808
  spec = {k: v for k, v in spec.items() if k not in ("defer_loading", "strict")}
728
809
  return json.dumps(spec, ensure_ascii=False)
729
810
 
730
- def _ordered_tool_indices(
811
+ @staticmethod
812
+ def _prompt_tools(tools: list[ToolSpec]) -> list[ToolSpec]:
813
+ return [
814
+ tool
815
+ for tool in tools
816
+ if not (tool.get("function") or tool).get("defer_loading")
817
+ ]
818
+
819
+ def _emit_tool_body(
820
+ self, content: Any, msg_idx: int, *, tools, emit_special, emit_text
821
+ ) -> None:
822
+ if (
823
+ isinstance(content, list)
824
+ and content
825
+ and isinstance(content[0], dict)
826
+ and content[0].get("type") == "tool_reference"
827
+ ):
828
+ if not tools:
829
+ raise ValueError("Tool references require tool specifications")
830
+ text = "<tools>\n"
831
+ for reference in content:
832
+ for tool in tools:
833
+ spec = tool.get("function") or tool
834
+ if spec.get("name") == reference.get("name"):
835
+ text += self._format_tool_spec(tool) + "\n"
836
+ emit_text(text + "</tools>", msg_idx, is_sampled=False, is_content=False)
837
+ else:
838
+ super()._emit_tool_body(
839
+ content,
840
+ msg_idx,
841
+ tools=tools,
842
+ emit_special=emit_special,
843
+ emit_text=emit_text,
844
+ )
845
+
846
+ def _tool_results(
731
847
  self, messages: list[Message], block_start: int
732
- ) -> list[int]:
733
- """Match the template's safe tool-result reordering rule."""
848
+ ) -> list[tuple[int, Any]]:
849
+ """Unpack and order tool results, retaining each input message's owner."""
734
850
  block_end = block_start
735
851
  while (
736
852
  block_end + 1 < len(messages)
@@ -738,31 +854,55 @@ class GLM53Renderer(GLM5Renderer):
738
854
  ):
739
855
  block_end += 1
740
856
 
857
+ def identifier(obj: Mapping[str, Any]) -> str:
858
+ return str(obj.get("tool_call_id") or obj.get("id") or "")
859
+
860
+ results: list[tuple[int, str, Any]] = []
861
+ for i in range(block_start, block_end + 1):
862
+ content = messages[i].get("content")
863
+ if _is_list_of_outputs(content):
864
+ for entry in content:
865
+ entry = entry if isinstance(entry, dict) else {}
866
+ results.append((i, identifier(entry), entry.get("output")))
867
+ else:
868
+ results.append((i, identifier(messages[i]), content))
869
+
741
870
  previous = messages[block_start - 1] if block_start else {}
742
871
  calls = (
743
872
  previous.get("tool_calls") if previous.get("role") == "assistant" else None
744
873
  )
745
- if not calls:
746
- return list(range(block_start, block_end + 1))
874
+ if calls:
875
+ call_ids = [identifier(call) for call in calls]
876
+ result_ids = [result_id for _, result_id, _ in results]
877
+ if (
878
+ all(call_ids)
879
+ and all(result_ids)
880
+ and len(set(call_ids)) == len(call_ids)
881
+ and len(set(result_ids)) == len(result_ids)
882
+ and set(result_ids) <= set(call_ids)
883
+ ):
884
+ by_id = {result_id: (i, content) for i, result_id, content in results}
885
+ return [by_id[call_id] for call_id in call_ids if call_id in by_id]
886
+ return [(i, content) for i, _, content in results]
747
887
 
748
- call_ids = [call.get("id") or call.get("tool_call_id") for call in calls]
749
- result_ids = [
750
- message.get("tool_call_id") or message.get("id")
751
- for message in messages[block_start : block_end + 1]
752
- ]
753
- if (
754
- any(identifier is None for identifier in call_ids + result_ids)
755
- or len(set(call_ids)) != len(call_ids)
756
- or len(set(result_ids)) != len(result_ids)
757
- or set(call_ids) != set(result_ids)
888
+ def bridge_to_next_turn(
889
+ self,
890
+ previous_prompt_ids: list[int],
891
+ previous_completion_ids: list[int],
892
+ new_messages: list[Message],
893
+ *,
894
+ tools: list[ToolSpec] | None = None,
895
+ ) -> RenderedTokens | None:
896
+ tool_messages = [m for m in new_messages if m.get("role") == "tool"]
897
+ if len(tool_messages) > 1 or any(
898
+ _is_list_of_outputs(m.get("content")) for m in tool_messages
758
899
  ):
759
- return list(range(block_start, block_end + 1))
760
-
761
- by_id = {
762
- identifier: index
763
- for identifier, index in zip(result_ids, range(block_start, block_end + 1))
764
- }
765
- return [by_id[identifier] for identifier in call_ids]
900
+ # Sorting needs the original assistant's tool-call IDs. Those IDs
901
+ # cannot be recovered from the raw sampled completion tokens.
902
+ return None
903
+ return super().bridge_to_next_turn(
904
+ previous_prompt_ids, previous_completion_ids, new_messages, tools=tools
905
+ )
766
906
 
767
907
  def render(
768
908
  self,
@@ -854,3 +994,154 @@ class GLM53Renderer(GLM5Renderer):
854
994
  emit_special(
855
995
  self._tool_call_end_tok, msg_idx, is_sampled=True, is_content=True
856
996
  )
997
+
998
+
999
+ _IMAGE_TYPES = ("image", "image_url")
1000
+ _UNSUPPORTED_MEDIA_TYPES = ("video", "video_url", "audio", "audio_url", "input_audio")
1001
+
1002
+
1003
+ class GLM53FlashRenderer(GLM53Renderer):
1004
+ """Deterministic message → token renderer for GLM-5.3 Flash.
1005
+
1006
+ GLM-5.3 Flash shares GLM-5.3's text template and adds images: the template
1007
+ renders each image part of a system, user or tool message as
1008
+ ``<|begin_of_image|><|image|><|end_of_image|>``.
1009
+
1010
+ Renders the logical prompt (``process_multimodal=False``): one ``<|image|>``
1011
+ per image, which the inference engine expands to the image's token count
1012
+ from the raw image. Rendering with expanded placeholders and processed
1013
+ pixels (``process_multimodal=True``) is not implemented yet. Video and
1014
+ audio are not supported.
1015
+ """
1016
+
1017
+ _config_cls = GLM53FlashRendererConfig
1018
+ supports_process_multimodal = True
1019
+
1020
+ def __init__(
1021
+ self,
1022
+ tokenizer: Tokenizer,
1023
+ config: GLM53FlashRendererConfig | None = None,
1024
+ ):
1025
+ super().__init__(tokenizer, config)
1026
+ self._begin_of_image = self._token_id("<|begin_of_image|>")
1027
+ self._image = self._token_id("<|image|>")
1028
+ self._end_of_image = self._token_id("<|end_of_image|>")
1029
+
1030
+ @property
1031
+ def mm_token_type_id_map(self) -> dict[int, int]:
1032
+ return {self._image: 1}
1033
+
1034
+ @staticmethod
1035
+ def _validate_media(messages: list[Message], *, process_multimodal: bool) -> None:
1036
+ for message in messages:
1037
+ contents = (
1038
+ _tool_contents(message.get("content"))
1039
+ if message.get("role") == "tool"
1040
+ else [message.get("content")]
1041
+ )
1042
+ for content in contents:
1043
+ if not isinstance(content, list):
1044
+ continue
1045
+ for item in content:
1046
+ GLM53FlashRenderer._validate_media_part(
1047
+ item,
1048
+ role=message.get("role"),
1049
+ process_multimodal=process_multimodal,
1050
+ )
1051
+
1052
+ @staticmethod
1053
+ def _validate_media_part(
1054
+ item: Any, *, role: str | None, process_multimodal: bool
1055
+ ) -> None:
1056
+ if not isinstance(item, dict):
1057
+ return
1058
+ kind = item.get("type")
1059
+ if kind in _UNSUPPORTED_MEDIA_TYPES:
1060
+ raise NotImplementedError(
1061
+ f"GLM53FlashRenderer does not support {kind!r} content"
1062
+ )
1063
+ if kind in _IMAGE_TYPES:
1064
+ if role not in ("system", "user", "tool"):
1065
+ raise NotImplementedError(
1066
+ f"GLM53FlashRenderer does not support images in {role!r} messages"
1067
+ )
1068
+ if process_multimodal:
1069
+ raise NotImplementedError(
1070
+ "GLM53FlashRenderer renders images only with "
1071
+ "process_multimodal=False (one placeholder per image)"
1072
+ )
1073
+
1074
+ def messages_in_prompt_order(self, messages: list[Message]) -> list[Message]:
1075
+ """Order raw-media inputs like the full render's tool-result blocks."""
1076
+ self._validate_media(messages, process_multimodal=False)
1077
+ ordered: list[Message] = []
1078
+ for i, message in enumerate(messages):
1079
+ if message.get("role") != "tool":
1080
+ ordered.append(message)
1081
+ elif i == 0 or messages[i - 1].get("role") != "tool":
1082
+ for j, content in self._tool_results(messages, i):
1083
+ result = messages[j].copy()
1084
+ result["content"] = content
1085
+ ordered.append(result)
1086
+ return ordered
1087
+
1088
+ def _emit_body(
1089
+ self, content: Any, msg_idx: int, *, emit_special, emit_text
1090
+ ) -> None:
1091
+ if not isinstance(content, list):
1092
+ super()._emit_body(
1093
+ content, msg_idx, emit_special=emit_special, emit_text=emit_text
1094
+ )
1095
+ return
1096
+ # Consecutive text parts are joined before encoding, as the template does.
1097
+ text = ""
1098
+ for item in content:
1099
+ if isinstance(item, str):
1100
+ text += item
1101
+ elif not isinstance(item, dict):
1102
+ continue
1103
+ elif item.get("type") == "text":
1104
+ text += item.get("text", "")
1105
+ elif item.get("type") in _IMAGE_TYPES:
1106
+ emit_text(text, msg_idx, is_sampled=False, is_content=True)
1107
+ text = ""
1108
+ # The placeholder stands for caller-provided image data (body
1109
+ # content); the markers around it are template scaffolding.
1110
+ emit_special(
1111
+ self._begin_of_image, msg_idx, is_sampled=False, is_content=False
1112
+ )
1113
+ emit_special(self._image, msg_idx, is_sampled=False, is_content=True)
1114
+ emit_special(
1115
+ self._end_of_image, msg_idx, is_sampled=False, is_content=False
1116
+ )
1117
+ emit_text(text, msg_idx, is_sampled=False, is_content=True)
1118
+
1119
+ def render(
1120
+ self,
1121
+ messages: list[Message],
1122
+ *,
1123
+ tools: list[ToolSpec] | None = None,
1124
+ add_generation_prompt: bool = False,
1125
+ process_multimodal: bool = True,
1126
+ ) -> RenderedTokens:
1127
+ self._validate_media(messages, process_multimodal=process_multimodal)
1128
+ return super().render(
1129
+ messages, tools=tools, add_generation_prompt=add_generation_prompt
1130
+ )
1131
+
1132
+ def bridge_to_next_turn(
1133
+ self,
1134
+ previous_prompt_ids: list[int],
1135
+ previous_completion_ids: list[int],
1136
+ new_messages: list[Message],
1137
+ *,
1138
+ tools: list[ToolSpec] | None = None,
1139
+ previous_multi_modal_data: MultiModalData | None = None,
1140
+ process_multimodal: bool = True,
1141
+ ) -> RenderedTokens | None:
1142
+ self._validate_media(new_messages, process_multimodal=process_multimodal)
1143
+ # Logical prompts carry no processed image data, so nothing from
1144
+ # ``previous_multi_modal_data`` needs carrying forward.
1145
+ return super().bridge_to_next_turn(
1146
+ previous_prompt_ids, previous_completion_ids, new_messages, tools=tools
1147
+ )