renderers 0.1.12.dev18__tar.gz → 0.1.12.dev20__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.dev18 → renderers-0.1.12.dev20}/PKG-INFO +56 -1
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/README.md +55 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/renderers/_version.py +2 -2
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/renderers/base.py +23 -2
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/renderers/deepseek_v3.py +5 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/renderers/deepseek_v4.py +19 -18
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/renderers/default.py +6 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/renderers/gemma4.py +6 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/renderers/glm45.py +11 -18
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/renderers/glm5.py +12 -17
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/renderers/gpt_oss.py +6 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/renderers/hy3.py +8 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/renderers/inkling.py +6 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/renderers/kimi_k2.py +5 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/renderers/kimi_k25.py +6 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/renderers/laguna_xs2.py +5 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/renderers/llama_3.py +5 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/renderers/minimax_m2.py +6 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/renderers/nemotron3.py +6 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/renderers/parsing.py +183 -298
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/renderers/prime_qwen3.py +5 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/renderers/qwen3.py +6 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/renderers/qwen35.py +6 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/renderers/qwen3_vl.py +5 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/renderers/reasoning.py +18 -7
- renderers-0.1.12.dev20/renderers/tagged.py +192 -0
- renderers-0.1.12.dev20/tests/audit_prefix_stability.py +81 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/tests/conftest.py +12 -0
- renderers-0.1.12.dev20/tests/fixtures/prefix_stability.json +792 -0
- renderers-0.1.12.dev20/tests/reference_continuations.py +164 -0
- renderers-0.1.12.dev20/tests/replay.py +48 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/tests/test_bridge.py +5 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/tests/test_deepseek_r1.py +6 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/tests/test_deepseek_v4.py +12 -7
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/tests/test_disabled_thinking_stability.py +5 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/tests/test_gemma4.py +5 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/tests/test_glm_tool_name_validation.py +5 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/tests/test_hy3.py +5 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/tests/test_inkling.py +5 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/tests/test_laguna_m1.py +5 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/tests/test_laguna_xs21.py +5 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/tests/test_multimodal.py +5 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/tests/test_parity.py +10 -2
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/tests/test_parse_response.py +11 -3
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/tests/test_parse_response_robustness.py +4 -5
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/tests/test_qwen38.py +5 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/tests/test_reasoning_boundaries.py +26 -8
- renderers-0.1.12.dev20/tests/test_roundtrip.py +211 -0
- renderers-0.1.12.dev20/tests/test_tagged_rollout.py +258 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/tests/test_tool_arg_type_preservation.py +5 -0
- renderers-0.1.12.dev18/tests/test_roundtrip.py +0 -323
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/.github/workflows/publish-dev.yml +0 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/.github/workflows/publish.yml +0 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/.github/workflows/style.yml +0 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/.github/workflows/test.yml +0 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/.gitignore +0 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/.pre-commit-config.yaml +0 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/LICENSE +0 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/docs/renderer-config.md +0 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/examples/README.md +0 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/examples/sglang/multiturn_generate_sglang.py +0 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/examples/sglang/online_multiturn_sglang.py +0 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/examples/tinker/multiturn_generate_tinker.py +0 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/examples/transformers/multiturn_generate_transformers.py +0 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/examples/vllm/multiturn_generate_vllm.py +0 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/pyproject.toml +0 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/renderers/__init__.py +0 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/renderers/client.py +0 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/renderers/configs.py +0 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/renderers/custom.py +0 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/renderers/deepseek_r1.py +0 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/renderers/laguna_s21.py +0 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/renderers/parsers.py +0 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/renderers/qwen36.py +0 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/renderers/qwen38.py +0 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/tests/parity.py +0 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/tests/reference_rendering.py +0 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/tests/test_build_helpers.py +0 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/tests/test_client.py +0 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/tests/test_custom.py +0 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/tests/test_incremental.py +0 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/tests/test_is_content.py +0 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/tests/test_kimi_k25_tool_schema.py +0 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/tests/test_llama_3.py +0 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/tests/test_load_tokenizer.py +0 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/tests/test_message_indices.py +0 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/tests/test_message_tool_names.py +0 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/tests/test_nemotron35_vlm.py +0 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/tests/test_nemotron3_ultra.py +0 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/tests/test_offsetless_tokenizers.py +0 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/tests/test_parsers.py +0 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/tests/test_preserve_thinking.py +0 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/tests/test_qwen35_size_coverage.py +0 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/tests/test_renderer_config.py +0 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/tests/test_sampled_mask.py +0 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/tests/test_tokens_per_message.py +0 -0
- {renderers-0.1.12.dev18 → renderers-0.1.12.dev20}/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.dev20
|
|
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
|
|
@@ -120,6 +120,8 @@ Hand-coded renderers ship for `qwen3`, `qwen3-vl`, `qwen3.5`, `qwen3.6`, `qwen3.
|
|
|
120
120
|
|
|
121
121
|
```python
|
|
122
122
|
class Renderer(Protocol):
|
|
123
|
+
@property
|
|
124
|
+
def is_prefix_stable(self) -> bool: ...
|
|
123
125
|
def render(messages, *, tools=None, add_generation_prompt=False) -> RenderedTokens: ...
|
|
124
126
|
def render_ids(messages, *, tools=None, add_generation_prompt=False) -> list[int]: ...
|
|
125
127
|
def parse_response(token_ids) -> ParsedResponse: ...
|
|
@@ -132,6 +134,35 @@ class Renderer(Protocol):
|
|
|
132
134
|
- `ParsedResponse` is `(content, reasoning_content, tool_calls)`. It scans token ids for special-token boundaries (e.g. id `151657` for `<tool_call>` on Qwen3) — a literal `"<tool_call>"` in user content tokenizes to ordinary text ids and never matches.
|
|
133
135
|
- Round-trip: rendering `[user, assistant(content, reasoning, tool_calls)]`, slicing the assistant completion, and feeding it through `parse_response` returns an equivalent structured message. Tested per-renderer in `tests/test_roundtrip.py`.
|
|
134
136
|
|
|
137
|
+
### Prefix stability
|
|
138
|
+
|
|
139
|
+
`renderer.is_prefix_stable` describes the constructed renderer after config and
|
|
140
|
+
`chat_template_kwargs` resolution. A `True` value guarantees that full renders
|
|
141
|
+
of assistant-terminated conversations stay token prefixes when more
|
|
142
|
+
user/tool/assistant messages are appended, with a fixed system/developer
|
|
143
|
+
preamble, tools, and `add_generation_prompt=False`. `False` means this is not
|
|
144
|
+
guaranteed; opaque `DefaultRenderer` templates report `False`.
|
|
145
|
+
|
|
146
|
+
The flag follows the template's emit rules, independently of the
|
|
147
|
+
`thinking_retention` bridge override. For example, GLM-5 with
|
|
148
|
+
`clear_thinking=False` is stable, while the default strips earlier reasoning.
|
|
149
|
+
Disabling thinking generation alone does not guarantee stability for datasets
|
|
150
|
+
that already contain reasoning. Other causes include GPT-OSS's final-turn
|
|
151
|
+
terminator and Nemotron's moving effort hint. DeepSeek V4 reports `False`
|
|
152
|
+
because its task/continuation message controls can rewrite the prefix; Inkling
|
|
153
|
+
reports `False` because reused tool-call IDs can change earlier tool-response
|
|
154
|
+
names, even though it retains reasoning.
|
|
155
|
+
|
|
156
|
+
SFT consumers can use this flag to warn that a single full-conversation render
|
|
157
|
+
may omit earlier reasoning. `build_training_sample` still returns one sample
|
|
158
|
+
per conversation, without expanding it into one sample per assistant turn.
|
|
159
|
+
The guarantee assumes unchanged message contents and deterministic tokenization
|
|
160
|
+
and media preprocessing. Stability does not guarantee that the template emits
|
|
161
|
+
every input field (some always omit reasoning), or that rendering reproduces sampled tokens. Continue
|
|
162
|
+
using `bridge_to_next_turn` for sampled trajectories. Custom renderers should
|
|
163
|
+
expose the same property; consumers supporting older/custom implementations
|
|
164
|
+
can conservatively use `getattr(renderer, "is_prefix_stable", False)`.
|
|
165
|
+
|
|
135
166
|
### `bridge_to_next_turn` (the core contract)
|
|
136
167
|
|
|
137
168
|
Given `(prev_prompt_ids, prev_completion_ids)` and new environment messages, return ids for the next turn's prompt such that the result starts with `prev_prompt_ids + prev_completion_ids` byte-for-byte and continues with the new messages plus the next assistant opener. If that cannot be proven safe, return `None` and the caller falls back to a full render.
|
|
@@ -276,6 +307,30 @@ uv run pytest
|
|
|
276
307
|
|
|
277
308
|
Round-trip parity (render → parse → original) and token-level parity against each model's independent reference encoder are tested per renderer. Most references use `apply_chat_template`; DeepSeek V4 uses its shipped Python encoder, and GPT-OSS uses Harmony.
|
|
278
309
|
|
|
310
|
+
Prefix-stability audit witnesses live in
|
|
311
|
+
[`tests/fixtures/prefix_stability.json`](tests/fixtures/prefix_stability.json).
|
|
312
|
+
Each case records a renderer/config, a conversation ending in an assistant,
|
|
313
|
+
messages to append, and the reason the original tokens stop being a prefix.
|
|
314
|
+
Shared scenarios keep the corpus small; paired stable settings guard the
|
|
315
|
+
configuration switches. `DefaultRenderer` is treated as unknown, without
|
|
316
|
+
inventing a witness for its opaque template.
|
|
317
|
+
|
|
318
|
+
This audit is opt-in and is not collected by the regular test suite or CI.
|
|
319
|
+
Run it manually when changing renderer emission rules or the stability
|
|
320
|
+
classification:
|
|
321
|
+
|
|
322
|
+
```bash
|
|
323
|
+
uv run pytest tests/audit_prefix_stability.py -q
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
The tests render both inputs and compare the original token IDs with the
|
|
327
|
+
same-length prefix of the extended render. They print the first differing
|
|
328
|
+
tokens and a decoded text diff on failure. These are executable witnesses,
|
|
329
|
+
not snapshots of token IDs that could pass without exercising the renderer.
|
|
330
|
+
Add a named case for each new instability mechanism or configuration branch.
|
|
331
|
+
A witness disproves stability; finite positive controls do not prove stability
|
|
332
|
+
for every possible conversation or third-party tokenizer/processor.
|
|
333
|
+
|
|
279
334
|
## License
|
|
280
335
|
|
|
281
336
|
Licensed under the [Apache License, Version 2.0](LICENSE).
|
|
@@ -100,6 +100,8 @@ Hand-coded renderers ship for `qwen3`, `qwen3-vl`, `qwen3.5`, `qwen3.6`, `qwen3.
|
|
|
100
100
|
|
|
101
101
|
```python
|
|
102
102
|
class Renderer(Protocol):
|
|
103
|
+
@property
|
|
104
|
+
def is_prefix_stable(self) -> bool: ...
|
|
103
105
|
def render(messages, *, tools=None, add_generation_prompt=False) -> RenderedTokens: ...
|
|
104
106
|
def render_ids(messages, *, tools=None, add_generation_prompt=False) -> list[int]: ...
|
|
105
107
|
def parse_response(token_ids) -> ParsedResponse: ...
|
|
@@ -112,6 +114,35 @@ class Renderer(Protocol):
|
|
|
112
114
|
- `ParsedResponse` is `(content, reasoning_content, tool_calls)`. It scans token ids for special-token boundaries (e.g. id `151657` for `<tool_call>` on Qwen3) — a literal `"<tool_call>"` in user content tokenizes to ordinary text ids and never matches.
|
|
113
115
|
- Round-trip: rendering `[user, assistant(content, reasoning, tool_calls)]`, slicing the assistant completion, and feeding it through `parse_response` returns an equivalent structured message. Tested per-renderer in `tests/test_roundtrip.py`.
|
|
114
116
|
|
|
117
|
+
### Prefix stability
|
|
118
|
+
|
|
119
|
+
`renderer.is_prefix_stable` describes the constructed renderer after config and
|
|
120
|
+
`chat_template_kwargs` resolution. A `True` value guarantees that full renders
|
|
121
|
+
of assistant-terminated conversations stay token prefixes when more
|
|
122
|
+
user/tool/assistant messages are appended, with a fixed system/developer
|
|
123
|
+
preamble, tools, and `add_generation_prompt=False`. `False` means this is not
|
|
124
|
+
guaranteed; opaque `DefaultRenderer` templates report `False`.
|
|
125
|
+
|
|
126
|
+
The flag follows the template's emit rules, independently of the
|
|
127
|
+
`thinking_retention` bridge override. For example, GLM-5 with
|
|
128
|
+
`clear_thinking=False` is stable, while the default strips earlier reasoning.
|
|
129
|
+
Disabling thinking generation alone does not guarantee stability for datasets
|
|
130
|
+
that already contain reasoning. Other causes include GPT-OSS's final-turn
|
|
131
|
+
terminator and Nemotron's moving effort hint. DeepSeek V4 reports `False`
|
|
132
|
+
because its task/continuation message controls can rewrite the prefix; Inkling
|
|
133
|
+
reports `False` because reused tool-call IDs can change earlier tool-response
|
|
134
|
+
names, even though it retains reasoning.
|
|
135
|
+
|
|
136
|
+
SFT consumers can use this flag to warn that a single full-conversation render
|
|
137
|
+
may omit earlier reasoning. `build_training_sample` still returns one sample
|
|
138
|
+
per conversation, without expanding it into one sample per assistant turn.
|
|
139
|
+
The guarantee assumes unchanged message contents and deterministic tokenization
|
|
140
|
+
and media preprocessing. Stability does not guarantee that the template emits
|
|
141
|
+
every input field (some always omit reasoning), or that rendering reproduces sampled tokens. Continue
|
|
142
|
+
using `bridge_to_next_turn` for sampled trajectories. Custom renderers should
|
|
143
|
+
expose the same property; consumers supporting older/custom implementations
|
|
144
|
+
can conservatively use `getattr(renderer, "is_prefix_stable", False)`.
|
|
145
|
+
|
|
115
146
|
### `bridge_to_next_turn` (the core contract)
|
|
116
147
|
|
|
117
148
|
Given `(prev_prompt_ids, prev_completion_ids)` and new environment messages, return ids for the next turn's prompt such that the result starts with `prev_prompt_ids + prev_completion_ids` byte-for-byte and continues with the new messages plus the next assistant opener. If that cannot be proven safe, return `None` and the caller falls back to a full render.
|
|
@@ -256,6 +287,30 @@ uv run pytest
|
|
|
256
287
|
|
|
257
288
|
Round-trip parity (render → parse → original) and token-level parity against each model's independent reference encoder are tested per renderer. Most references use `apply_chat_template`; DeepSeek V4 uses its shipped Python encoder, and GPT-OSS uses Harmony.
|
|
258
289
|
|
|
290
|
+
Prefix-stability audit witnesses live in
|
|
291
|
+
[`tests/fixtures/prefix_stability.json`](tests/fixtures/prefix_stability.json).
|
|
292
|
+
Each case records a renderer/config, a conversation ending in an assistant,
|
|
293
|
+
messages to append, and the reason the original tokens stop being a prefix.
|
|
294
|
+
Shared scenarios keep the corpus small; paired stable settings guard the
|
|
295
|
+
configuration switches. `DefaultRenderer` is treated as unknown, without
|
|
296
|
+
inventing a witness for its opaque template.
|
|
297
|
+
|
|
298
|
+
This audit is opt-in and is not collected by the regular test suite or CI.
|
|
299
|
+
Run it manually when changing renderer emission rules or the stability
|
|
300
|
+
classification:
|
|
301
|
+
|
|
302
|
+
```bash
|
|
303
|
+
uv run pytest tests/audit_prefix_stability.py -q
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
The tests render both inputs and compare the original token IDs with the
|
|
307
|
+
same-length prefix of the extended render. They print the first differing
|
|
308
|
+
tokens and a decoded text diff on failure. These are executable witnesses,
|
|
309
|
+
not snapshots of token IDs that could pass without exercising the renderer.
|
|
310
|
+
Add a named case for each new instability mechanism or configuration branch.
|
|
311
|
+
A witness disproves stability; finite positive controls do not prove stability
|
|
312
|
+
for every possible conversation or third-party tokenizer/processor.
|
|
313
|
+
|
|
259
314
|
## License
|
|
260
315
|
|
|
261
316
|
Licensed under the [Apache License, Version 2.0](LICENSE).
|
|
@@ -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.dev20'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 12, 'dev20')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -722,6 +722,26 @@ class ChatTemplateTokenizer(Tokenizer, Protocol):
|
|
|
722
722
|
class Renderer(Protocol):
|
|
723
723
|
"""Owns message ↔ token conversion for a specific model family."""
|
|
724
724
|
|
|
725
|
+
@property
|
|
726
|
+
def is_prefix_stable(self) -> bool:
|
|
727
|
+
"""Whether extending a conversation preserves its rendered token prefix.
|
|
728
|
+
|
|
729
|
+
For valid conversations ending in an assistant message, ``True`` means
|
|
730
|
+
that appending user/tool/assistant messages leaves the existing token
|
|
731
|
+
IDs unchanged when both renders use ``add_generation_prompt=False``.
|
|
732
|
+
The system/developer preamble, tools, and renderer configuration must
|
|
733
|
+
stay fixed. This describes full renders, not sampled completions or
|
|
734
|
+
the ability to bridge them; keep using ``bridge_to_next_turn`` for that.
|
|
735
|
+
|
|
736
|
+
The declaration is conservative over message contents (including
|
|
737
|
+
supplied reasoning even when thinking generation is disabled) and
|
|
738
|
+
tools. ``False`` means stability is not guaranteed, including opaque
|
|
739
|
+
templates. It does not mean every conversation changes. Conversely,
|
|
740
|
+
``True`` does not promise to emit fields the template always ignores.
|
|
741
|
+
Custom renderers should declare this property from their emit rules.
|
|
742
|
+
"""
|
|
743
|
+
...
|
|
744
|
+
|
|
725
745
|
def render(
|
|
726
746
|
self,
|
|
727
747
|
messages: list[Message],
|
|
@@ -762,8 +782,9 @@ class Renderer(Protocol):
|
|
|
762
782
|
Pass the exact ``prompt_ids`` used for sampling whenever available.
|
|
763
783
|
This identifies reasoning prefilled by the prompt, including Gemma's
|
|
764
784
|
post-tool thought channel and partial assistant prefixes. Omitting it,
|
|
765
|
-
passing None, and passing []
|
|
766
|
-
|
|
785
|
+
passing None, and passing [] are equivalent: the format's completion
|
|
786
|
+
grammar applies without prompt context. GLM and DeepSeek V4 also use
|
|
787
|
+
the configured generation mode to determine the initial channel.
|
|
767
788
|
|
|
768
789
|
``tools`` is the same list passed to ``render`` for this turn.
|
|
769
790
|
XML-style formats (Qwen3.5, GLM, MiniMax, Laguna) render argument
|
|
@@ -56,6 +56,11 @@ class DeepSeekV3Renderer:
|
|
|
56
56
|
stored for protocol uniformity.
|
|
57
57
|
"""
|
|
58
58
|
|
|
59
|
+
@property
|
|
60
|
+
def is_prefix_stable(self) -> bool:
|
|
61
|
+
"""Whether full renders preserve completed conversation prefixes."""
|
|
62
|
+
return True
|
|
63
|
+
|
|
59
64
|
#: Default typed config; the R1 subclass overrides this.
|
|
60
65
|
_config_cls: type = DeepSeekV3RendererConfig
|
|
61
66
|
_implied_thinking_retention = "all"
|
|
@@ -22,8 +22,6 @@ import json
|
|
|
22
22
|
from dataclasses import dataclass, field
|
|
23
23
|
from typing import Any, Mapping
|
|
24
24
|
|
|
25
|
-
from renderers.reasoning import scan_reasoning, prompt_ends_in_reasoning
|
|
26
|
-
|
|
27
25
|
from renderers.base import (
|
|
28
26
|
Message,
|
|
29
27
|
ParsedResponse,
|
|
@@ -41,7 +39,7 @@ from renderers.base import (
|
|
|
41
39
|
)
|
|
42
40
|
from renderers.configs import DeepSeekV4RendererConfig
|
|
43
41
|
from renderers.parsing import parse_deepseek_v4
|
|
44
|
-
|
|
42
|
+
from renderers.reasoning import prompt_ends_in_reasoning
|
|
45
43
|
|
|
46
44
|
_BOS = "<|begin▁of▁sentence|>"
|
|
47
45
|
_EOS = "<|end▁of▁sentence|>"
|
|
@@ -294,6 +292,12 @@ def _prepare_messages(messages: list[Message]) -> list[_LogicalMessage]:
|
|
|
294
292
|
class DeepSeekV4Renderer:
|
|
295
293
|
"""Renderer for ``deepseek-ai/DeepSeek-V4-Flash-0731``."""
|
|
296
294
|
|
|
295
|
+
@property
|
|
296
|
+
def is_prefix_stable(self) -> bool:
|
|
297
|
+
"""Whether full renders preserve completed conversation prefixes."""
|
|
298
|
+
# Task transitions and unterminated assistant continuations can rewrite the prefix.
|
|
299
|
+
return False
|
|
300
|
+
|
|
297
301
|
_implied_thinking_retention = "tool_cycle"
|
|
298
302
|
|
|
299
303
|
def __init__(
|
|
@@ -792,15 +796,18 @@ class DeepSeekV4Renderer:
|
|
|
792
796
|
self._tokenizer,
|
|
793
797
|
token_ids,
|
|
794
798
|
stop_ids={self._eos},
|
|
795
|
-
thinking_enabled=
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
799
|
+
thinking_enabled=(
|
|
800
|
+
prompt_ends_in_reasoning(
|
|
801
|
+
self._tokenizer,
|
|
802
|
+
prompt_ids,
|
|
803
|
+
stop_ids=set(self.get_stop_token_ids()),
|
|
804
|
+
assistant_prefix="<|Assistant|>",
|
|
805
|
+
)
|
|
806
|
+
if prompt_ids
|
|
807
|
+
else self.config.enable_thinking
|
|
800
808
|
),
|
|
801
809
|
think_end_id=self._think_end,
|
|
802
810
|
think_start_id=self._think_start,
|
|
803
|
-
dsml_id=self._dsml,
|
|
804
811
|
)
|
|
805
812
|
|
|
806
813
|
def get_stop_token_ids(self) -> list[int]:
|
|
@@ -821,16 +828,10 @@ class DeepSeekV4Renderer:
|
|
|
821
828
|
):
|
|
822
829
|
return None
|
|
823
830
|
|
|
824
|
-
boundary =
|
|
825
|
-
|
|
826
|
-
previous_completion_ids,
|
|
827
|
-
prompt_ids=previous_prompt_ids,
|
|
828
|
-
stop_ids=set(self.get_stop_token_ids()),
|
|
829
|
-
tool_start_id=self._dsml,
|
|
830
|
-
tool_start_closes_reasoning=True,
|
|
831
|
-
assistant_prefix="<|Assistant|>",
|
|
831
|
+
boundary = self.parse_response(
|
|
832
|
+
previous_completion_ids, prompt_ids=previous_prompt_ids, tools=tools
|
|
832
833
|
)
|
|
833
|
-
if boundary.
|
|
834
|
+
if boundary.reasoning_complete is False:
|
|
834
835
|
if any(t in self.get_stop_token_ids() for t in previous_completion_ids):
|
|
835
836
|
return None
|
|
836
837
|
previous_completion_ids = [*previous_completion_ids, self._think_end]
|
|
@@ -97,6 +97,12 @@ class DefaultRenderer:
|
|
|
97
97
|
:class:`renderers.DefaultRendererConfig`).
|
|
98
98
|
"""
|
|
99
99
|
|
|
100
|
+
@property
|
|
101
|
+
def is_prefix_stable(self) -> bool:
|
|
102
|
+
"""Whether full renders preserve completed conversation prefixes."""
|
|
103
|
+
# An opaque Jinja template cannot promise prefix stability.
|
|
104
|
+
return False
|
|
105
|
+
|
|
100
106
|
def __init__(
|
|
101
107
|
self,
|
|
102
108
|
tokenizer: ChatTemplateTokenizer,
|
|
@@ -431,6 +431,12 @@ class _ArgumentParser:
|
|
|
431
431
|
class Gemma4Renderer:
|
|
432
432
|
"""Deterministic renderer for the canonical Gemma 4 instruction models."""
|
|
433
433
|
|
|
434
|
+
@property
|
|
435
|
+
def is_prefix_stable(self) -> bool:
|
|
436
|
+
"""Whether full renders preserve completed conversation prefixes."""
|
|
437
|
+
# preserve_thinking only retains tool-call reasoning, not final answers.
|
|
438
|
+
return False
|
|
439
|
+
|
|
434
440
|
_config_cls = Gemma4RendererConfig
|
|
435
441
|
|
|
436
442
|
def __init__(
|
|
@@ -13,14 +13,12 @@ from __future__ import annotations
|
|
|
13
13
|
import json
|
|
14
14
|
from typing import Any
|
|
15
15
|
|
|
16
|
-
from renderers.reasoning import scan_reasoning, prompt_ends_in_reasoning
|
|
17
|
-
|
|
18
16
|
from renderers.base import (
|
|
19
17
|
Message,
|
|
20
18
|
ParsedResponse,
|
|
21
19
|
RenderedTokens,
|
|
22
|
-
ToolSpec,
|
|
23
20
|
Tokenizer,
|
|
21
|
+
ToolSpec,
|
|
24
22
|
_content_mask_or_empty,
|
|
25
23
|
attribute_text_segments,
|
|
26
24
|
extract_message_tool_names,
|
|
@@ -55,6 +53,12 @@ _TOOLS_FOOTER = (
|
|
|
55
53
|
class GLM45Renderer:
|
|
56
54
|
"""Deterministic message → token renderer for GLM-4.5 Air models."""
|
|
57
55
|
|
|
56
|
+
@property
|
|
57
|
+
def is_prefix_stable(self) -> bool:
|
|
58
|
+
"""Whether full renders preserve completed conversation prefixes."""
|
|
59
|
+
# Historical reasoning is stripped regardless of bridge policy.
|
|
60
|
+
return False
|
|
61
|
+
|
|
58
62
|
def __init__(
|
|
59
63
|
self,
|
|
60
64
|
tokenizer: Tokenizer,
|
|
@@ -290,12 +294,7 @@ class GLM45Renderer:
|
|
|
290
294
|
arg_value_id=self._arg_value,
|
|
291
295
|
arg_value_end_id=self._arg_value_end,
|
|
292
296
|
tools=tools,
|
|
293
|
-
|
|
294
|
-
self._tokenizer,
|
|
295
|
-
prompt_ids,
|
|
296
|
-
stop_ids=set(self.get_stop_token_ids()),
|
|
297
|
-
assistant_prefix="<|assistant|>",
|
|
298
|
-
),
|
|
297
|
+
thinking_enabled=self.config.enable_thinking,
|
|
299
298
|
)
|
|
300
299
|
|
|
301
300
|
def get_stop_token_ids(self) -> list[int]:
|
|
@@ -316,16 +315,10 @@ class GLM45Renderer:
|
|
|
316
315
|
):
|
|
317
316
|
return None
|
|
318
317
|
|
|
319
|
-
boundary =
|
|
320
|
-
|
|
321
|
-
previous_completion_ids,
|
|
322
|
-
prompt_ids=previous_prompt_ids,
|
|
323
|
-
stop_ids=set(self.get_stop_token_ids()),
|
|
324
|
-
tool_start_id=self._tool_call_tok,
|
|
325
|
-
tool_start_closes_reasoning=True,
|
|
326
|
-
assistant_prefix="<|assistant|>",
|
|
318
|
+
boundary = self.parse_response(
|
|
319
|
+
previous_completion_ids, prompt_ids=previous_prompt_ids, tools=tools
|
|
327
320
|
)
|
|
328
|
-
if boundary.
|
|
321
|
+
if boundary.reasoning_complete is False:
|
|
329
322
|
if any(t in self.get_stop_token_ids() for t in previous_completion_ids):
|
|
330
323
|
return None
|
|
331
324
|
previous_completion_ids = [*previous_completion_ids, self._think_end]
|
|
@@ -14,8 +14,6 @@ from __future__ import annotations
|
|
|
14
14
|
import json
|
|
15
15
|
from typing import Any
|
|
16
16
|
|
|
17
|
-
from renderers.reasoning import scan_reasoning, prompt_ends_in_reasoning
|
|
18
|
-
|
|
19
17
|
from renderers.base import (
|
|
20
18
|
Message,
|
|
21
19
|
ParsedResponse,
|
|
@@ -57,6 +55,14 @@ _TOOLS_FOOTER = (
|
|
|
57
55
|
class GLM5Renderer:
|
|
58
56
|
"""Deterministic message → token renderer for GLM-5 models."""
|
|
59
57
|
|
|
58
|
+
@property
|
|
59
|
+
def is_prefix_stable(self) -> bool:
|
|
60
|
+
"""Whether full renders preserve completed conversation prefixes."""
|
|
61
|
+
# GLM-5.1 also changes empty thinking wrappers across user boundaries.
|
|
62
|
+
return not self.config.clear_thinking and not (
|
|
63
|
+
self.empty_think_on_last_assistant and self.config.enable_thinking
|
|
64
|
+
)
|
|
65
|
+
|
|
60
66
|
# GLM-5.1 flips this on: even when the most-recent assistant has no
|
|
61
67
|
# reasoning content, the template wraps it with ``<think></think>``
|
|
62
68
|
# instead of just emitting ``</think>`` as a separator. Subclassed in
|
|
@@ -343,12 +349,7 @@ class GLM5Renderer:
|
|
|
343
349
|
arg_value_id=self._arg_value,
|
|
344
350
|
arg_value_end_id=self._arg_value_end,
|
|
345
351
|
tools=tools,
|
|
346
|
-
|
|
347
|
-
self._tokenizer,
|
|
348
|
-
prompt_ids,
|
|
349
|
-
stop_ids=set(self.get_stop_token_ids()),
|
|
350
|
-
assistant_prefix="<|assistant|>",
|
|
351
|
-
),
|
|
352
|
+
thinking_enabled=getattr(self.config, "enable_thinking", True),
|
|
352
353
|
)
|
|
353
354
|
|
|
354
355
|
def get_stop_token_ids(self) -> list[int]:
|
|
@@ -369,16 +370,10 @@ class GLM5Renderer:
|
|
|
369
370
|
):
|
|
370
371
|
return None
|
|
371
372
|
|
|
372
|
-
boundary =
|
|
373
|
-
|
|
374
|
-
previous_completion_ids,
|
|
375
|
-
prompt_ids=previous_prompt_ids,
|
|
376
|
-
stop_ids=set(self.get_stop_token_ids()),
|
|
377
|
-
tool_start_id=self._tool_call_tok,
|
|
378
|
-
tool_start_closes_reasoning=True,
|
|
379
|
-
assistant_prefix="<|assistant|>",
|
|
373
|
+
boundary = self.parse_response(
|
|
374
|
+
previous_completion_ids, prompt_ids=previous_prompt_ids, tools=tools
|
|
380
375
|
)
|
|
381
|
-
if boundary.
|
|
376
|
+
if boundary.reasoning_complete is False:
|
|
382
377
|
if any(t in self.get_stop_token_ids() for t in previous_completion_ids):
|
|
383
378
|
return None
|
|
384
379
|
previous_completion_ids = [*previous_completion_ids, self._think_end]
|
|
@@ -121,6 +121,12 @@ def _arguments_to_str(arguments: Any) -> str:
|
|
|
121
121
|
class GptOssRenderer:
|
|
122
122
|
"""Deterministic message → token renderer for OpenAI gpt-oss (harmony)."""
|
|
123
123
|
|
|
124
|
+
@property
|
|
125
|
+
def is_prefix_stable(self) -> bool:
|
|
126
|
+
"""Whether full renders preserve completed conversation prefixes."""
|
|
127
|
+
# The final assistant terminator changes even when auto_drop_analysis=False.
|
|
128
|
+
return False
|
|
129
|
+
|
|
124
130
|
def __init__(
|
|
125
131
|
self,
|
|
126
132
|
tokenizer: Tokenizer,
|
|
@@ -76,6 +76,14 @@ _TOOL_RESPONSE_END = f"</tool_response{_HYTK}>"
|
|
|
76
76
|
class Hy3Renderer:
|
|
77
77
|
"""Deterministic message → token renderer for Tencent Hy3 models."""
|
|
78
78
|
|
|
79
|
+
@property
|
|
80
|
+
def is_prefix_stable(self) -> bool:
|
|
81
|
+
"""Whether full renders preserve completed conversation prefixes."""
|
|
82
|
+
# Require retention regardless of whether tools are supplied.
|
|
83
|
+
return not self.config.raw_last_assistant and (
|
|
84
|
+
self.config.is_training or self.config.preserved_thinking is True
|
|
85
|
+
)
|
|
86
|
+
|
|
79
87
|
def __init__(
|
|
80
88
|
self,
|
|
81
89
|
tokenizer: Tokenizer,
|
|
@@ -173,6 +173,12 @@ class InklingRenderer:
|
|
|
173
173
|
the :class:`~renderers.base.MultimodalRenderer` protocol.
|
|
174
174
|
"""
|
|
175
175
|
|
|
176
|
+
@property
|
|
177
|
+
def is_prefix_stable(self) -> bool:
|
|
178
|
+
"""Whether full renders preserve completed conversation prefixes."""
|
|
179
|
+
# Reused tool-call IDs can change names resolved for earlier tool responses.
|
|
180
|
+
return False
|
|
181
|
+
|
|
176
182
|
def __init__(
|
|
177
183
|
self,
|
|
178
184
|
tokenizer: Tokenizer,
|
|
@@ -47,6 +47,11 @@ class KimiK2Renderer:
|
|
|
47
47
|
but have no effect on the byte-level output.
|
|
48
48
|
"""
|
|
49
49
|
|
|
50
|
+
@property
|
|
51
|
+
def is_prefix_stable(self) -> bool:
|
|
52
|
+
"""Whether full renders preserve completed conversation prefixes."""
|
|
53
|
+
return True
|
|
54
|
+
|
|
50
55
|
def __init__(
|
|
51
56
|
self,
|
|
52
57
|
tokenizer: Tokenizer,
|
|
@@ -593,6 +593,12 @@ class KimiK25Renderer:
|
|
|
593
593
|
The tokenizer should be ``moonshotai/Kimi-K2-Instruct`` (same as K2).
|
|
594
594
|
"""
|
|
595
595
|
|
|
596
|
+
@property
|
|
597
|
+
def is_prefix_stable(self) -> bool:
|
|
598
|
+
"""Whether full renders preserve completed conversation prefixes."""
|
|
599
|
+
# Completing a tool cycle strips its earlier reasoning, even with thinking=False.
|
|
600
|
+
return False
|
|
601
|
+
|
|
596
602
|
supports_process_multimodal = True
|
|
597
603
|
|
|
598
604
|
def __init__(
|
|
@@ -120,6 +120,11 @@ _TOOLS_HEADER_XS21 = (
|
|
|
120
120
|
|
|
121
121
|
|
|
122
122
|
class LagunaXS2Renderer:
|
|
123
|
+
@property
|
|
124
|
+
def is_prefix_stable(self) -> bool:
|
|
125
|
+
"""Whether full renders preserve completed conversation prefixes."""
|
|
126
|
+
return True
|
|
127
|
+
|
|
123
128
|
def __init__(
|
|
124
129
|
self,
|
|
125
130
|
tokenizer: Tokenizer,
|
|
@@ -91,6 +91,11 @@ _TOOLS_IN_USER_INTRO = (
|
|
|
91
91
|
class Llama3Renderer:
|
|
92
92
|
"""Deterministic message → token renderer for Llama-3.x Instruct models."""
|
|
93
93
|
|
|
94
|
+
@property
|
|
95
|
+
def is_prefix_stable(self) -> bool:
|
|
96
|
+
"""Whether full renders preserve completed conversation prefixes."""
|
|
97
|
+
return True
|
|
98
|
+
|
|
94
99
|
def __init__(
|
|
95
100
|
self,
|
|
96
101
|
tokenizer: Tokenizer,
|
|
@@ -58,6 +58,12 @@ _TOOLS_INSTRUCTIONS = (
|
|
|
58
58
|
class MiniMaxM2Renderer:
|
|
59
59
|
"""Deterministic message → token renderer for MiniMax M2 / M2.5 models."""
|
|
60
60
|
|
|
61
|
+
@property
|
|
62
|
+
def is_prefix_stable(self) -> bool:
|
|
63
|
+
"""Whether full renders preserve completed conversation prefixes."""
|
|
64
|
+
# Only assistants after the last user retain reasoning.
|
|
65
|
+
return False
|
|
66
|
+
|
|
61
67
|
def __init__(
|
|
62
68
|
self,
|
|
63
69
|
tokenizer: Tokenizer,
|
|
@@ -118,6 +118,12 @@ class Nemotron3Renderer:
|
|
|
118
118
|
their own discriminator and differ only by the class-level hooks here.
|
|
119
119
|
"""
|
|
120
120
|
|
|
121
|
+
@property
|
|
122
|
+
def is_prefix_stable(self) -> bool:
|
|
123
|
+
"""Whether full renders preserve completed conversation prefixes."""
|
|
124
|
+
# An effort hint moves to the newest user, independently of thinking retention.
|
|
125
|
+
return not self.config.truncate_history_thinking and not self._effort_hint
|
|
126
|
+
|
|
121
127
|
# Variant hooks (overridden by ``Nemotron3UltraRenderer``): the default
|
|
122
128
|
# config to build when none is passed, and whether to use Ultra's
|
|
123
129
|
# reasoning-block glue.
|