renderers 0.1.12.dev17__tar.gz → 0.1.12.dev19__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.dev17 → renderers-0.1.12.dev19}/.github/workflows/test.yml +1 -1
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/PKG-INFO +57 -2
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/README.md +55 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/examples/sglang/multiturn_generate_sglang.py +1 -1
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/examples/sglang/online_multiturn_sglang.py +1 -1
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/examples/transformers/multiturn_generate_transformers.py +1 -1
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/examples/vllm/multiturn_generate_vllm.py +1 -1
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/pyproject.toml +1 -1
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/renderers/_version.py +2 -2
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/renderers/base.py +73 -42
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/renderers/configs.py +16 -16
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/renderers/deepseek_v3.py +5 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/renderers/deepseek_v4.py +6 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/renderers/default.py +6 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/renderers/gemma4.py +6 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/renderers/glm45.py +6 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/renderers/glm5.py +8 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/renderers/gpt_oss.py +6 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/renderers/hy3.py +8 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/renderers/inkling.py +6 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/renderers/kimi_k2.py +5 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/renderers/kimi_k25.py +6 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/renderers/laguna_xs2.py +5 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/renderers/llama_3.py +5 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/renderers/minimax_m2.py +6 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/renderers/nemotron3.py +6 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/renderers/prime_qwen3.py +5 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/renderers/qwen3.py +6 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/renderers/qwen35.py +6 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/renderers/qwen3_vl.py +5 -0
- renderers-0.1.12.dev19/tests/audit_prefix_stability.py +81 -0
- renderers-0.1.12.dev19/tests/fixtures/prefix_stability.json +792 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/tests/test_parsers.py +10 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/tests/test_renderer_config.py +6 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/uv.lock +5 -288
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/.github/workflows/publish-dev.yml +0 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/.github/workflows/publish.yml +0 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/.github/workflows/style.yml +0 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/.gitignore +0 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/.pre-commit-config.yaml +0 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/LICENSE +0 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/docs/renderer-config.md +0 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/examples/README.md +0 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/examples/tinker/multiturn_generate_tinker.py +0 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/renderers/__init__.py +0 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/renderers/client.py +0 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/renderers/custom.py +0 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/renderers/deepseek_r1.py +0 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/renderers/laguna_s21.py +0 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/renderers/parsers.py +0 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/renderers/parsing.py +0 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/renderers/qwen36.py +0 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/renderers/qwen38.py +0 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/renderers/reasoning.py +0 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/tests/conftest.py +0 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/tests/parity.py +0 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/tests/reference_rendering.py +0 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/tests/test_bridge.py +0 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/tests/test_build_helpers.py +0 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/tests/test_client.py +0 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/tests/test_custom.py +0 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/tests/test_deepseek_r1.py +0 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/tests/test_deepseek_v4.py +0 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/tests/test_disabled_thinking_stability.py +0 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/tests/test_gemma4.py +0 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/tests/test_glm_tool_name_validation.py +0 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/tests/test_hy3.py +0 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/tests/test_incremental.py +0 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/tests/test_inkling.py +0 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/tests/test_is_content.py +0 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/tests/test_kimi_k25_tool_schema.py +0 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/tests/test_laguna_m1.py +0 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/tests/test_laguna_xs21.py +0 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/tests/test_llama_3.py +0 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/tests/test_load_tokenizer.py +0 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/tests/test_message_indices.py +0 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/tests/test_message_tool_names.py +0 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/tests/test_multimodal.py +0 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/tests/test_nemotron35_vlm.py +0 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/tests/test_nemotron3_ultra.py +0 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/tests/test_offsetless_tokenizers.py +0 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/tests/test_parity.py +0 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/tests/test_parse_response.py +0 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/tests/test_parse_response_robustness.py +0 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/tests/test_preserve_thinking.py +0 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/tests/test_qwen35_size_coverage.py +0 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/tests/test_qwen38.py +0 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/tests/test_reasoning_boundaries.py +0 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/tests/test_roundtrip.py +0 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/tests/test_sampled_mask.py +0 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/tests/test_tokens_per_message.py +0 -0
- {renderers-0.1.12.dev17 → renderers-0.1.12.dev19}/tests/test_tool_arg_type_preservation.py +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: renderers
|
|
3
|
-
Version: 0.1.12.
|
|
3
|
+
Version: 0.1.12.dev19
|
|
4
4
|
Summary: Chat template renderers — deterministic message-to-token conversion for LLM training
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
License-File: LICENSE
|
|
7
|
-
Requires-Python: <3.15,>=3.
|
|
7
|
+
Requires-Python: <3.15,>=3.11
|
|
8
8
|
Requires-Dist: jinja2
|
|
9
9
|
Requires-Dist: numpy
|
|
10
10
|
Requires-Dist: openai-harmony>=0.0.4
|
|
@@ -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).
|
|
@@ -13,7 +13,7 @@ description = "Chat template renderers — deterministic message-to-token conver
|
|
|
13
13
|
readme = "README.md"
|
|
14
14
|
license = "Apache-2.0"
|
|
15
15
|
license-files = ["LICENSE"]
|
|
16
|
-
requires-python = ">=3.
|
|
16
|
+
requires-python = ">=3.11,<3.15"
|
|
17
17
|
dependencies = [
|
|
18
18
|
"numpy",
|
|
19
19
|
"openai>=1.108.1",
|
|
@@ -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.dev19'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 12, 'dev19')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -1,15 +1,14 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import enum
|
|
4
1
|
import logging
|
|
5
2
|
from collections.abc import Mapping
|
|
6
3
|
from dataclasses import dataclass, field
|
|
4
|
+
from enum import StrEnum
|
|
7
5
|
from typing import (
|
|
8
6
|
TYPE_CHECKING,
|
|
9
7
|
Any,
|
|
10
8
|
Callable,
|
|
11
9
|
Literal,
|
|
12
10
|
Protocol,
|
|
11
|
+
Required,
|
|
13
12
|
TypedDict,
|
|
14
13
|
cast,
|
|
15
14
|
runtime_checkable,
|
|
@@ -110,8 +109,8 @@ class Message(TypedDict, total=False):
|
|
|
110
109
|
Optional keys mirror the OpenAI chat format for tool calling.
|
|
111
110
|
"""
|
|
112
111
|
|
|
113
|
-
role: str
|
|
114
|
-
content: Content
|
|
112
|
+
role: Required[str]
|
|
113
|
+
content: Required[Content]
|
|
115
114
|
tool_calls: list[ToolCall]
|
|
116
115
|
tool_call_id: str
|
|
117
116
|
name: str
|
|
@@ -562,7 +561,7 @@ class RenderedTokens:
|
|
|
562
561
|
return mask
|
|
563
562
|
|
|
564
563
|
|
|
565
|
-
class ToolCallParseStatus(
|
|
564
|
+
class ToolCallParseStatus(StrEnum):
|
|
566
565
|
"""Per-attempt outcome of parsing a single ``<tool_call>`` block.
|
|
567
566
|
|
|
568
567
|
The renderer parser's job is JSON-syntax → ``dict`` (the parser-level
|
|
@@ -723,6 +722,26 @@ class ChatTemplateTokenizer(Tokenizer, Protocol):
|
|
|
723
722
|
class Renderer(Protocol):
|
|
724
723
|
"""Owns message ↔ token conversion for a specific model family."""
|
|
725
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
|
+
|
|
726
745
|
def render(
|
|
727
746
|
self,
|
|
728
747
|
messages: list[Message],
|
|
@@ -1310,15 +1329,19 @@ def load_tokenizer(model_name_or_path: str):
|
|
|
1310
1329
|
the requested Meta ID so auto-resolution still selects ``Llama3Renderer``.
|
|
1311
1330
|
Requires the ``renderers[transformers]`` extra.
|
|
1312
1331
|
"""
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1332
|
+
try:
|
|
1333
|
+
_require_transformers("Loading a tokenizer")
|
|
1334
|
+
load_name_or_path = _tokenizer_source_for(model_name_or_path)
|
|
1335
|
+
kwargs = _tokenizer_load_kwargs(load_name_or_path)
|
|
1336
|
+
tok = _load_tokenizer_via_auto(load_name_or_path, **kwargs)
|
|
1337
|
+
return _preserve_requested_tokenizer_name(
|
|
1338
|
+
tok,
|
|
1339
|
+
requested_name_or_path=model_name_or_path,
|
|
1340
|
+
loaded_name_or_path=load_name_or_path,
|
|
1341
|
+
)
|
|
1342
|
+
except Exception as exc:
|
|
1343
|
+
exc.add_note(f"While loading tokenizer {model_name_or_path!r}.")
|
|
1344
|
+
raise
|
|
1322
1345
|
|
|
1323
1346
|
|
|
1324
1347
|
def _populate_registry():
|
|
@@ -1393,7 +1416,7 @@ def _populate_registry():
|
|
|
1393
1416
|
|
|
1394
1417
|
def create_renderer(
|
|
1395
1418
|
tokenizer,
|
|
1396
|
-
config: RendererConfig | None = None,
|
|
1419
|
+
config: "RendererConfig | None" = None,
|
|
1397
1420
|
*,
|
|
1398
1421
|
chat_template_kwargs: Mapping[str, Any] | None = None,
|
|
1399
1422
|
) -> Renderer:
|
|
@@ -1425,26 +1448,34 @@ def create_renderer(
|
|
|
1425
1448
|
"""
|
|
1426
1449
|
_populate_registry()
|
|
1427
1450
|
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1451
|
+
try:
|
|
1452
|
+
config = _resolve_renderer_config(
|
|
1453
|
+
tokenizer,
|
|
1454
|
+
config,
|
|
1455
|
+
chat_template_kwargs=chat_template_kwargs,
|
|
1456
|
+
)
|
|
1457
|
+
from renderers.configs import CustomRendererConfig
|
|
1458
|
+
from renderers.custom import custom_renderer_config, load_custom_renderer
|
|
1459
|
+
|
|
1460
|
+
if isinstance(config, CustomRendererConfig):
|
|
1461
|
+
renderer_cls = load_custom_renderer(config.import_path)
|
|
1462
|
+
return renderer_cls(tokenizer, custom_renderer_config(config))
|
|
1463
|
+
cls = RENDERER_REGISTRY.get(config.name)
|
|
1464
|
+
if cls is None:
|
|
1465
|
+
raise ValueError(
|
|
1466
|
+
f"Unknown renderer {config.name!r}. Available: {', '.join(sorted(RENDERER_REGISTRY))}"
|
|
1467
|
+
)
|
|
1468
|
+
return cls(tokenizer, config)
|
|
1469
|
+
except Exception as exc:
|
|
1470
|
+
model_name = getattr(tokenizer, "name_or_path", "<unnamed tokenizer>")
|
|
1471
|
+
renderer_name = config.name if config is not None else "auto"
|
|
1472
|
+
exc.add_note(
|
|
1473
|
+
f"While creating renderer {renderer_name!r} for tokenizer {model_name!r}."
|
|
1443
1474
|
)
|
|
1444
|
-
|
|
1475
|
+
raise
|
|
1445
1476
|
|
|
1446
1477
|
|
|
1447
|
-
def template_field_names(config: RendererConfig) -> frozenset[str]:
|
|
1478
|
+
def template_field_names(config: "RendererConfig") -> frozenset[str]:
|
|
1448
1479
|
"""Chat-template kwargs that ``config``'s renderer accepts, custom renderers included."""
|
|
1449
1480
|
from renderers.configs import CustomRendererConfig
|
|
1450
1481
|
from renderers.custom import load_custom_renderer
|
|
@@ -1457,9 +1488,9 @@ def template_field_names(config: RendererConfig) -> frozenset[str]:
|
|
|
1457
1488
|
|
|
1458
1489
|
|
|
1459
1490
|
def merge_chat_template_kwargs(
|
|
1460
|
-
config: RendererConfig,
|
|
1491
|
+
config: "RendererConfig",
|
|
1461
1492
|
chat_template_kwargs: Mapping[str, Any] | None,
|
|
1462
|
-
) -> RendererConfig:
|
|
1493
|
+
) -> "RendererConfig":
|
|
1463
1494
|
"""Return ``config`` with the template kwargs applied, validated against its allowlist."""
|
|
1464
1495
|
if not chat_template_kwargs:
|
|
1465
1496
|
return config
|
|
@@ -1504,10 +1535,10 @@ def merge_chat_template_kwargs(
|
|
|
1504
1535
|
|
|
1505
1536
|
def _resolve_renderer_config(
|
|
1506
1537
|
tokenizer,
|
|
1507
|
-
config: RendererConfig | None,
|
|
1538
|
+
config: "RendererConfig | None",
|
|
1508
1539
|
*,
|
|
1509
1540
|
chat_template_kwargs: Mapping[str, Any] | None = None,
|
|
1510
|
-
) -> RendererConfig:
|
|
1541
|
+
) -> "RendererConfig":
|
|
1511
1542
|
"""Resolve auto/default config and merge chat-template kwargs."""
|
|
1512
1543
|
from renderers.configs import AutoRendererConfig
|
|
1513
1544
|
|
|
@@ -1526,10 +1557,10 @@ def _resolve_renderer_config(
|
|
|
1526
1557
|
|
|
1527
1558
|
def _resolve_auto_config(
|
|
1528
1559
|
tokenizer,
|
|
1529
|
-
auto: AutoRendererConfig,
|
|
1560
|
+
auto: "AutoRendererConfig",
|
|
1530
1561
|
*,
|
|
1531
1562
|
chat_template_kwargs: Mapping[str, Any] | None = None,
|
|
1532
|
-
) -> RendererConfig:
|
|
1563
|
+
) -> "RendererConfig":
|
|
1533
1564
|
"""Map ``AutoRendererConfig`` → concrete typed config via the
|
|
1534
1565
|
tokenizer's ``name_or_path``.
|
|
1535
1566
|
|
|
@@ -2117,8 +2148,8 @@ def introduces_user_query(
|
|
|
2117
2148
|
|
|
2118
2149
|
def resolve_thinking_retention(
|
|
2119
2150
|
config: Any,
|
|
2120
|
-
implied: ResolvedThinkingRetention,
|
|
2121
|
-
) -> ResolvedThinkingRetention:
|
|
2151
|
+
implied: "ResolvedThinkingRetention",
|
|
2152
|
+
) -> "ResolvedThinkingRetention":
|
|
2122
2153
|
"""Resolve the effective bridge policy for a renderer instance.
|
|
2123
2154
|
|
|
2124
2155
|
``config.thinking_retention is None`` means "derive from template knobs";
|
|
@@ -2132,7 +2163,7 @@ def resolve_thinking_retention(
|
|
|
2132
2163
|
|
|
2133
2164
|
|
|
2134
2165
|
def should_rerender_for_thinking_retention(
|
|
2135
|
-
thinking_retention: ResolvedThinkingRetention,
|
|
2166
|
+
thinking_retention: "ResolvedThinkingRetention",
|
|
2136
2167
|
new_messages: list[Message],
|
|
2137
2168
|
*,
|
|
2138
2169
|
is_user_query: Callable[[Message], bool] = _is_user_message,
|
|
@@ -21,7 +21,7 @@ keys its tokenizer's template will honour, so it can't enumerate them.
|
|
|
21
21
|
|
|
22
22
|
from __future__ import annotations
|
|
23
23
|
|
|
24
|
-
from typing import Annotated, ClassVar, Literal, Union
|
|
24
|
+
from typing import Annotated, ClassVar, Literal, Self, Union
|
|
25
25
|
|
|
26
26
|
from pydantic import ConfigDict, Field, model_validator
|
|
27
27
|
from pydantic_config import BaseConfig
|
|
@@ -159,7 +159,7 @@ class DefaultRendererConfig(BaseRendererConfig):
|
|
|
159
159
|
_allow_opaque_template_kwargs = True
|
|
160
160
|
|
|
161
161
|
@model_validator(mode="after")
|
|
162
|
-
def _reject_legacy_preserve_flags(self):
|
|
162
|
+
def _reject_legacy_preserve_flags(self) -> Self:
|
|
163
163
|
# ``extra="allow"`` would otherwise swallow the removed ``preserve_*``
|
|
164
164
|
# bools into ``model_extra`` and forward them to apply_chat_template,
|
|
165
165
|
# silently dropping the user's intent (DefaultRenderer can't
|
|
@@ -261,7 +261,7 @@ class Qwen36RendererConfig(BaseRendererConfig):
|
|
|
261
261
|
_internal_fields = frozenset({"image_cache_max"})
|
|
262
262
|
|
|
263
263
|
@model_validator(mode="after")
|
|
264
|
-
def _check_thinking_retention(self):
|
|
264
|
+
def _check_thinking_retention(self) -> Self:
|
|
265
265
|
_reject_thinking_retention_conflict(
|
|
266
266
|
self,
|
|
267
267
|
"preserve_thinking",
|
|
@@ -307,7 +307,7 @@ class Qwen38RendererConfig(BaseRendererConfig):
|
|
|
307
307
|
_internal_fields = frozenset({"image_cache_max"})
|
|
308
308
|
|
|
309
309
|
@model_validator(mode="after")
|
|
310
|
-
def _check_thinking_retention(self):
|
|
310
|
+
def _check_thinking_retention(self) -> Self:
|
|
311
311
|
_reject_thinking_retention_conflict(
|
|
312
312
|
self,
|
|
313
313
|
"preserve_thinking",
|
|
@@ -350,7 +350,7 @@ class Gemma4RendererConfig(BaseRendererConfig):
|
|
|
350
350
|
_internal_fields = frozenset({"image_cache_max"})
|
|
351
351
|
|
|
352
352
|
@model_validator(mode="after")
|
|
353
|
-
def _check_thinking_retention(self):
|
|
353
|
+
def _check_thinking_retention(self) -> Self:
|
|
354
354
|
_reject_thinking_retention_conflict(
|
|
355
355
|
self,
|
|
356
356
|
"preserve_thinking",
|
|
@@ -377,7 +377,7 @@ class GLM5RendererConfig(BaseRendererConfig):
|
|
|
377
377
|
to ``"all"``."""
|
|
378
378
|
|
|
379
379
|
@model_validator(mode="after")
|
|
380
|
-
def _check_thinking_retention(self):
|
|
380
|
+
def _check_thinking_retention(self) -> Self:
|
|
381
381
|
_reject_thinking_retention_conflict(
|
|
382
382
|
self,
|
|
383
383
|
"clear_thinking",
|
|
@@ -401,7 +401,7 @@ class GLM51RendererConfig(BaseRendererConfig):
|
|
|
401
401
|
"""See :class:`GLM5RendererConfig.clear_thinking`."""
|
|
402
402
|
|
|
403
403
|
@model_validator(mode="after")
|
|
404
|
-
def _check_thinking_retention(self):
|
|
404
|
+
def _check_thinking_retention(self) -> Self:
|
|
405
405
|
_reject_thinking_retention_conflict(
|
|
406
406
|
self,
|
|
407
407
|
"clear_thinking",
|
|
@@ -424,7 +424,7 @@ class GLM53RendererConfig(BaseRendererConfig):
|
|
|
424
424
|
"""Reasoning-effort system preamble emitted by the canonical template."""
|
|
425
425
|
|
|
426
426
|
@model_validator(mode="after")
|
|
427
|
-
def _check_thinking_retention(self):
|
|
427
|
+
def _check_thinking_retention(self) -> Self:
|
|
428
428
|
_reject_thinking_retention_conflict(
|
|
429
429
|
self,
|
|
430
430
|
"clear_thinking",
|
|
@@ -506,7 +506,7 @@ class Hy3RendererConfig(BaseRendererConfig):
|
|
|
506
506
|
and suppresses the generation prompt (``add_generation_prompt=False``)."""
|
|
507
507
|
|
|
508
508
|
@model_validator(mode="after")
|
|
509
|
-
def _check_thinking_retention(self):
|
|
509
|
+
def _check_thinking_retention(self) -> Self:
|
|
510
510
|
if self.preserved_thinking is not None and self.thinking_retention is not None:
|
|
511
511
|
implied = "all" if self.preserved_thinking else "tool_cycle"
|
|
512
512
|
if self.thinking_retention != implied:
|
|
@@ -566,7 +566,7 @@ class InklingRendererConfig(BaseRendererConfig):
|
|
|
566
566
|
_internal_fields = frozenset({"image_cache_max", "audio_cache_max"})
|
|
567
567
|
|
|
568
568
|
@model_validator(mode="after")
|
|
569
|
-
def _check_reasoning_effort(self):
|
|
569
|
+
def _check_reasoning_effort(self) -> Self:
|
|
570
570
|
eff = self.reasoning_effort
|
|
571
571
|
if isinstance(eff, str):
|
|
572
572
|
if eff.strip() not in INKLING_EFFORT_MAP:
|
|
@@ -631,7 +631,7 @@ class GptOssRendererConfig(BaseRendererConfig):
|
|
|
631
631
|
)
|
|
632
632
|
|
|
633
633
|
@model_validator(mode="after")
|
|
634
|
-
def _check_thinking_retention(self):
|
|
634
|
+
def _check_thinking_retention(self) -> Self:
|
|
635
635
|
_reject_thinking_retention_conflict(
|
|
636
636
|
self,
|
|
637
637
|
"auto_drop_analysis",
|
|
@@ -836,7 +836,7 @@ class Nemotron3RendererConfig(BaseRendererConfig):
|
|
|
836
836
|
policy to ``"all"``."""
|
|
837
837
|
|
|
838
838
|
@model_validator(mode="after")
|
|
839
|
-
def _check_thinking_retention(self):
|
|
839
|
+
def _check_thinking_retention(self) -> Self:
|
|
840
840
|
_reject_thinking_retention_conflict(
|
|
841
841
|
self,
|
|
842
842
|
"truncate_history_thinking",
|
|
@@ -877,7 +877,7 @@ class Nemotron3UltraRendererConfig(BaseRendererConfig):
|
|
|
877
877
|
"""See :class:`Nemotron3RendererConfig.truncate_history_thinking`."""
|
|
878
878
|
|
|
879
879
|
@model_validator(mode="after")
|
|
880
|
-
def _check_thinking_retention(self):
|
|
880
|
+
def _check_thinking_retention(self) -> Self:
|
|
881
881
|
_reject_thinking_retention_conflict(
|
|
882
882
|
self,
|
|
883
883
|
"truncate_history_thinking",
|
|
@@ -913,7 +913,7 @@ class Nemotron35RendererConfig(BaseRendererConfig):
|
|
|
913
913
|
"""See :class:`Nemotron3RendererConfig.truncate_history_thinking`."""
|
|
914
914
|
|
|
915
915
|
@model_validator(mode="after")
|
|
916
|
-
def _check_thinking_retention(self):
|
|
916
|
+
def _check_thinking_retention(self) -> Self:
|
|
917
917
|
_reject_thinking_retention_conflict(
|
|
918
918
|
self,
|
|
919
919
|
"truncate_history_thinking",
|
|
@@ -984,7 +984,7 @@ class DeepSeekV4RendererConfig(BaseRendererConfig):
|
|
|
984
984
|
"""
|
|
985
985
|
|
|
986
986
|
@model_validator(mode="after")
|
|
987
|
-
def _check_thinking_retention(self):
|
|
987
|
+
def _check_thinking_retention(self) -> Self:
|
|
988
988
|
_reject_thinking_retention_conflict(
|
|
989
989
|
self,
|
|
990
990
|
"drop_thinking",
|
|
@@ -1019,7 +1019,7 @@ class CustomRendererConfig(BaseRendererConfig):
|
|
|
1019
1019
|
_internal_fields = frozenset({"import_path"})
|
|
1020
1020
|
|
|
1021
1021
|
@model_validator(mode="after")
|
|
1022
|
-
def _validate_renderer_fields(self):
|
|
1022
|
+
def _validate_renderer_fields(self) -> Self:
|
|
1023
1023
|
# Fail at config load, not at the first render: the import path must
|
|
1024
1024
|
# resolve and the renderer's config must accept the other fields.
|
|
1025
1025
|
from renderers.custom import custom_renderer_config
|
|
@@ -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"
|
|
@@ -294,6 +294,12 @@ def _prepare_messages(messages: list[Message]) -> list[_LogicalMessage]:
|
|
|
294
294
|
class DeepSeekV4Renderer:
|
|
295
295
|
"""Renderer for ``deepseek-ai/DeepSeek-V4-Flash-0731``."""
|
|
296
296
|
|
|
297
|
+
@property
|
|
298
|
+
def is_prefix_stable(self) -> bool:
|
|
299
|
+
"""Whether full renders preserve completed conversation prefixes."""
|
|
300
|
+
# Task transitions and unterminated assistant continuations can rewrite the prefix.
|
|
301
|
+
return False
|
|
302
|
+
|
|
297
303
|
_implied_thinking_retention = "tool_cycle"
|
|
298
304
|
|
|
299
305
|
def __init__(
|
|
@@ -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__(
|
|
@@ -55,6 +55,12 @@ _TOOLS_FOOTER = (
|
|
|
55
55
|
class GLM45Renderer:
|
|
56
56
|
"""Deterministic message → token renderer for GLM-4.5 Air models."""
|
|
57
57
|
|
|
58
|
+
@property
|
|
59
|
+
def is_prefix_stable(self) -> bool:
|
|
60
|
+
"""Whether full renders preserve completed conversation prefixes."""
|
|
61
|
+
# Historical reasoning is stripped regardless of bridge policy.
|
|
62
|
+
return False
|
|
63
|
+
|
|
58
64
|
def __init__(
|
|
59
65
|
self,
|
|
60
66
|
tokenizer: Tokenizer,
|