renderers 0.1.11.dev4__tar.gz → 0.1.12__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.11.dev4 → renderers-0.1.12}/.github/workflows/test.yml +1 -1
- {renderers-0.1.11.dev4 → renderers-0.1.12}/PKG-INFO +139 -9
- {renderers-0.1.11.dev4 → renderers-0.1.12}/README.md +137 -7
- {renderers-0.1.11.dev4 → renderers-0.1.12}/docs/renderer-config.md +4 -2
- {renderers-0.1.11.dev4 → renderers-0.1.12}/examples/sglang/multiturn_generate_sglang.py +7 -3
- {renderers-0.1.11.dev4 → renderers-0.1.12}/examples/sglang/online_multiturn_sglang.py +5 -3
- {renderers-0.1.11.dev4 → renderers-0.1.12}/examples/tinker/multiturn_generate_tinker.py +6 -2
- {renderers-0.1.11.dev4 → renderers-0.1.12}/examples/transformers/multiturn_generate_transformers.py +7 -3
- {renderers-0.1.11.dev4 → renderers-0.1.12}/examples/vllm/multiturn_generate_vllm.py +7 -3
- {renderers-0.1.11.dev4 → renderers-0.1.12}/pyproject.toml +2 -2
- {renderers-0.1.11.dev4 → renderers-0.1.12}/renderers/__init__.py +23 -9
- {renderers-0.1.11.dev4 → renderers-0.1.12}/renderers/_version.py +2 -2
- {renderers-0.1.11.dev4 → renderers-0.1.12}/renderers/base.py +170 -53
- {renderers-0.1.11.dev4 → renderers-0.1.12}/renderers/client.py +176 -19
- {renderers-0.1.11.dev4 → renderers-0.1.12}/renderers/configs.py +76 -16
- renderers-0.1.12/renderers/custom.py +99 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12}/renderers/deepseek_v3.py +31 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12}/renderers/deepseek_v4.py +33 -7
- {renderers-0.1.11.dev4 → renderers-0.1.12}/renderers/default.py +58 -28
- {renderers-0.1.11.dev4 → renderers-0.1.12}/renderers/gemma4.py +84 -21
- {renderers-0.1.11.dev4 → renderers-0.1.12}/renderers/glm45.py +28 -36
- {renderers-0.1.11.dev4 → renderers-0.1.12}/renderers/glm5.py +245 -52
- {renderers-0.1.11.dev4 → renderers-0.1.12}/renderers/gpt_oss.py +67 -3
- {renderers-0.1.11.dev4 → renderers-0.1.12}/renderers/hy3.py +36 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12}/renderers/inkling.py +52 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12}/renderers/kimi_k2.py +32 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12}/renderers/kimi_k25.py +87 -99
- {renderers-0.1.11.dev4 → renderers-0.1.12}/renderers/laguna_xs2.py +49 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12}/renderers/llama_3.py +6 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12}/renderers/minimax_m2.py +29 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12}/renderers/nemotron3.py +280 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12}/renderers/parsers.py +4 -6
- {renderers-0.1.11.dev4 → renderers-0.1.12}/renderers/parsing.py +434 -382
- {renderers-0.1.11.dev4 → renderers-0.1.12}/renderers/prime_qwen3.py +27 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12}/renderers/qwen3.py +45 -7
- {renderers-0.1.11.dev4 → renderers-0.1.12}/renderers/qwen35.py +81 -31
- {renderers-0.1.11.dev4 → renderers-0.1.12}/renderers/qwen38.py +31 -5
- {renderers-0.1.11.dev4 → renderers-0.1.12}/renderers/qwen3_vl.py +72 -26
- renderers-0.1.12/renderers/reasoning.py +244 -0
- renderers-0.1.12/renderers/tagged.py +192 -0
- renderers-0.1.12/tests/audit_prefix_stability.py +81 -0
- renderers-0.1.12/tests/conftest.py +71 -0
- renderers-0.1.12/tests/fixtures/prefix_stability.json +792 -0
- renderers-0.1.12/tests/parity.py +786 -0
- renderers-0.1.12/tests/reference_continuations.py +164 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12}/tests/reference_rendering.py +283 -29
- renderers-0.1.12/tests/replay.py +48 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12}/tests/test_bridge.py +32 -24
- {renderers-0.1.11.dev4 → renderers-0.1.12}/tests/test_build_helpers.py +104 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12}/tests/test_client.py +194 -3
- renderers-0.1.12/tests/test_custom.py +194 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12}/tests/test_deepseek_r1.py +9 -70
- {renderers-0.1.11.dev4 → renderers-0.1.12}/tests/test_deepseek_v4.py +44 -11
- {renderers-0.1.11.dev4 → renderers-0.1.12}/tests/test_disabled_thinking_stability.py +22 -33
- {renderers-0.1.11.dev4 → renderers-0.1.12}/tests/test_gemma4.py +8 -7
- {renderers-0.1.11.dev4 → renderers-0.1.12}/tests/test_glm_tool_name_validation.py +8 -5
- {renderers-0.1.11.dev4 → renderers-0.1.12}/tests/test_hy3.py +9 -1
- {renderers-0.1.11.dev4 → renderers-0.1.12}/tests/test_incremental.py +1 -1
- {renderers-0.1.11.dev4 → renderers-0.1.12}/tests/test_inkling.py +5 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12}/tests/test_laguna_m1.py +6 -79
- {renderers-0.1.11.dev4 → renderers-0.1.12}/tests/test_laguna_xs21.py +41 -4
- {renderers-0.1.11.dev4 → renderers-0.1.12}/tests/test_llama_3.py +4 -147
- {renderers-0.1.11.dev4 → renderers-0.1.12}/tests/test_message_indices.py +64 -1
- {renderers-0.1.11.dev4 → renderers-0.1.12}/tests/test_multimodal.py +36 -6
- renderers-0.1.12/tests/test_nemotron35_vlm.py +322 -0
- renderers-0.1.12/tests/test_parity.py +230 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12}/tests/test_parse_response.py +97 -13
- {renderers-0.1.11.dev4 → renderers-0.1.12}/tests/test_parse_response_robustness.py +18 -19
- {renderers-0.1.11.dev4 → renderers-0.1.12}/tests/test_parsers.py +24 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12}/tests/test_qwen35_size_coverage.py +4 -91
- {renderers-0.1.11.dev4 → renderers-0.1.12}/tests/test_qwen38.py +79 -0
- renderers-0.1.12/tests/test_reasoning_boundaries.py +630 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12}/tests/test_renderer_config.py +13 -1
- renderers-0.1.12/tests/test_roundtrip.py +211 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12}/tests/test_sampled_mask.py +2 -0
- renderers-0.1.12/tests/test_tagged_rollout.py +258 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12}/tests/test_tool_arg_type_preservation.py +7 -10
- {renderers-0.1.11.dev4 → renderers-0.1.12}/uv.lock +269 -292
- renderers-0.1.11.dev4/tests/conftest.py +0 -171
- renderers-0.1.11.dev4/tests/test_gpt_oss_harmony_parity.py +0 -271
- renderers-0.1.11.dev4/tests/test_laguna_s21.py +0 -128
- renderers-0.1.11.dev4/tests/test_nemotron3_parity.py +0 -683
- renderers-0.1.11.dev4/tests/test_prime_qwen3_parity.py +0 -254
- renderers-0.1.11.dev4/tests/test_render_ids.py +0 -402
- renderers-0.1.11.dev4/tests/test_renderer_config_parity.py +0 -602
- renderers-0.1.11.dev4/tests/test_roundtrip.py +0 -361
- {renderers-0.1.11.dev4 → renderers-0.1.12}/.github/workflows/publish-dev.yml +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12}/.github/workflows/publish.yml +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12}/.github/workflows/style.yml +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12}/.gitignore +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12}/.pre-commit-config.yaml +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12}/LICENSE +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12}/examples/README.md +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12}/renderers/deepseek_r1.py +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12}/renderers/laguna_s21.py +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12}/renderers/qwen36.py +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12}/tests/test_is_content.py +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12}/tests/test_kimi_k25_tool_schema.py +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12}/tests/test_load_tokenizer.py +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12}/tests/test_message_tool_names.py +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12}/tests/test_nemotron3_ultra.py +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12}/tests/test_offsetless_tokenizers.py +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12}/tests/test_preserve_thinking.py +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12}/tests/test_tokens_per_message.py +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: renderers
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.12
|
|
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.
|
|
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
|
|
@@ -63,10 +63,47 @@ prompt_ids = r.render_ids(
|
|
|
63
63
|
# Feed prompt_ids to a Token-In, Token-Out endpoint.
|
|
64
64
|
# It returns completion_ids sampled by the model.
|
|
65
65
|
|
|
66
|
-
parsed = r.parse_response(completion_ids)
|
|
66
|
+
parsed = r.parse_response(completion_ids, prompt_ids=prompt_ids)
|
|
67
67
|
# ParsedResponse(content=..., reasoning_content=..., tool_calls=...)
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
+
Pass the exact sampling `prompt_ids` into `parse_response` to identify reasoning
|
|
71
|
+
opened by the generation prompt. Without its closing marker, the sampled text
|
|
72
|
+
stays in `reasoning_content`, with empty `content` and no executable tool calls,
|
|
73
|
+
even if the engine reports `stop`. This applies to every supported reasoning
|
|
74
|
+
format, including Harmony analysis and Gemma's post-tool thought channel.
|
|
75
|
+
Omitting `prompt_ids`, passing `None`, and passing `[]` all parse the completion
|
|
76
|
+
as self-contained: reasoning needs its own initial opener or channel header.
|
|
77
|
+
Generation settings never supply missing parsing context. If the prompt supplied
|
|
78
|
+
the opener, pass that prompt; `generate` does this automatically.
|
|
79
|
+
|
|
80
|
+
Where vLLM 0.26's parser does the same, the format's atomic tool-call opener
|
|
81
|
+
also ends reasoning that has no closing marker: Qwen3 / Qwen3-VL / Qwen3.5+,
|
|
82
|
+
Prime-Qwen3, Nemotron-3, GLM, MiniMax-M2, DeepSeek-V4, Kimi-K2 / K2.5, Gemma 4,
|
|
83
|
+
and Inkling. The call parses normally, and `bridge_to_next_turn` extends it
|
|
84
|
+
without inserting a close. A closing marker later in the completion still wins:
|
|
85
|
+
tool markup before it stays reasoning. Inkling ends thinking and tool segments
|
|
86
|
+
with the same `<|end_message|>`, so there the first opener decides.
|
|
87
|
+
|
|
88
|
+
For reasoning-first formats, only an initial `<think>` (after any assistant
|
|
89
|
+
header) or reasoning already opened in the prompt starts reasoning. After its
|
|
90
|
+
first closing marker, the rest is content. Later think markers remain literal
|
|
91
|
+
content: `Before<think>example</think>After` is entirely content. Harmony,
|
|
92
|
+
Gemma, and Inkling follow their explicit channel or message-segment grammar.
|
|
93
|
+
|
|
94
|
+
`ParsedResponse.reasoning_complete` is `False` while a reasoning channel is open,
|
|
95
|
+
`True` when it is closed (or the format has no reasoning channel), and `None` when
|
|
96
|
+
its state cannot be determined. It does not certify tool syntax, normal engine
|
|
97
|
+
termination, nonempty final output, or task success. The generate client forwards
|
|
98
|
+
this narrow signal and passes prompt context automatically. Custom renderers used
|
|
99
|
+
with `generate` should accept `parse_response(..., tools=None, prompt_ids=None)`;
|
|
100
|
+
formats that do not need prompt context can ignore it.
|
|
101
|
+
|
|
102
|
+
Summary consumers should require normal termination (`finish_reason="stop"`),
|
|
103
|
+
nonempty final content, and no tool calls. A length-limited final answer remains
|
|
104
|
+
truncated even when its reasoning channel is closed. Keep rejected attempts in
|
|
105
|
+
usage/training accounting, and replace conversation history only after acceptance.
|
|
106
|
+
|
|
70
107
|
For the next turn, extend the previous sampled stream instead of re-rendering history:
|
|
71
108
|
|
|
72
109
|
```python
|
|
@@ -77,12 +114,14 @@ next_prompt_ids = r.bridge_to_next_turn(
|
|
|
77
114
|
)
|
|
78
115
|
```
|
|
79
116
|
|
|
80
|
-
Hand-coded renderers ship for `qwen3`, `qwen3-vl`, `qwen3.5`, `qwen3.6`, `qwen3.8`, `gemma4`, `glm-5`, `glm-5.1`, `glm-4.5`, `minimax-m2`, `deepseek-v3`, `deepseek-r1`, `deepseek-v4` (V4 Flash 0731), `kimi-k2`, `kimi-k2.5` / `kimi-k2.6`, `laguna-xs.2`, `laguna-xs-2.1`, `laguna-s-2.1`, `laguna-m.1`, `nemotron-3`, `nemotron-3-ultra`, `nemotron-3.5`, `llama-3`, `gpt-oss`, `hy3`, `inkling` / `inkling-small`, and `prime-qwen3`. Anything else falls back to `DefaultRenderer`, a generic `apply_chat_template` wrapper. `qwen3-vl`, `qwen3.5`, `qwen3.6`, `qwen3.8`, `gemma4`, `kimi-k2.5` / `kimi-k2.6`, and the Inkling checkpoints are multimodal (Inkling handles both image **and** audio).
|
|
117
|
+
Hand-coded renderers ship for `qwen3`, `qwen3-vl`, `qwen3.5`, `qwen3.6`, `qwen3.8`, `gemma4`, `glm-5`, `glm-5.1`, `glm-5.3`, `glm-4.5`, `minimax-m2`, `deepseek-v3`, `deepseek-r1`, `deepseek-v4` (V4 Flash 0731), `kimi-k2`, `kimi-k2.5` / `kimi-k2.6`, `laguna-xs.2`, `laguna-xs-2.1`, `laguna-s-2.1`, `laguna-m.1`, `nemotron-3`, `nemotron-3-ultra`, `nemotron-3.5`, `llama-3`, `gpt-oss`, `hy3`, `inkling` / `inkling-small`, and `prime-qwen3`. Anything else falls back to `DefaultRenderer`, a generic `apply_chat_template` wrapper. `qwen3-vl`, `qwen3.5`, `qwen3.6`, `qwen3.8`, `gemma4`, `kimi-k2.5` / `kimi-k2.6`, and the Inkling checkpoints are multimodal (Inkling handles both image **and** audio).
|
|
81
118
|
|
|
82
119
|
## API
|
|
83
120
|
|
|
84
121
|
```python
|
|
85
122
|
class Renderer(Protocol):
|
|
123
|
+
@property
|
|
124
|
+
def is_prefix_stable(self) -> bool: ...
|
|
86
125
|
def render(messages, *, tools=None, add_generation_prompt=False) -> RenderedTokens: ...
|
|
87
126
|
def render_ids(messages, *, tools=None, add_generation_prompt=False) -> list[int]: ...
|
|
88
127
|
def parse_response(token_ids) -> ParsedResponse: ...
|
|
@@ -90,18 +129,49 @@ class Renderer(Protocol):
|
|
|
90
129
|
def bridge_to_next_turn(prev_prompt_ids, prev_completion_ids, new_messages, *, tools=None) -> list[int] | None: ...
|
|
91
130
|
```
|
|
92
131
|
|
|
93
|
-
- `RenderedTokens` carries `token_ids` **and** `message_indices` — one entry per token attributing each to its
|
|
132
|
+
- `RenderedTokens` carries `token_ids` **and** `message_indices` — one entry per token attributing each to its owning message (`-1` for structural scaffolding). Sampled closing markers belong to their assistant, including GLM markers emitted at the next user/tool boundary; user/tool bodies keep their own ownership.
|
|
133
|
+
- `build_training_sample(..., message_loss_mask=[0, 0, 0, 1])` keeps all messages as context but restricts loss to selected messages. Supply one boolean or integer `0`/`1` per message, or `None` for the normal mask. Excluding an assistant also excludes its closing token. Selection never enables targets disabled by the existing role/content settings or changes token IDs.
|
|
94
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.
|
|
95
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`.
|
|
96
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
|
+
|
|
97
166
|
### `bridge_to_next_turn` (the core contract)
|
|
98
167
|
|
|
99
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.
|
|
100
169
|
|
|
101
170
|
Each hand-coded bridge:
|
|
102
|
-
1.
|
|
103
|
-
2.
|
|
104
|
-
3.
|
|
171
|
+
1. Checks the previous turn's reasoning state. If reasoning is unfinished and no stop was sampled, append its closing marker as non-loss prompt context before the canonical turn close. If a stop was already sampled inside reasoning, return `None`: inserting a close before it would violate the exact-prefix contract. Parsing never repairs the sampled output.
|
|
172
|
+
2. Anchors at the previous turn's canonical close token. On clean stops it's already in `prev_completion_ids`. On truncation, the renderer synthesizes the close as non-loss prompt context.
|
|
173
|
+
3. Refuses assistant content in `new_messages` — re-rendering sampled tokens would replace them with canonical template bytes.
|
|
174
|
+
4. Renders only the new messages in the framing the model family expects.
|
|
105
175
|
|
|
106
176
|
`DefaultRenderer.bridge_to_next_turn` returns `None` unconditionally — the template's close is unknown, so the contract can't be proven.
|
|
107
177
|
|
|
@@ -183,6 +253,42 @@ One shared behaviour flag lives on typed renderer configs: `thinking_retention`,
|
|
|
183
253
|
|
|
184
254
|
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).
|
|
185
255
|
|
|
256
|
+
## Custom renderers
|
|
257
|
+
|
|
258
|
+
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:
|
|
259
|
+
|
|
260
|
+
```python
|
|
261
|
+
from typing import Literal
|
|
262
|
+
|
|
263
|
+
from renderers.configs import GLM53RendererConfig
|
|
264
|
+
from renderers.glm5 import GLM53Renderer
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class NoThinkingGLM53Config(GLM53RendererConfig):
|
|
268
|
+
name: Literal["glm-5.3-no-thinking"] = "glm-5.3-no-thinking"
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
class NoThinkingGLM53Renderer(GLM53Renderer):
|
|
272
|
+
config_class = NoThinkingGLM53Config
|
|
273
|
+
|
|
274
|
+
def _emit_generation_prompt(self, emit_special) -> None:
|
|
275
|
+
emit_special(self._assistant, -1, is_sampled=False, is_content=False)
|
|
276
|
+
emit_special(self._think, -1, is_sampled=False, is_content=False)
|
|
277
|
+
emit_special(self._think_end, -1, is_sampled=False, is_content=False)
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
```python
|
|
281
|
+
from renderers import CustomRendererConfig, create_renderer
|
|
282
|
+
|
|
283
|
+
config = CustomRendererConfig(
|
|
284
|
+
import_path="my_renderers.glm53_no_thinking.NoThinkingGLM53Renderer",
|
|
285
|
+
clear_thinking=True,
|
|
286
|
+
)
|
|
287
|
+
renderer = create_renderer(tokenizer, config)
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
Downstream TOML configs select it the same way, with `name = "custom"` and an `import_path`.
|
|
291
|
+
|
|
186
292
|
## `DefaultRenderer`
|
|
187
293
|
|
|
188
294
|
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.
|
|
@@ -199,7 +305,31 @@ uv sync --group dev
|
|
|
199
305
|
uv run pytest
|
|
200
306
|
```
|
|
201
307
|
|
|
202
|
-
Round-trip parity (render → parse → original) and token-level parity against
|
|
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.
|
|
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.
|
|
203
333
|
|
|
204
334
|
## License
|
|
205
335
|
|
|
@@ -43,10 +43,47 @@ prompt_ids = r.render_ids(
|
|
|
43
43
|
# Feed prompt_ids to a Token-In, Token-Out endpoint.
|
|
44
44
|
# It returns completion_ids sampled by the model.
|
|
45
45
|
|
|
46
|
-
parsed = r.parse_response(completion_ids)
|
|
46
|
+
parsed = r.parse_response(completion_ids, prompt_ids=prompt_ids)
|
|
47
47
|
# ParsedResponse(content=..., reasoning_content=..., tool_calls=...)
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
+
Pass the exact sampling `prompt_ids` into `parse_response` to identify reasoning
|
|
51
|
+
opened by the generation prompt. Without its closing marker, the sampled text
|
|
52
|
+
stays in `reasoning_content`, with empty `content` and no executable tool calls,
|
|
53
|
+
even if the engine reports `stop`. This applies to every supported reasoning
|
|
54
|
+
format, including Harmony analysis and Gemma's post-tool thought channel.
|
|
55
|
+
Omitting `prompt_ids`, passing `None`, and passing `[]` all parse the completion
|
|
56
|
+
as self-contained: reasoning needs its own initial opener or channel header.
|
|
57
|
+
Generation settings never supply missing parsing context. If the prompt supplied
|
|
58
|
+
the opener, pass that prompt; `generate` does this automatically.
|
|
59
|
+
|
|
60
|
+
Where vLLM 0.26's parser does the same, the format's atomic tool-call opener
|
|
61
|
+
also ends reasoning that has no closing marker: Qwen3 / Qwen3-VL / Qwen3.5+,
|
|
62
|
+
Prime-Qwen3, Nemotron-3, GLM, MiniMax-M2, DeepSeek-V4, Kimi-K2 / K2.5, Gemma 4,
|
|
63
|
+
and Inkling. The call parses normally, and `bridge_to_next_turn` extends it
|
|
64
|
+
without inserting a close. A closing marker later in the completion still wins:
|
|
65
|
+
tool markup before it stays reasoning. Inkling ends thinking and tool segments
|
|
66
|
+
with the same `<|end_message|>`, so there the first opener decides.
|
|
67
|
+
|
|
68
|
+
For reasoning-first formats, only an initial `<think>` (after any assistant
|
|
69
|
+
header) or reasoning already opened in the prompt starts reasoning. After its
|
|
70
|
+
first closing marker, the rest is content. Later think markers remain literal
|
|
71
|
+
content: `Before<think>example</think>After` is entirely content. Harmony,
|
|
72
|
+
Gemma, and Inkling follow their explicit channel or message-segment grammar.
|
|
73
|
+
|
|
74
|
+
`ParsedResponse.reasoning_complete` is `False` while a reasoning channel is open,
|
|
75
|
+
`True` when it is closed (or the format has no reasoning channel), and `None` when
|
|
76
|
+
its state cannot be determined. It does not certify tool syntax, normal engine
|
|
77
|
+
termination, nonempty final output, or task success. The generate client forwards
|
|
78
|
+
this narrow signal and passes prompt context automatically. Custom renderers used
|
|
79
|
+
with `generate` should accept `parse_response(..., tools=None, prompt_ids=None)`;
|
|
80
|
+
formats that do not need prompt context can ignore it.
|
|
81
|
+
|
|
82
|
+
Summary consumers should require normal termination (`finish_reason="stop"`),
|
|
83
|
+
nonempty final content, and no tool calls. A length-limited final answer remains
|
|
84
|
+
truncated even when its reasoning channel is closed. Keep rejected attempts in
|
|
85
|
+
usage/training accounting, and replace conversation history only after acceptance.
|
|
86
|
+
|
|
50
87
|
For the next turn, extend the previous sampled stream instead of re-rendering history:
|
|
51
88
|
|
|
52
89
|
```python
|
|
@@ -57,12 +94,14 @@ next_prompt_ids = r.bridge_to_next_turn(
|
|
|
57
94
|
)
|
|
58
95
|
```
|
|
59
96
|
|
|
60
|
-
Hand-coded renderers ship for `qwen3`, `qwen3-vl`, `qwen3.5`, `qwen3.6`, `qwen3.8`, `gemma4`, `glm-5`, `glm-5.1`, `glm-4.5`, `minimax-m2`, `deepseek-v3`, `deepseek-r1`, `deepseek-v4` (V4 Flash 0731), `kimi-k2`, `kimi-k2.5` / `kimi-k2.6`, `laguna-xs.2`, `laguna-xs-2.1`, `laguna-s-2.1`, `laguna-m.1`, `nemotron-3`, `nemotron-3-ultra`, `nemotron-3.5`, `llama-3`, `gpt-oss`, `hy3`, `inkling` / `inkling-small`, and `prime-qwen3`. Anything else falls back to `DefaultRenderer`, a generic `apply_chat_template` wrapper. `qwen3-vl`, `qwen3.5`, `qwen3.6`, `qwen3.8`, `gemma4`, `kimi-k2.5` / `kimi-k2.6`, and the Inkling checkpoints are multimodal (Inkling handles both image **and** audio).
|
|
97
|
+
Hand-coded renderers ship for `qwen3`, `qwen3-vl`, `qwen3.5`, `qwen3.6`, `qwen3.8`, `gemma4`, `glm-5`, `glm-5.1`, `glm-5.3`, `glm-4.5`, `minimax-m2`, `deepseek-v3`, `deepseek-r1`, `deepseek-v4` (V4 Flash 0731), `kimi-k2`, `kimi-k2.5` / `kimi-k2.6`, `laguna-xs.2`, `laguna-xs-2.1`, `laguna-s-2.1`, `laguna-m.1`, `nemotron-3`, `nemotron-3-ultra`, `nemotron-3.5`, `llama-3`, `gpt-oss`, `hy3`, `inkling` / `inkling-small`, and `prime-qwen3`. Anything else falls back to `DefaultRenderer`, a generic `apply_chat_template` wrapper. `qwen3-vl`, `qwen3.5`, `qwen3.6`, `qwen3.8`, `gemma4`, `kimi-k2.5` / `kimi-k2.6`, and the Inkling checkpoints are multimodal (Inkling handles both image **and** audio).
|
|
61
98
|
|
|
62
99
|
## API
|
|
63
100
|
|
|
64
101
|
```python
|
|
65
102
|
class Renderer(Protocol):
|
|
103
|
+
@property
|
|
104
|
+
def is_prefix_stable(self) -> bool: ...
|
|
66
105
|
def render(messages, *, tools=None, add_generation_prompt=False) -> RenderedTokens: ...
|
|
67
106
|
def render_ids(messages, *, tools=None, add_generation_prompt=False) -> list[int]: ...
|
|
68
107
|
def parse_response(token_ids) -> ParsedResponse: ...
|
|
@@ -70,18 +109,49 @@ class Renderer(Protocol):
|
|
|
70
109
|
def bridge_to_next_turn(prev_prompt_ids, prev_completion_ids, new_messages, *, tools=None) -> list[int] | None: ...
|
|
71
110
|
```
|
|
72
111
|
|
|
73
|
-
- `RenderedTokens` carries `token_ids` **and** `message_indices` — one entry per token attributing each to its
|
|
112
|
+
- `RenderedTokens` carries `token_ids` **and** `message_indices` — one entry per token attributing each to its owning message (`-1` for structural scaffolding). Sampled closing markers belong to their assistant, including GLM markers emitted at the next user/tool boundary; user/tool bodies keep their own ownership.
|
|
113
|
+
- `build_training_sample(..., message_loss_mask=[0, 0, 0, 1])` keeps all messages as context but restricts loss to selected messages. Supply one boolean or integer `0`/`1` per message, or `None` for the normal mask. Excluding an assistant also excludes its closing token. Selection never enables targets disabled by the existing role/content settings or changes token IDs.
|
|
74
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.
|
|
75
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`.
|
|
76
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
|
+
|
|
77
146
|
### `bridge_to_next_turn` (the core contract)
|
|
78
147
|
|
|
79
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.
|
|
80
149
|
|
|
81
150
|
Each hand-coded bridge:
|
|
82
|
-
1.
|
|
83
|
-
2.
|
|
84
|
-
3.
|
|
151
|
+
1. Checks the previous turn's reasoning state. If reasoning is unfinished and no stop was sampled, append its closing marker as non-loss prompt context before the canonical turn close. If a stop was already sampled inside reasoning, return `None`: inserting a close before it would violate the exact-prefix contract. Parsing never repairs the sampled output.
|
|
152
|
+
2. Anchors at the previous turn's canonical close token. On clean stops it's already in `prev_completion_ids`. On truncation, the renderer synthesizes the close as non-loss prompt context.
|
|
153
|
+
3. Refuses assistant content in `new_messages` — re-rendering sampled tokens would replace them with canonical template bytes.
|
|
154
|
+
4. Renders only the new messages in the framing the model family expects.
|
|
85
155
|
|
|
86
156
|
`DefaultRenderer.bridge_to_next_turn` returns `None` unconditionally — the template's close is unknown, so the contract can't be proven.
|
|
87
157
|
|
|
@@ -163,6 +233,42 @@ One shared behaviour flag lives on typed renderer configs: `thinking_retention`,
|
|
|
163
233
|
|
|
164
234
|
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).
|
|
165
235
|
|
|
236
|
+
## Custom renderers
|
|
237
|
+
|
|
238
|
+
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:
|
|
239
|
+
|
|
240
|
+
```python
|
|
241
|
+
from typing import Literal
|
|
242
|
+
|
|
243
|
+
from renderers.configs import GLM53RendererConfig
|
|
244
|
+
from renderers.glm5 import GLM53Renderer
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class NoThinkingGLM53Config(GLM53RendererConfig):
|
|
248
|
+
name: Literal["glm-5.3-no-thinking"] = "glm-5.3-no-thinking"
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class NoThinkingGLM53Renderer(GLM53Renderer):
|
|
252
|
+
config_class = NoThinkingGLM53Config
|
|
253
|
+
|
|
254
|
+
def _emit_generation_prompt(self, emit_special) -> None:
|
|
255
|
+
emit_special(self._assistant, -1, is_sampled=False, is_content=False)
|
|
256
|
+
emit_special(self._think, -1, is_sampled=False, is_content=False)
|
|
257
|
+
emit_special(self._think_end, -1, is_sampled=False, is_content=False)
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
```python
|
|
261
|
+
from renderers import CustomRendererConfig, create_renderer
|
|
262
|
+
|
|
263
|
+
config = CustomRendererConfig(
|
|
264
|
+
import_path="my_renderers.glm53_no_thinking.NoThinkingGLM53Renderer",
|
|
265
|
+
clear_thinking=True,
|
|
266
|
+
)
|
|
267
|
+
renderer = create_renderer(tokenizer, config)
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Downstream TOML configs select it the same way, with `name = "custom"` and an `import_path`.
|
|
271
|
+
|
|
166
272
|
## `DefaultRenderer`
|
|
167
273
|
|
|
168
274
|
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.
|
|
@@ -179,7 +285,31 @@ uv sync --group dev
|
|
|
179
285
|
uv run pytest
|
|
180
286
|
```
|
|
181
287
|
|
|
182
|
-
Round-trip parity (render → parse → original) and token-level parity against
|
|
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.
|
|
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.
|
|
183
313
|
|
|
184
314
|
## License
|
|
185
315
|
|
|
@@ -21,8 +21,8 @@ construction.
|
|
|
21
21
|
Use `type(config).template_field_names()` to inspect the explicit allowlist of
|
|
22
22
|
fields accepted through `chat_template_kwargs`. Every renderer-specific field
|
|
23
23
|
is classified as either a template field or a renderer-only field at class
|
|
24
|
-
definition time. Template fields are covered by
|
|
25
|
-
|
|
24
|
+
definition time. Template fields are covered by the model/scenario/kwarg
|
|
25
|
+
product in `tests/test_parity.py`.
|
|
26
26
|
|
|
27
27
|
| Renderer | Config class | Template fields | Renderer-only fields |
|
|
28
28
|
| --- | --- | --- | --- |
|
|
@@ -34,6 +34,7 @@ definition time. Template fields are covered by parity tests against
|
|
|
34
34
|
| Qwen3-VL | `Qwen3VLRendererConfig` | `add_vision_id` | `image_cache_max` |
|
|
35
35
|
| Gemma 4 | `Gemma4RendererConfig` | `enable_thinking`, `preserve_thinking` | `image_cache_max` |
|
|
36
36
|
| GLM-5 / 5.1 | `GLM5RendererConfig` / `GLM51RendererConfig` | `enable_thinking`, `clear_thinking` | - |
|
|
37
|
+
| GLM-5.3 | `GLM53RendererConfig` | `clear_thinking`, `reasoning_effort` | - |
|
|
37
38
|
| GLM-4.5 | `GLM45RendererConfig` | `enable_thinking` | - |
|
|
38
39
|
| gpt-oss | `GptOssRendererConfig` | `reasoning_effort`, `conversation_start_date` | `use_system_prompt`, `knowledge_cutoff`, `model_identity`, `auto_drop_analysis` |
|
|
39
40
|
| Hy3 | `Hy3RendererConfig` | `reasoning_effort`, `preserved_thinking`, `is_training`, `raw_last_assistant`, `fallback_strategy` | - |
|
|
@@ -140,6 +141,7 @@ the knobs its template actually exposes:
|
|
|
140
141
|
| Qwen3.8 | `preserve_thinking=True -> all`; else `enable_thinking=False -> all`; else `tool_cycle` |
|
|
141
142
|
| Gemma 4 | `preserve_thinking=True -> all`; else `enable_thinking=False -> all`; else `tool_cycle` |
|
|
142
143
|
| GLM-5 / 5.1 | `clear_thinking=False -> all`; else `enable_thinking=False -> all`; else `tool_cycle` |
|
|
144
|
+
| GLM-5.3 | `clear_thinking=False -> all`, else `tool_cycle` |
|
|
143
145
|
| GLM-4.5 | `enable_thinking=False -> all`, else `tool_cycle` |
|
|
144
146
|
| gpt-oss | `auto_drop_analysis=False -> all`, else `tool_cycle` |
|
|
145
147
|
| Hy3 | `preserved_thinking=True -> all`, else `tool_cycle` |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env -S uv run --script
|
|
2
2
|
# /// script
|
|
3
|
-
# requires-python = ">=3.
|
|
3
|
+
# requires-python = ">=3.11,<3.14"
|
|
4
4
|
# dependencies = [
|
|
5
5
|
# "renderers>=0.1.6",
|
|
6
6
|
# "sglang==0.5.10.post1",
|
|
@@ -138,7 +138,7 @@ def main() -> None:
|
|
|
138
138
|
)
|
|
139
139
|
output1 = engine.generate(input_ids=prompt_ids, sampling_params=sampling)
|
|
140
140
|
completion1 = completion_ids(output1, prompt_ids)
|
|
141
|
-
parsed1 = renderer.parse_response(completion1)
|
|
141
|
+
parsed1 = renderer.parse_response(completion1, prompt_ids=prompt_ids)
|
|
142
142
|
print_parsed(label, "turn 1", parsed1)
|
|
143
143
|
|
|
144
144
|
assistant = {"role": "assistant", "content": parsed1.content}
|
|
@@ -197,7 +197,11 @@ def main() -> None:
|
|
|
197
197
|
|
|
198
198
|
output2 = engine.generate(input_ids=bridged_ids, sampling_params=sampling)
|
|
199
199
|
completion2 = completion_ids(output2, bridged_ids)
|
|
200
|
-
print_parsed(
|
|
200
|
+
print_parsed(
|
|
201
|
+
label,
|
|
202
|
+
"turn 2",
|
|
203
|
+
renderer.parse_response(completion2, prompt_ids=bridged_ids),
|
|
204
|
+
)
|
|
201
205
|
|
|
202
206
|
engine.shutdown()
|
|
203
207
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env -S uv run --script
|
|
2
2
|
# /// script
|
|
3
|
-
# requires-python = ">=3.
|
|
3
|
+
# requires-python = ">=3.11,<3.14"
|
|
4
4
|
# dependencies = [
|
|
5
5
|
# "renderers>=0.1.6",
|
|
6
6
|
# "transformers>=5.3.0",
|
|
@@ -161,7 +161,7 @@ async def run_one(
|
|
|
161
161
|
max_new_tokens=max_new_tokens,
|
|
162
162
|
)
|
|
163
163
|
completion1 = completion_ids(output1, prompt_ids)
|
|
164
|
-
parsed1 = renderer.parse_response(completion1)
|
|
164
|
+
parsed1 = renderer.parse_response(completion1, prompt_ids=prompt_ids)
|
|
165
165
|
print_parsed(label, "turn 1", parsed1)
|
|
166
166
|
|
|
167
167
|
assistant: dict[str, Any] = {"role": "assistant", "content": parsed1.content}
|
|
@@ -226,7 +226,9 @@ async def run_one(
|
|
|
226
226
|
max_new_tokens=max_new_tokens,
|
|
227
227
|
)
|
|
228
228
|
completion2 = completion_ids(output2, bridged_ids)
|
|
229
|
-
print_parsed(
|
|
229
|
+
print_parsed(
|
|
230
|
+
label, "turn 2", renderer.parse_response(completion2, prompt_ids=bridged_ids)
|
|
231
|
+
)
|
|
230
232
|
|
|
231
233
|
|
|
232
234
|
async def main() -> None:
|
|
@@ -128,7 +128,7 @@ async def main() -> None:
|
|
|
128
128
|
sampling_params=sampling_params,
|
|
129
129
|
)
|
|
130
130
|
completion1 = list(output1.sequences[0].tokens)
|
|
131
|
-
parsed1 = renderer.parse_response(completion1)
|
|
131
|
+
parsed1 = renderer.parse_response(completion1, prompt_ids=prompt_ids)
|
|
132
132
|
print_parsed(label, "turn 1", parsed1)
|
|
133
133
|
|
|
134
134
|
assistant = {"role": "assistant", "content": parsed1.content}
|
|
@@ -191,7 +191,11 @@ async def main() -> None:
|
|
|
191
191
|
sampling_params=sampling_params,
|
|
192
192
|
)
|
|
193
193
|
completion2 = list(output2.sequences[0].tokens)
|
|
194
|
-
print_parsed(
|
|
194
|
+
print_parsed(
|
|
195
|
+
label,
|
|
196
|
+
"turn 2",
|
|
197
|
+
renderer.parse_response(completion2, prompt_ids=bridged_ids),
|
|
198
|
+
)
|
|
195
199
|
|
|
196
200
|
|
|
197
201
|
if __name__ == "__main__":
|
{renderers-0.1.11.dev4 → renderers-0.1.12}/examples/transformers/multiturn_generate_transformers.py
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env -S uv run --script
|
|
2
2
|
# /// script
|
|
3
|
-
# requires-python = ">=3.
|
|
3
|
+
# requires-python = ">=3.11,<3.14"
|
|
4
4
|
# dependencies = [
|
|
5
5
|
# "renderers>=0.1.6",
|
|
6
6
|
# "transformers>=4.50.0",
|
|
@@ -135,7 +135,7 @@ def main() -> None:
|
|
|
135
135
|
pad_token_id=pad_token_id,
|
|
136
136
|
)[0]
|
|
137
137
|
completion1 = output1[input_ids.shape[-1] :].tolist()
|
|
138
|
-
parsed1 = renderer.parse_response(completion1)
|
|
138
|
+
parsed1 = renderer.parse_response(completion1, prompt_ids=prompt_ids)
|
|
139
139
|
print_parsed(label, "turn 1", parsed1)
|
|
140
140
|
|
|
141
141
|
assistant = {"role": "assistant", "content": parsed1.content}
|
|
@@ -203,7 +203,11 @@ def main() -> None:
|
|
|
203
203
|
pad_token_id=pad_token_id,
|
|
204
204
|
)[0]
|
|
205
205
|
completion2 = output2[bridged_input_ids.shape[-1] :].tolist()
|
|
206
|
-
print_parsed(
|
|
206
|
+
print_parsed(
|
|
207
|
+
label,
|
|
208
|
+
"turn 2",
|
|
209
|
+
renderer.parse_response(completion2, prompt_ids=bridged_ids),
|
|
210
|
+
)
|
|
207
211
|
|
|
208
212
|
del hf_model
|
|
209
213
|
gc.collect()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env -S uv run --script
|
|
2
2
|
# /// script
|
|
3
|
-
# requires-python = ">=3.
|
|
3
|
+
# requires-python = ">=3.11,<3.14"
|
|
4
4
|
# dependencies = [
|
|
5
5
|
# "renderers>=0.1.6",
|
|
6
6
|
# "vllm>=0.20",
|
|
@@ -131,7 +131,7 @@ def main() -> None:
|
|
|
131
131
|
use_tqdm=False,
|
|
132
132
|
)[0]
|
|
133
133
|
completion1 = list(output1.outputs[0].token_ids)
|
|
134
|
-
parsed1 = renderer.parse_response(completion1)
|
|
134
|
+
parsed1 = renderer.parse_response(completion1, prompt_ids=prompt_ids)
|
|
135
135
|
print_parsed(label, "turn 1", parsed1)
|
|
136
136
|
|
|
137
137
|
assistant = {"role": "assistant", "content": parsed1.content}
|
|
@@ -194,7 +194,11 @@ def main() -> None:
|
|
|
194
194
|
use_tqdm=False,
|
|
195
195
|
)[0]
|
|
196
196
|
completion2 = list(output2.outputs[0].token_ids)
|
|
197
|
-
print_parsed(
|
|
197
|
+
print_parsed(
|
|
198
|
+
label,
|
|
199
|
+
"turn 2",
|
|
200
|
+
renderer.parse_response(completion2, prompt_ids=bridged_ids),
|
|
201
|
+
)
|
|
198
202
|
|
|
199
203
|
del llm
|
|
200
204
|
gc.collect()
|
|
@@ -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",
|
|
@@ -27,7 +27,7 @@ dependencies = [
|
|
|
27
27
|
# release through 0.5.12.post1 hard-pins ``openai-harmony==0.0.4``, so a
|
|
28
28
|
# higher floor makes ``renderers`` uninstallable alongside SGLang. The
|
|
29
29
|
# GptOssRenderer renders byte-identically on 0.0.4 (verified token-for-token
|
|
30
|
-
# against 0.0.8) and ``tests/
|
|
30
|
+
# against 0.0.8) and the Harmony cells in ``tests/test_parity.py`` pass,
|
|
31
31
|
# so the older harmony is safe.
|
|
32
32
|
"openai-harmony>=0.0.4",
|
|
33
33
|
# ``BaseRendererConfig`` inherits from ``pydantic_config.BaseConfig`` so
|