renderers 0.1.12.dev11__tar.gz → 0.1.12.dev12__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.dev11 → renderers-0.1.12.dev12}/PKG-INFO +37 -1
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/README.md +36 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/renderers/__init__.py +10 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/renderers/_version.py +2 -2
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/renderers/base.py +36 -7
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/renderers/configs.py +35 -0
- renderers-0.1.12.dev12/renderers/custom.py +99 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/renderers/deepseek_v4.py +5 -4
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/renderers/glm5.py +12 -10
- renderers-0.1.12.dev12/tests/test_custom.py +194 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/.github/workflows/publish-dev.yml +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/.github/workflows/publish.yml +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/.github/workflows/style.yml +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/.github/workflows/test.yml +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/.gitignore +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/.pre-commit-config.yaml +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/LICENSE +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/docs/renderer-config.md +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/examples/README.md +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/examples/sglang/multiturn_generate_sglang.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/examples/sglang/online_multiturn_sglang.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/examples/tinker/multiturn_generate_tinker.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/examples/transformers/multiturn_generate_transformers.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/examples/vllm/multiturn_generate_vllm.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/pyproject.toml +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/renderers/client.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/renderers/deepseek_r1.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/renderers/deepseek_v3.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/renderers/default.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/renderers/gemma4.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/renderers/glm45.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/renderers/gpt_oss.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/renderers/hy3.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/renderers/inkling.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/renderers/kimi_k2.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/renderers/kimi_k25.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/renderers/laguna_s21.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/renderers/laguna_xs2.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/renderers/llama_3.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/renderers/minimax_m2.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/renderers/nemotron3.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/renderers/parsers.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/renderers/parsing.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/renderers/prime_qwen3.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/renderers/qwen3.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/renderers/qwen35.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/renderers/qwen36.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/renderers/qwen38.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/renderers/qwen3_vl.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/renderers/reasoning.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/tests/conftest.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/tests/parity.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/tests/reference_rendering.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/tests/test_bridge.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/tests/test_build_helpers.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/tests/test_client.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/tests/test_deepseek_r1.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/tests/test_deepseek_v4.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/tests/test_disabled_thinking_stability.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/tests/test_gemma4.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/tests/test_glm_tool_name_validation.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/tests/test_hy3.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/tests/test_incremental.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/tests/test_inkling.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/tests/test_is_content.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/tests/test_kimi_k25_tool_schema.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/tests/test_laguna_m1.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/tests/test_laguna_xs21.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/tests/test_llama_3.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/tests/test_load_tokenizer.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/tests/test_message_indices.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/tests/test_message_tool_names.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/tests/test_multimodal.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/tests/test_nemotron35_vlm.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/tests/test_nemotron3_ultra.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/tests/test_offsetless_tokenizers.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/tests/test_parity.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/tests/test_parse_response.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/tests/test_parse_response_robustness.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/tests/test_parsers.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/tests/test_preserve_thinking.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/tests/test_qwen35_size_coverage.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/tests/test_qwen38.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/tests/test_reasoning_boundaries.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/tests/test_renderer_config.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/tests/test_roundtrip.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/tests/test_sampled_mask.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/tests/test_tokens_per_message.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/tests/test_tool_arg_type_preservation.py +0 -0
- {renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: renderers
|
|
3
|
-
Version: 0.1.12.
|
|
3
|
+
Version: 0.1.12.dev12
|
|
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
|
|
@@ -221,6 +221,42 @@ One shared behaviour flag lives on typed renderer configs: `thinking_retention`,
|
|
|
221
221
|
|
|
222
222
|
Generic `thinking_retention` does **not** change full `render()` output: a full re-render always follows the Python chat-template implementation. Only real template knobs can change full-render thinking behaviour. GLM-5 `clear_thinking=False`, Nemotron-3 `truncate_history_thinking=False`, Qwen3.6 `preserve_thinking=True`, and GPT-OSS `auto_drop_analysis=False` all imply bridge policy `"all"`; no-thinking generation knobs also imply `"all"` when `thinking_retention` is unset. Setting a direct keep/drop template knob and a contradictory `thinking_retention` raises at config-load. The full per-renderer mapping lives in [`docs/renderer-config.md`](docs/renderer-config.md).
|
|
223
223
|
|
|
224
|
+
## Custom renderers
|
|
225
|
+
|
|
226
|
+
A renderer can live outside this package. Point a `custom` config at the class with `import_path`, as `my_module.Class` or `path/to/file.py:Class`; relative file paths resolve against the working directory. The class sets `config_class` to its own `BaseRendererConfig` subclass, which validates every other field of the config and supplies its `chat_template_kwargs` allowlist. For example, a GLM-5.3 renderer whose generation prompt prefills an empty think block, so the model starts at the answer:
|
|
227
|
+
|
|
228
|
+
```python
|
|
229
|
+
from typing import Literal
|
|
230
|
+
|
|
231
|
+
from renderers.configs import GLM53RendererConfig
|
|
232
|
+
from renderers.glm5 import GLM53Renderer
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class NoThinkingGLM53Config(GLM53RendererConfig):
|
|
236
|
+
name: Literal["glm-5.3-no-thinking"] = "glm-5.3-no-thinking"
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class NoThinkingGLM53Renderer(GLM53Renderer):
|
|
240
|
+
config_class = NoThinkingGLM53Config
|
|
241
|
+
|
|
242
|
+
def _emit_generation_prompt(self, emit_special) -> None:
|
|
243
|
+
emit_special(self._assistant, -1, is_sampled=False, is_content=False)
|
|
244
|
+
emit_special(self._think, -1, is_sampled=False, is_content=False)
|
|
245
|
+
emit_special(self._think_end, -1, is_sampled=False, is_content=False)
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
```python
|
|
249
|
+
from renderers import CustomRendererConfig, create_renderer
|
|
250
|
+
|
|
251
|
+
config = CustomRendererConfig(
|
|
252
|
+
import_path="my_renderers.glm53_no_thinking.NoThinkingGLM53Renderer",
|
|
253
|
+
clear_thinking=True,
|
|
254
|
+
)
|
|
255
|
+
renderer = create_renderer(tokenizer, config)
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Downstream TOML configs select it the same way, with `name = "custom"` and an `import_path`.
|
|
259
|
+
|
|
224
260
|
## `DefaultRenderer`
|
|
225
261
|
|
|
226
262
|
Fallback for unsupported text-only models. Wraps `apply_chat_template` and accepts `tool_parser` / `reasoning_parser` (vLLM convention) plus arbitrary Jinja kwargs via `DefaultRendererConfig`'s `extra="allow"`. Explicit `thinking_retention` is rejected: `bridge_to_next_turn` returns `None` because the template's close is unknown, so multi-turn rollouts fall back to full re-render. Implementing a hand-coded renderer is a few hundred lines of Python (`render_ids` + `parse_response` + `bridge_to_next_turn`) and is the only path that closes the failure modes above by construction.
|
|
@@ -201,6 +201,42 @@ One shared behaviour flag lives on typed renderer configs: `thinking_retention`,
|
|
|
201
201
|
|
|
202
202
|
Generic `thinking_retention` does **not** change full `render()` output: a full re-render always follows the Python chat-template implementation. Only real template knobs can change full-render thinking behaviour. GLM-5 `clear_thinking=False`, Nemotron-3 `truncate_history_thinking=False`, Qwen3.6 `preserve_thinking=True`, and GPT-OSS `auto_drop_analysis=False` all imply bridge policy `"all"`; no-thinking generation knobs also imply `"all"` when `thinking_retention` is unset. Setting a direct keep/drop template knob and a contradictory `thinking_retention` raises at config-load. The full per-renderer mapping lives in [`docs/renderer-config.md`](docs/renderer-config.md).
|
|
203
203
|
|
|
204
|
+
## Custom renderers
|
|
205
|
+
|
|
206
|
+
A renderer can live outside this package. Point a `custom` config at the class with `import_path`, as `my_module.Class` or `path/to/file.py:Class`; relative file paths resolve against the working directory. The class sets `config_class` to its own `BaseRendererConfig` subclass, which validates every other field of the config and supplies its `chat_template_kwargs` allowlist. For example, a GLM-5.3 renderer whose generation prompt prefills an empty think block, so the model starts at the answer:
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
from typing import Literal
|
|
210
|
+
|
|
211
|
+
from renderers.configs import GLM53RendererConfig
|
|
212
|
+
from renderers.glm5 import GLM53Renderer
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class NoThinkingGLM53Config(GLM53RendererConfig):
|
|
216
|
+
name: Literal["glm-5.3-no-thinking"] = "glm-5.3-no-thinking"
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class NoThinkingGLM53Renderer(GLM53Renderer):
|
|
220
|
+
config_class = NoThinkingGLM53Config
|
|
221
|
+
|
|
222
|
+
def _emit_generation_prompt(self, emit_special) -> None:
|
|
223
|
+
emit_special(self._assistant, -1, is_sampled=False, is_content=False)
|
|
224
|
+
emit_special(self._think, -1, is_sampled=False, is_content=False)
|
|
225
|
+
emit_special(self._think_end, -1, is_sampled=False, is_content=False)
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
```python
|
|
229
|
+
from renderers import CustomRendererConfig, create_renderer
|
|
230
|
+
|
|
231
|
+
config = CustomRendererConfig(
|
|
232
|
+
import_path="my_renderers.glm53_no_thinking.NoThinkingGLM53Renderer",
|
|
233
|
+
clear_thinking=True,
|
|
234
|
+
)
|
|
235
|
+
renderer = create_renderer(tokenizer, config)
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Downstream TOML configs select it the same way, with `name = "custom"` and an `import_path`.
|
|
239
|
+
|
|
204
240
|
## `DefaultRenderer`
|
|
205
241
|
|
|
206
242
|
Fallback for unsupported text-only models. Wraps `apply_chat_template` and accepts `tool_parser` / `reasoning_parser` (vLLM convention) plus arbitrary Jinja kwargs via `DefaultRendererConfig`'s `extra="allow"`. Explicit `thinking_retention` is rejected: `bridge_to_next_turn` returns `None` because the template's close is unknown, so multi-turn rollouts fall back to full re-render. Implementing a hand-coded renderer is a few hundred lines of Python (`render_ids` + `parse_response` + `bridge_to_next_turn`) and is the only path that closes the failure modes above by construction.
|
|
@@ -37,13 +37,16 @@ from renderers.base import (
|
|
|
37
37
|
create_renderer,
|
|
38
38
|
extract_message_tool_names,
|
|
39
39
|
is_multimodal,
|
|
40
|
+
merge_chat_template_kwargs,
|
|
40
41
|
reject_assistant_in_extension,
|
|
42
|
+
template_field_names,
|
|
41
43
|
trim_to_turn_close,
|
|
42
44
|
)
|
|
43
45
|
from renderers.client import MalformedGenerateResponseError, OverlongPromptError
|
|
44
46
|
from renderers.configs import (
|
|
45
47
|
AutoRendererConfig,
|
|
46
48
|
BaseRendererConfig,
|
|
49
|
+
CustomRendererConfig,
|
|
47
50
|
DeepSeekR1RendererConfig,
|
|
48
51
|
DeepSeekV3RendererConfig,
|
|
49
52
|
DeepSeekV4RendererConfig,
|
|
@@ -76,6 +79,7 @@ from renderers.configs import (
|
|
|
76
79
|
RendererConfig,
|
|
77
80
|
config_from_name,
|
|
78
81
|
)
|
|
82
|
+
from renderers.custom import custom_renderer_config, load_custom_renderer, load_target
|
|
79
83
|
|
|
80
84
|
# Concrete renderer classes are lazy-loaded so that consumers needing only the
|
|
81
85
|
# config layer (``RendererConfig`` discriminated union) don't import every
|
|
@@ -138,6 +142,7 @@ __all__ = [
|
|
|
138
142
|
"ChatTemplateTokenizer",
|
|
139
143
|
"Content",
|
|
140
144
|
"ContentPart",
|
|
145
|
+
"CustomRendererConfig",
|
|
141
146
|
"DeepSeekR1Renderer",
|
|
142
147
|
"DeepSeekR1RendererConfig",
|
|
143
148
|
"DeepSeekV3Renderer",
|
|
@@ -226,8 +231,13 @@ __all__ = [
|
|
|
226
231
|
"build_trajectory_step",
|
|
227
232
|
"config_from_name",
|
|
228
233
|
"create_renderer",
|
|
234
|
+
"custom_renderer_config",
|
|
229
235
|
"extract_message_tool_names",
|
|
230
236
|
"is_multimodal",
|
|
237
|
+
"load_custom_renderer",
|
|
238
|
+
"load_target",
|
|
239
|
+
"merge_chat_template_kwargs",
|
|
231
240
|
"reject_assistant_in_extension",
|
|
241
|
+
"template_field_names",
|
|
232
242
|
"trim_to_turn_close",
|
|
233
243
|
]
|
|
@@ -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.dev12'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 12, 'dev12')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -1424,6 +1424,12 @@ def create_renderer(
|
|
|
1424
1424
|
config,
|
|
1425
1425
|
chat_template_kwargs=chat_template_kwargs,
|
|
1426
1426
|
)
|
|
1427
|
+
from renderers.configs import CustomRendererConfig
|
|
1428
|
+
from renderers.custom import custom_renderer_config, load_custom_renderer
|
|
1429
|
+
|
|
1430
|
+
if isinstance(config, CustomRendererConfig):
|
|
1431
|
+
renderer_cls = load_custom_renderer(config.import_path)
|
|
1432
|
+
return renderer_cls(tokenizer, custom_renderer_config(config))
|
|
1427
1433
|
cls = RENDERER_REGISTRY.get(config.name)
|
|
1428
1434
|
if cls is None:
|
|
1429
1435
|
raise ValueError(
|
|
@@ -1432,26 +1438,49 @@ def create_renderer(
|
|
|
1432
1438
|
return cls(tokenizer, config)
|
|
1433
1439
|
|
|
1434
1440
|
|
|
1435
|
-
def
|
|
1441
|
+
def template_field_names(config: RendererConfig) -> frozenset[str]:
|
|
1442
|
+
"""Chat-template kwargs that ``config``'s renderer accepts, custom renderers included."""
|
|
1443
|
+
from renderers.configs import CustomRendererConfig
|
|
1444
|
+
from renderers.custom import load_custom_renderer
|
|
1445
|
+
|
|
1446
|
+
if isinstance(config, CustomRendererConfig):
|
|
1447
|
+
return load_custom_renderer(
|
|
1448
|
+
config.import_path
|
|
1449
|
+
).config_class.template_field_names()
|
|
1450
|
+
return type(config).template_field_names()
|
|
1451
|
+
|
|
1452
|
+
|
|
1453
|
+
def merge_chat_template_kwargs(
|
|
1436
1454
|
config: RendererConfig,
|
|
1437
1455
|
chat_template_kwargs: Mapping[str, Any] | None,
|
|
1438
1456
|
) -> RendererConfig:
|
|
1457
|
+
"""Return ``config`` with the template kwargs applied, validated against its allowlist."""
|
|
1439
1458
|
if not chat_template_kwargs:
|
|
1440
1459
|
return config
|
|
1441
1460
|
if not isinstance(chat_template_kwargs, Mapping):
|
|
1442
1461
|
raise TypeError("chat_template_kwargs must be a mapping.")
|
|
1462
|
+
from renderers.configs import CustomRendererConfig
|
|
1463
|
+
from renderers.custom import load_custom_renderer
|
|
1464
|
+
|
|
1443
1465
|
kwargs = dict(chat_template_kwargs)
|
|
1444
1466
|
config_cls = type(config)
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1467
|
+
# A custom config carries its renderer's fields as extras, so the renderer's
|
|
1468
|
+
# own config class decides which kwargs are template controls.
|
|
1469
|
+
fields_cls = (
|
|
1470
|
+
load_custom_renderer(config.import_path).config_class
|
|
1471
|
+
if isinstance(config, CustomRendererConfig)
|
|
1472
|
+
else config_cls
|
|
1473
|
+
)
|
|
1474
|
+
allowed = fields_cls.template_field_names()
|
|
1475
|
+
if fields_cls._allow_opaque_template_kwargs:
|
|
1476
|
+
reserved = frozenset(fields_cls.model_fields) - allowed - {"name"}
|
|
1448
1477
|
unsupported = frozenset(kwargs) & reserved
|
|
1449
1478
|
else:
|
|
1450
1479
|
unsupported = frozenset(kwargs) - allowed
|
|
1451
1480
|
if unsupported:
|
|
1452
1481
|
allowed_text = (
|
|
1453
1482
|
"opaque Jinja kwargs"
|
|
1454
|
-
if
|
|
1483
|
+
if fields_cls._allow_opaque_template_kwargs
|
|
1455
1484
|
else ", ".join(sorted(allowed)) or "(none)"
|
|
1456
1485
|
)
|
|
1457
1486
|
raise ValueError(
|
|
@@ -1486,7 +1515,7 @@ def _resolve_renderer_config(
|
|
|
1486
1515
|
chat_template_kwargs=chat_template_kwargs,
|
|
1487
1516
|
)
|
|
1488
1517
|
|
|
1489
|
-
return
|
|
1518
|
+
return merge_chat_template_kwargs(config, chat_template_kwargs)
|
|
1490
1519
|
|
|
1491
1520
|
|
|
1492
1521
|
def _resolve_auto_config(
|
|
@@ -1514,7 +1543,7 @@ def _resolve_auto_config(
|
|
|
1514
1543
|
|
|
1515
1544
|
if renderer_name is not None:
|
|
1516
1545
|
cfg_cls = _config_class_for(renderer_name)
|
|
1517
|
-
return
|
|
1546
|
+
return merge_chat_template_kwargs(
|
|
1518
1547
|
cfg_cls(**preserve_carry),
|
|
1519
1548
|
chat_template_kwargs,
|
|
1520
1549
|
)
|
|
@@ -994,6 +994,40 @@ class DeepSeekV4RendererConfig(BaseRendererConfig):
|
|
|
994
994
|
return self
|
|
995
995
|
|
|
996
996
|
|
|
997
|
+
class CustomRendererConfig(BaseRendererConfig):
|
|
998
|
+
"""Config for a renderer that lives outside this package.
|
|
999
|
+
|
|
1000
|
+
``import_path`` names the renderer class as ``my_module.Class`` or
|
|
1001
|
+
``path/to/file.py:Class``; relative file paths resolve against the
|
|
1002
|
+
working directory. The class sets ``config_class`` to its own
|
|
1003
|
+
:class:`BaseRendererConfig` subclass, and that class validates every other
|
|
1004
|
+
field of this config. A model-specific renderer can therefore ship without
|
|
1005
|
+
a change to this package::
|
|
1006
|
+
|
|
1007
|
+
[renderer]
|
|
1008
|
+
name = "custom"
|
|
1009
|
+
import_path = "my_renderers.glm53_no_thinking.NoThinkingGLM53Renderer"
|
|
1010
|
+
clear_thinking = true
|
|
1011
|
+
"""
|
|
1012
|
+
|
|
1013
|
+
model_config = ConfigDict(frozen=True, extra="allow")
|
|
1014
|
+
|
|
1015
|
+
name: Literal["custom"] = "custom"
|
|
1016
|
+
import_path: str
|
|
1017
|
+
"""Renderer class as ``my_module.Class`` or ``path/to/file.py:Class``."""
|
|
1018
|
+
|
|
1019
|
+
_internal_fields = frozenset({"import_path"})
|
|
1020
|
+
|
|
1021
|
+
@model_validator(mode="after")
|
|
1022
|
+
def _validate_renderer_fields(self):
|
|
1023
|
+
# Fail at config load, not at the first render: the import path must
|
|
1024
|
+
# resolve and the renderer's config must accept the other fields.
|
|
1025
|
+
from renderers.custom import custom_renderer_config
|
|
1026
|
+
|
|
1027
|
+
custom_renderer_config(self)
|
|
1028
|
+
return self
|
|
1029
|
+
|
|
1030
|
+
|
|
997
1031
|
RendererConfig = Annotated[
|
|
998
1032
|
Union[
|
|
999
1033
|
AutoRendererConfig,
|
|
@@ -1026,6 +1060,7 @@ RendererConfig = Annotated[
|
|
|
1026
1060
|
DeepSeekV3RendererConfig,
|
|
1027
1061
|
DeepSeekR1RendererConfig,
|
|
1028
1062
|
DeepSeekV4RendererConfig,
|
|
1063
|
+
CustomRendererConfig,
|
|
1029
1064
|
],
|
|
1030
1065
|
Field(discriminator="name"),
|
|
1031
1066
|
]
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Load custom renderers from importable modules or local Python files.
|
|
2
|
+
|
|
3
|
+
An import path is ``my_module.Name`` (dotted, like any ``import``) or
|
|
4
|
+
``path/to/file.py:Name``. File paths are resolved against the working
|
|
5
|
+
directory and imported once per process under a stable module name, so every
|
|
6
|
+
caller gets the same class objects.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import hashlib
|
|
12
|
+
import importlib
|
|
13
|
+
import importlib.util
|
|
14
|
+
import sys
|
|
15
|
+
from functools import lru_cache
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from types import ModuleType
|
|
18
|
+
from typing import TYPE_CHECKING, Any
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from renderers.configs import BaseRendererConfig, CustomRendererConfig
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def load_target(import_path: str) -> Any:
|
|
25
|
+
"""Return the object that ``import_path`` names."""
|
|
26
|
+
module_ref, sep, attr = import_path.rpartition(":")
|
|
27
|
+
if not sep and not _is_file_ref(import_path):
|
|
28
|
+
module_ref, sep, attr = import_path.rpartition(".")
|
|
29
|
+
if not sep or not module_ref or not attr:
|
|
30
|
+
raise ValueError(
|
|
31
|
+
f"Import path {import_path!r} must look like 'my_module.Name' or 'path/to/file.py:Name'"
|
|
32
|
+
)
|
|
33
|
+
module = load_module(module_ref)
|
|
34
|
+
try:
|
|
35
|
+
return getattr(module, attr)
|
|
36
|
+
except AttributeError:
|
|
37
|
+
raise ValueError(f"Module {module_ref!r} has no attribute {attr!r}") from None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def load_module(module_ref: str) -> ModuleType:
|
|
41
|
+
"""Import a module by dotted name or by file path."""
|
|
42
|
+
if _is_file_ref(module_ref):
|
|
43
|
+
return _load_file_module(str(Path(module_ref).expanduser().resolve()))
|
|
44
|
+
return importlib.import_module(module_ref)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _is_file_ref(ref: str) -> bool:
|
|
48
|
+
return ref.endswith(".py") or "/" in ref
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@lru_cache(maxsize=None)
|
|
52
|
+
def _load_file_module(path: str) -> ModuleType:
|
|
53
|
+
file = Path(path)
|
|
54
|
+
if not file.is_file():
|
|
55
|
+
raise FileNotFoundError(f"Custom renderer file {path!r} does not exist")
|
|
56
|
+
# A path-derived name keeps two files with the same name apart.
|
|
57
|
+
digest = hashlib.sha1(path.encode()).hexdigest()[:8]
|
|
58
|
+
name = f"renderers_custom_{file.stem}_{digest}"
|
|
59
|
+
if name in sys.modules:
|
|
60
|
+
return sys.modules[name]
|
|
61
|
+
spec = importlib.util.spec_from_file_location(name, file)
|
|
62
|
+
if spec is None or spec.loader is None:
|
|
63
|
+
raise ImportError(f"Cannot import custom renderer file {path!r}")
|
|
64
|
+
module = importlib.util.module_from_spec(spec)
|
|
65
|
+
sys.modules[name] = module
|
|
66
|
+
try:
|
|
67
|
+
spec.loader.exec_module(module)
|
|
68
|
+
except BaseException:
|
|
69
|
+
sys.modules.pop(name, None)
|
|
70
|
+
raise
|
|
71
|
+
return module
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def load_custom_renderer(import_path: str) -> Any:
|
|
75
|
+
"""Return the renderer class that ``import_path`` names.
|
|
76
|
+
|
|
77
|
+
The class must set ``config_class`` to its own
|
|
78
|
+
:class:`~renderers.configs.BaseRendererConfig` subclass.
|
|
79
|
+
"""
|
|
80
|
+
from renderers.configs import BaseRendererConfig
|
|
81
|
+
|
|
82
|
+
renderer_cls = load_target(import_path)
|
|
83
|
+
config_cls = getattr(renderer_cls, "config_class", None)
|
|
84
|
+
if not (
|
|
85
|
+
isinstance(config_cls, type) and issubclass(config_cls, BaseRendererConfig)
|
|
86
|
+
):
|
|
87
|
+
raise TypeError(
|
|
88
|
+
f"Custom renderer {import_path!r} must set config_class to a BaseRendererConfig subclass"
|
|
89
|
+
)
|
|
90
|
+
return renderer_cls
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def custom_renderer_config(config: "CustomRendererConfig") -> "BaseRendererConfig":
|
|
94
|
+
"""The renderer's own typed config, built from the fields next to ``import_path``."""
|
|
95
|
+
config_cls = load_custom_renderer(config.import_path).config_class
|
|
96
|
+
data = dict(config.model_extra or {})
|
|
97
|
+
for field_name in config.model_fields_set - {"name", "import_path"}:
|
|
98
|
+
data[field_name] = getattr(config, field_name)
|
|
99
|
+
return config_cls.model_validate(data)
|
|
@@ -331,6 +331,10 @@ class DeepSeekV4Renderer:
|
|
|
331
331
|
raise ValueError(f"Expected one token for {token!r}, got {ids}")
|
|
332
332
|
return ids[0]
|
|
333
333
|
|
|
334
|
+
def _reasoning_effort_prompt(self) -> str:
|
|
335
|
+
"""Text emitted after BOS in thinking mode; subclasses can replace it."""
|
|
336
|
+
return _REASONING_EFFORT_PROMPTS[self.config.reasoning_effort]
|
|
337
|
+
|
|
334
338
|
@staticmethod
|
|
335
339
|
def _render_tools(tools: list[ToolSpec]) -> str:
|
|
336
340
|
schemas = [_json(dict(_tool_function(tool))) for tool in tools]
|
|
@@ -573,10 +577,7 @@ class DeepSeekV4Renderer:
|
|
|
573
577
|
if add_bos:
|
|
574
578
|
emit_special(self._bos, -1)
|
|
575
579
|
if add_effort_prompt and self.config.enable_thinking:
|
|
576
|
-
emit_text(
|
|
577
|
-
_REASONING_EFFORT_PROMPTS[self.config.reasoning_effort],
|
|
578
|
-
-1,
|
|
579
|
-
)
|
|
580
|
+
emit_text(self._reasoning_effort_prompt(), -1)
|
|
580
581
|
|
|
581
582
|
last_query_index = -1
|
|
582
583
|
for index, message in enumerate(logical_messages):
|
|
@@ -286,11 +286,7 @@ class GLM5Renderer:
|
|
|
286
286
|
# sampling starts — the model continues from these, never emits
|
|
287
287
|
# them. Always is_sampled=False / is_content=False.
|
|
288
288
|
if add_generation_prompt:
|
|
289
|
-
|
|
290
|
-
if getattr(self.config, "enable_thinking", True):
|
|
291
|
-
emit_special(self._think, -1, is_sampled=False, is_content=False)
|
|
292
|
-
else:
|
|
293
|
-
emit_special(self._think_end, -1, is_sampled=False, is_content=False)
|
|
289
|
+
self._emit_generation_prompt(emit_special)
|
|
294
290
|
|
|
295
291
|
return RenderedTokens(
|
|
296
292
|
token_ids=tokens,
|
|
@@ -301,6 +297,16 @@ class GLM5Renderer:
|
|
|
301
297
|
message_tool_names=extract_message_tool_names(messages),
|
|
302
298
|
)
|
|
303
299
|
|
|
300
|
+
def _emit_generation_prompt(self, emit_special) -> None:
|
|
301
|
+
"""Generation prompt: ``<|assistant|><think>`` when thinking is on, or
|
|
302
|
+
``<|assistant|></think>`` when ``enable_thinking=False``. A subclass can
|
|
303
|
+
override this to prefill a different prompt."""
|
|
304
|
+
emit_special(self._assistant, -1, is_sampled=False, is_content=False)
|
|
305
|
+
if getattr(self.config, "enable_thinking", True):
|
|
306
|
+
emit_special(self._think, -1, is_sampled=False, is_content=False)
|
|
307
|
+
else:
|
|
308
|
+
emit_special(self._think_end, -1, is_sampled=False, is_content=False)
|
|
309
|
+
|
|
304
310
|
@staticmethod
|
|
305
311
|
def _ordered_tool_indices(messages: list[Message], block_start: int) -> list[int]:
|
|
306
312
|
"""Return one contiguous tool block in its source order."""
|
|
@@ -499,11 +505,7 @@ class GLM5Renderer:
|
|
|
499
505
|
return None
|
|
500
506
|
|
|
501
507
|
# Generation prompt — match the gen-prompt branch of ``render()``.
|
|
502
|
-
|
|
503
|
-
if getattr(self.config, "enable_thinking", True):
|
|
504
|
-
emit_special(self._think, -1)
|
|
505
|
-
else:
|
|
506
|
-
emit_special(self._think_end, -1)
|
|
508
|
+
self._emit_generation_prompt(emit_special)
|
|
507
509
|
|
|
508
510
|
total_len = len(previous_ids) + len(ext)
|
|
509
511
|
return RenderedTokens(
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""Custom renderers loaded from local files or importable modules."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import pickle
|
|
6
|
+
import textwrap
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
from pydantic import TypeAdapter, ValidationError
|
|
10
|
+
|
|
11
|
+
from renderers import (
|
|
12
|
+
CustomRendererConfig,
|
|
13
|
+
GLM53RendererConfig,
|
|
14
|
+
RendererConfig,
|
|
15
|
+
create_renderer,
|
|
16
|
+
custom_renderer_config,
|
|
17
|
+
load_custom_renderer,
|
|
18
|
+
merge_chat_template_kwargs,
|
|
19
|
+
template_field_names,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
TOY_SOURCE = textwrap.dedent(
|
|
23
|
+
"""
|
|
24
|
+
from typing import Literal
|
|
25
|
+
|
|
26
|
+
from renderers.configs import BaseRendererConfig
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ToyRendererConfig(BaseRendererConfig):
|
|
30
|
+
name: Literal["toy"] = "toy"
|
|
31
|
+
effort: int = 1
|
|
32
|
+
cache_size: int = 8
|
|
33
|
+
_template_fields = frozenset({"effort"})
|
|
34
|
+
_internal_fields = frozenset({"cache_size"})
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ToyRenderer:
|
|
38
|
+
config_class = ToyRendererConfig
|
|
39
|
+
|
|
40
|
+
def __init__(self, tokenizer, config):
|
|
41
|
+
self.tokenizer = tokenizer
|
|
42
|
+
self.config = config
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class NoConfigRenderer:
|
|
46
|
+
pass
|
|
47
|
+
"""
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@pytest.fixture
|
|
52
|
+
def toy_file(tmp_path):
|
|
53
|
+
path = tmp_path / "toy_renderer.py"
|
|
54
|
+
path.write_text(TOY_SOURCE)
|
|
55
|
+
return path
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def import_path(path, name="ToyRenderer"):
|
|
59
|
+
return f"{path}:{name}"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_custom_fields_validate_through_the_renderer_config(toy_file):
|
|
63
|
+
config = CustomRendererConfig(import_path=import_path(toy_file), effort=3)
|
|
64
|
+
assert custom_renderer_config(config).effort == 3
|
|
65
|
+
assert custom_renderer_config(config).name == "toy"
|
|
66
|
+
with pytest.raises(ValidationError):
|
|
67
|
+
CustomRendererConfig(import_path=import_path(toy_file), unknown=1)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_shared_renderer_fields_reach_the_renderer_config(toy_file):
|
|
71
|
+
config = CustomRendererConfig(
|
|
72
|
+
import_path=import_path(toy_file), thinking_retention="all"
|
|
73
|
+
)
|
|
74
|
+
assert custom_renderer_config(config).thinking_retention == "all"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_discriminated_union_parses_renderer_configs(toy_file):
|
|
78
|
+
config = TypeAdapter(RendererConfig).validate_python(
|
|
79
|
+
{"name": "custom", "import_path": import_path(toy_file), "effort": 2}
|
|
80
|
+
)
|
|
81
|
+
assert isinstance(config, CustomRendererConfig)
|
|
82
|
+
assert custom_renderer_config(config).effort == 2
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_create_renderer_builds_the_custom_renderer_with_its_own_config(toy_file):
|
|
86
|
+
tokenizer = object()
|
|
87
|
+
renderer = create_renderer(
|
|
88
|
+
tokenizer, CustomRendererConfig(import_path=import_path(toy_file), effort=4)
|
|
89
|
+
)
|
|
90
|
+
assert type(renderer).__name__ == "ToyRenderer"
|
|
91
|
+
assert renderer.tokenizer is tokenizer
|
|
92
|
+
assert type(renderer.config).__name__ == "ToyRendererConfig"
|
|
93
|
+
assert renderer.config.effort == 4
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_chat_template_kwargs_use_the_custom_renderer_allowlist(toy_file):
|
|
97
|
+
config = CustomRendererConfig(import_path=import_path(toy_file))
|
|
98
|
+
renderer = create_renderer(object(), config, chat_template_kwargs={"effort": 5})
|
|
99
|
+
assert renderer.config.effort == 5
|
|
100
|
+
with pytest.raises(ValueError, match="cache_size"):
|
|
101
|
+
create_renderer(object(), config, chat_template_kwargs={"cache_size": 2})
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_module_import_paths_import_by_name(toy_file, monkeypatch):
|
|
105
|
+
monkeypatch.syspath_prepend(str(toy_file.parent))
|
|
106
|
+
config = CustomRendererConfig(import_path="toy_renderer.ToyRenderer", effort=6)
|
|
107
|
+
assert create_renderer(object(), config).config.effort == 6
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_file_import_paths_load_once_per_process(toy_file):
|
|
111
|
+
assert load_custom_renderer(import_path(toy_file)) is load_custom_renderer(
|
|
112
|
+
import_path(toy_file)
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_invalid_import_paths_fail_clearly(toy_file, tmp_path):
|
|
117
|
+
with pytest.raises(ValueError, match="must look like"):
|
|
118
|
+
CustomRendererConfig(import_path=str(toy_file))
|
|
119
|
+
with pytest.raises(ValueError, match="has no attribute"):
|
|
120
|
+
CustomRendererConfig(import_path=import_path(toy_file, "Missing"))
|
|
121
|
+
with pytest.raises(TypeError, match="config_class"):
|
|
122
|
+
CustomRendererConfig(import_path=import_path(toy_file, "NoConfigRenderer"))
|
|
123
|
+
with pytest.raises(FileNotFoundError):
|
|
124
|
+
CustomRendererConfig(import_path=f"{tmp_path / 'missing.py'}:ToyRenderer")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_renderer_configs_pickle_and_key_caches(toy_file):
|
|
128
|
+
low = CustomRendererConfig(import_path=import_path(toy_file), effort=1)
|
|
129
|
+
high = CustomRendererConfig(import_path=import_path(toy_file), effort=9)
|
|
130
|
+
assert pickle.loads(pickle.dumps(high)) == high
|
|
131
|
+
cache = {low: "low", high: "high"}
|
|
132
|
+
assert (
|
|
133
|
+
cache[CustomRendererConfig(import_path=import_path(toy_file), effort=9)]
|
|
134
|
+
== "high"
|
|
135
|
+
)
|
|
136
|
+
assert low != high
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_public_helpers_resolve_custom_template_fields(toy_file):
|
|
140
|
+
config = CustomRendererConfig(import_path=import_path(toy_file))
|
|
141
|
+
assert template_field_names(config) == frozenset({"effort"})
|
|
142
|
+
merged = merge_chat_template_kwargs(config, {"effort": 7})
|
|
143
|
+
assert isinstance(merged, CustomRendererConfig)
|
|
144
|
+
assert custom_renderer_config(merged).effort == 7
|
|
145
|
+
with pytest.raises(ValueError, match="cache_size"):
|
|
146
|
+
merge_chat_template_kwargs(config, {"cache_size": 1})
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
GLM53_NO_THINKING_SOURCE = textwrap.dedent(
|
|
150
|
+
"""
|
|
151
|
+
from typing import Literal
|
|
152
|
+
|
|
153
|
+
from renderers.configs import GLM53RendererConfig
|
|
154
|
+
from renderers.glm5 import GLM53Renderer
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class NoThinkingGLM53Config(GLM53RendererConfig):
|
|
158
|
+
name: Literal["glm-5.3-no-thinking"] = "glm-5.3-no-thinking"
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class NoThinkingGLM53Renderer(GLM53Renderer):
|
|
162
|
+
config_class = NoThinkingGLM53Config
|
|
163
|
+
|
|
164
|
+
def _emit_generation_prompt(self, emit_special) -> None:
|
|
165
|
+
emit_special(self._assistant, -1, is_sampled=False, is_content=False)
|
|
166
|
+
emit_special(self._think, -1, is_sampled=False, is_content=False)
|
|
167
|
+
emit_special(self._think_end, -1, is_sampled=False, is_content=False)
|
|
168
|
+
"""
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def test_readme_example_prefills_the_think_block(tmp_path):
|
|
173
|
+
from renderers.base import load_tokenizer
|
|
174
|
+
|
|
175
|
+
path = tmp_path / "glm53_no_thinking.py"
|
|
176
|
+
path.write_text(GLM53_NO_THINKING_SOURCE)
|
|
177
|
+
tokenizer = load_tokenizer("zai-org/GLM-5.3-BF16")
|
|
178
|
+
messages = [{"role": "user", "content": "What is 2 + 2?"}]
|
|
179
|
+
|
|
180
|
+
custom = create_renderer(
|
|
181
|
+
tokenizer,
|
|
182
|
+
CustomRendererConfig(
|
|
183
|
+
import_path=import_path(path, "NoThinkingGLM53Renderer"),
|
|
184
|
+
clear_thinking=True,
|
|
185
|
+
),
|
|
186
|
+
)
|
|
187
|
+
builtin = create_renderer(tokenizer, GLM53RendererConfig(clear_thinking=True))
|
|
188
|
+
|
|
189
|
+
ids = custom.render_ids(messages, add_generation_prompt=True)
|
|
190
|
+
builtin_ids = builtin.render_ids(messages, add_generation_prompt=True)
|
|
191
|
+
assert tokenizer.decode(ids[-3:]) == "<|assistant|><think></think>"
|
|
192
|
+
assert tokenizer.decode(builtin_ids[-2:]) == "<|assistant|><think>"
|
|
193
|
+
assert ids[:-1] == builtin_ids
|
|
194
|
+
assert custom.config.clear_thinking is True
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/examples/sglang/multiturn_generate_sglang.py
RENAMED
|
File without changes
|
{renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/examples/sglang/online_multiturn_sglang.py
RENAMED
|
File without changes
|
{renderers-0.1.12.dev11 → renderers-0.1.12.dev12}/examples/tinker/multiturn_generate_tinker.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|