renderers 0.1.12.dev3__tar.gz → 0.1.12.dev5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/.github/workflows/test.yml +1 -1
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/PKG-INFO +36 -6
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/README.md +34 -4
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/examples/sglang/multiturn_generate_sglang.py +6 -2
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/examples/sglang/online_multiturn_sglang.py +4 -2
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/examples/tinker/multiturn_generate_tinker.py +6 -2
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/examples/transformers/multiturn_generate_transformers.py +6 -2
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/examples/vllm/multiturn_generate_vllm.py +6 -2
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/pyproject.toml +1 -1
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/renderers/_version.py +2 -2
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/renderers/base.py +14 -0
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/renderers/client.py +6 -1
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/renderers/deepseek_v3.py +26 -0
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/renderers/deepseek_v4.py +24 -1
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/renderers/default.py +52 -28
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/renderers/gemma4.py +64 -11
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/renderers/glm45.py +22 -0
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/renderers/glm5.py +22 -0
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/renderers/gpt_oss.py +61 -3
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/renderers/hy3.py +28 -0
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/renderers/inkling.py +41 -0
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/renderers/kimi_k2.py +26 -0
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/renderers/kimi_k25.py +51 -94
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/renderers/laguna_xs2.py +44 -0
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/renderers/llama_3.py +1 -0
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/renderers/minimax_m2.py +22 -0
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/renderers/nemotron3.py +21 -0
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/renderers/parsers.py +4 -6
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/renderers/parsing.py +234 -127
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/renderers/prime_qwen3.py +21 -0
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/renderers/qwen3.py +20 -0
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/renderers/qwen35.py +20 -0
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/renderers/qwen3_vl.py +21 -0
- renderers-0.1.12.dev5/renderers/reasoning.py +199 -0
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/tests/test_bridge.py +7 -0
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/tests/test_client.py +29 -2
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/tests/test_deepseek_v4.py +32 -4
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/tests/test_gemma4.py +1 -1
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/tests/test_glm_tool_name_validation.py +1 -1
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/tests/test_hy3.py +4 -1
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/tests/test_laguna_m1.py +1 -1
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/tests/test_laguna_xs21.py +36 -4
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/tests/test_multimodal.py +8 -2
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/tests/test_parse_response.py +25 -13
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/tests/test_parse_response_robustness.py +14 -14
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/tests/test_parsers.py +14 -0
- renderers-0.1.12.dev5/tests/test_reasoning_boundaries.py +504 -0
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/tests/test_roundtrip.py +7 -7
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/uv.lock +271 -10
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/.github/workflows/publish-dev.yml +0 -0
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/.github/workflows/publish.yml +0 -0
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/.github/workflows/style.yml +0 -0
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/.gitignore +0 -0
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/.pre-commit-config.yaml +0 -0
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/LICENSE +0 -0
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/docs/renderer-config.md +0 -0
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/examples/README.md +0 -0
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/renderers/__init__.py +0 -0
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/renderers/configs.py +0 -0
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/renderers/deepseek_r1.py +0 -0
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/renderers/laguna_s21.py +0 -0
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/renderers/qwen36.py +0 -0
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/renderers/qwen38.py +0 -0
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/tests/conftest.py +0 -0
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/tests/parity.py +0 -0
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/tests/reference_rendering.py +0 -0
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/tests/test_build_helpers.py +0 -0
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/tests/test_deepseek_r1.py +0 -0
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/tests/test_disabled_thinking_stability.py +0 -0
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/tests/test_incremental.py +0 -0
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/tests/test_inkling.py +0 -0
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/tests/test_is_content.py +0 -0
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/tests/test_kimi_k25_tool_schema.py +0 -0
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/tests/test_llama_3.py +0 -0
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/tests/test_load_tokenizer.py +0 -0
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/tests/test_message_indices.py +0 -0
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/tests/test_message_tool_names.py +0 -0
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/tests/test_nemotron3_ultra.py +0 -0
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/tests/test_offsetless_tokenizers.py +0 -0
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/tests/test_parity.py +0 -0
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/tests/test_preserve_thinking.py +0 -0
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/tests/test_qwen35_size_coverage.py +0 -0
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/tests/test_qwen38.py +0 -0
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/tests/test_renderer_config.py +0 -0
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/tests/test_sampled_mask.py +0 -0
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/tests/test_tokens_per_message.py +0 -0
- {renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/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.dev5
|
|
4
4
|
Summary: Chat template renderers — deterministic message-to-token conversion for LLM training
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
License-File: LICENSE
|
|
7
|
-
Requires-Python: <3.
|
|
7
|
+
Requires-Python: <3.15,>=3.10
|
|
8
8
|
Requires-Dist: jinja2
|
|
9
9
|
Requires-Dist: numpy
|
|
10
10
|
Requires-Dist: openai-harmony>=0.0.4
|
|
@@ -63,10 +63,39 @@ 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
|
+
For reasoning-first formats, only an initial `<think>` (after any assistant
|
|
81
|
+
header) or reasoning already opened in the prompt starts reasoning. After its
|
|
82
|
+
first closing marker, the rest is content. Later think markers remain literal
|
|
83
|
+
content: `Before<think>example</think>After` is entirely content. Harmony,
|
|
84
|
+
Gemma, and Inkling follow their explicit channel or message-segment grammar.
|
|
85
|
+
|
|
86
|
+
`ParsedResponse.reasoning_complete` is `False` while a reasoning channel is open,
|
|
87
|
+
`True` when it is closed (or the format has no reasoning channel), and `None` when
|
|
88
|
+
its state cannot be determined. It does not certify tool syntax, normal engine
|
|
89
|
+
termination, nonempty final output, or task success. The generate client forwards
|
|
90
|
+
this narrow signal and passes prompt context automatically. Custom renderers used
|
|
91
|
+
with `generate` should accept `parse_response(..., tools=None, prompt_ids=None)`;
|
|
92
|
+
formats that do not need prompt context can ignore it.
|
|
93
|
+
|
|
94
|
+
Summary consumers should require normal termination (`finish_reason="stop"`),
|
|
95
|
+
nonempty final content, and no tool calls. A length-limited final answer remains
|
|
96
|
+
truncated even when its reasoning channel is closed. Keep rejected attempts in
|
|
97
|
+
usage/training accounting, and replace conversation history only after acceptance.
|
|
98
|
+
|
|
70
99
|
For the next turn, extend the previous sampled stream instead of re-rendering history:
|
|
71
100
|
|
|
72
101
|
```python
|
|
@@ -99,9 +128,10 @@ class Renderer(Protocol):
|
|
|
99
128
|
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
129
|
|
|
101
130
|
Each hand-coded bridge:
|
|
102
|
-
1.
|
|
103
|
-
2.
|
|
104
|
-
3.
|
|
131
|
+
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.
|
|
132
|
+
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.
|
|
133
|
+
3. Refuses assistant content in `new_messages` — re-rendering sampled tokens would replace them with canonical template bytes.
|
|
134
|
+
4. Renders only the new messages in the framing the model family expects.
|
|
105
135
|
|
|
106
136
|
`DefaultRenderer.bridge_to_next_turn` returns `None` unconditionally — the template's close is unknown, so the contract can't be proven.
|
|
107
137
|
|
|
@@ -43,10 +43,39 @@ 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
|
+
For reasoning-first formats, only an initial `<think>` (after any assistant
|
|
61
|
+
header) or reasoning already opened in the prompt starts reasoning. After its
|
|
62
|
+
first closing marker, the rest is content. Later think markers remain literal
|
|
63
|
+
content: `Before<think>example</think>After` is entirely content. Harmony,
|
|
64
|
+
Gemma, and Inkling follow their explicit channel or message-segment grammar.
|
|
65
|
+
|
|
66
|
+
`ParsedResponse.reasoning_complete` is `False` while a reasoning channel is open,
|
|
67
|
+
`True` when it is closed (or the format has no reasoning channel), and `None` when
|
|
68
|
+
its state cannot be determined. It does not certify tool syntax, normal engine
|
|
69
|
+
termination, nonempty final output, or task success. The generate client forwards
|
|
70
|
+
this narrow signal and passes prompt context automatically. Custom renderers used
|
|
71
|
+
with `generate` should accept `parse_response(..., tools=None, prompt_ids=None)`;
|
|
72
|
+
formats that do not need prompt context can ignore it.
|
|
73
|
+
|
|
74
|
+
Summary consumers should require normal termination (`finish_reason="stop"`),
|
|
75
|
+
nonempty final content, and no tool calls. A length-limited final answer remains
|
|
76
|
+
truncated even when its reasoning channel is closed. Keep rejected attempts in
|
|
77
|
+
usage/training accounting, and replace conversation history only after acceptance.
|
|
78
|
+
|
|
50
79
|
For the next turn, extend the previous sampled stream instead of re-rendering history:
|
|
51
80
|
|
|
52
81
|
```python
|
|
@@ -79,9 +108,10 @@ class Renderer(Protocol):
|
|
|
79
108
|
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
109
|
|
|
81
110
|
Each hand-coded bridge:
|
|
82
|
-
1.
|
|
83
|
-
2.
|
|
84
|
-
3.
|
|
111
|
+
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.
|
|
112
|
+
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.
|
|
113
|
+
3. Refuses assistant content in `new_messages` — re-rendering sampled tokens would replace them with canonical template bytes.
|
|
114
|
+
4. Renders only the new messages in the framing the model family expects.
|
|
85
115
|
|
|
86
116
|
`DefaultRenderer.bridge_to_next_turn` returns `None` unconditionally — the template's close is unknown, so the contract can't be proven.
|
|
87
117
|
|
{renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/examples/sglang/multiturn_generate_sglang.py
RENAMED
|
@@ -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
|
|
|
@@ -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:
|
{renderers-0.1.12.dev3 → renderers-0.1.12.dev5}/examples/tinker/multiturn_generate_tinker.py
RENAMED
|
@@ -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__":
|
|
@@ -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()
|
|
@@ -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.10,<3.
|
|
16
|
+
requires-python = ">=3.10,<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.dev5'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 12, 'dev5')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -632,6 +632,13 @@ class ParsedResponse:
|
|
|
632
632
|
content: str
|
|
633
633
|
reasoning_content: str | None = None
|
|
634
634
|
tool_calls: list[ParsedToolCall] = field(default_factory=list)
|
|
635
|
+
reasoning_complete: bool | None = True
|
|
636
|
+
"""Whether the reasoning channel is closed at the end of the completion.
|
|
637
|
+
|
|
638
|
+
False means generation ended inside reasoning; this is unrelated to the
|
|
639
|
+
engine finish reason, tool-call validity, or whether final text is useful.
|
|
640
|
+
None means the parser cannot determine the state for its reasoning format.
|
|
641
|
+
"""
|
|
635
642
|
|
|
636
643
|
|
|
637
644
|
@dataclass
|
|
@@ -743,9 +750,16 @@ class Renderer(Protocol):
|
|
|
743
750
|
token_ids: list[int],
|
|
744
751
|
*,
|
|
745
752
|
tools: list[ToolSpec] | None = None,
|
|
753
|
+
prompt_ids: list[int] | None = None,
|
|
746
754
|
) -> ParsedResponse:
|
|
747
755
|
"""Parse completion tokens back into a structured message.
|
|
748
756
|
|
|
757
|
+
Pass the exact ``prompt_ids`` used for sampling whenever available.
|
|
758
|
+
This identifies reasoning prefilled by the prompt, including Gemma's
|
|
759
|
+
post-tool thought channel and partial assistant prefixes. Omitting it,
|
|
760
|
+
passing None, and passing [] all parse a self-contained completion;
|
|
761
|
+
generation settings do not imply an initial reasoning channel.
|
|
762
|
+
|
|
749
763
|
``tools`` is the same list passed to ``render`` for this turn.
|
|
750
764
|
XML-style formats (Qwen3.5, GLM, MiniMax, Laguna) render argument
|
|
751
765
|
values verbatim inside ``<arg_value>`` tags with no quoting, so
|
|
@@ -367,7 +367,11 @@ async def generate(
|
|
|
367
367
|
|
|
368
368
|
completion_logprobs = _parse_completion_logprobs(choice, completion_ids)
|
|
369
369
|
|
|
370
|
-
parsed = renderer.parse_response(
|
|
370
|
+
parsed = renderer.parse_response(
|
|
371
|
+
completion_ids,
|
|
372
|
+
prompt_ids=list(effective_prompt_ids or prompt_ids),
|
|
373
|
+
tools=tools,
|
|
374
|
+
)
|
|
371
375
|
|
|
372
376
|
routed_experts = choice.get("routed_experts")
|
|
373
377
|
# vLLM's native kept-set sampling masks (``--return-sampling-mask``):
|
|
@@ -401,6 +405,7 @@ async def generate(
|
|
|
401
405
|
"reasoning_content": parsed.reasoning_content,
|
|
402
406
|
"tool_calls": parsed.tool_calls,
|
|
403
407
|
"finish_reason": finish_reason,
|
|
408
|
+
"reasoning_complete": parsed.reasoning_complete,
|
|
404
409
|
"routed_experts": routed_experts,
|
|
405
410
|
"sampling_mask": sampling_mask,
|
|
406
411
|
# The mm sidecar consumed on the request side, surfaced back so
|
|
@@ -14,6 +14,8 @@ from __future__ import annotations
|
|
|
14
14
|
|
|
15
15
|
import json
|
|
16
16
|
|
|
17
|
+
from renderers.reasoning import scan_reasoning, prompt_ends_in_reasoning
|
|
18
|
+
|
|
17
19
|
from renderers.base import (
|
|
18
20
|
Message,
|
|
19
21
|
ParsedResponse,
|
|
@@ -283,6 +285,7 @@ class DeepSeekV3Renderer:
|
|
|
283
285
|
token_ids: list[int],
|
|
284
286
|
*,
|
|
285
287
|
tools: list[ToolSpec] | None = None, # noqa: ARG002 — args land in a ```json fence, schema not needed
|
|
288
|
+
prompt_ids: list[int] | None = None,
|
|
286
289
|
) -> ParsedResponse:
|
|
287
290
|
return parse_deepseek_v3(
|
|
288
291
|
self._tokenizer,
|
|
@@ -293,6 +296,12 @@ class DeepSeekV3Renderer:
|
|
|
293
296
|
tool_call_begin_id=self._tool_call_begin,
|
|
294
297
|
tool_call_end_id=self._tool_call_end,
|
|
295
298
|
tool_sep_id=self._tool_sep,
|
|
299
|
+
prefilled_thinking=prompt_ends_in_reasoning(
|
|
300
|
+
self._tokenizer,
|
|
301
|
+
prompt_ids,
|
|
302
|
+
stop_ids=set(self.get_stop_token_ids()),
|
|
303
|
+
assistant_prefix="<|Assistant|>",
|
|
304
|
+
),
|
|
296
305
|
)
|
|
297
306
|
|
|
298
307
|
def get_stop_token_ids(self) -> list[int]:
|
|
@@ -312,6 +321,23 @@ class DeepSeekV3Renderer:
|
|
|
312
321
|
or reject_assistant_in_extension(new_messages)
|
|
313
322
|
):
|
|
314
323
|
return None
|
|
324
|
+
|
|
325
|
+
boundary = scan_reasoning(
|
|
326
|
+
self._tokenizer,
|
|
327
|
+
previous_completion_ids,
|
|
328
|
+
prompt_ids=previous_prompt_ids,
|
|
329
|
+
stop_ids=set(self.get_stop_token_ids()),
|
|
330
|
+
tool_start_id=self._tool_calls_begin,
|
|
331
|
+
assistant_prefix="<|Assistant|>",
|
|
332
|
+
)
|
|
333
|
+
if boundary.is_open:
|
|
334
|
+
if any(t in self.get_stop_token_ids() for t in previous_completion_ids):
|
|
335
|
+
return None
|
|
336
|
+
previous_completion_ids = [
|
|
337
|
+
*previous_completion_ids,
|
|
338
|
+
*list(self._tokenizer.encode("</think>", add_special_tokens=False)),
|
|
339
|
+
]
|
|
340
|
+
|
|
315
341
|
if should_rerender_for_thinking_retention(
|
|
316
342
|
self.effective_thinking_retention,
|
|
317
343
|
new_messages,
|
|
@@ -22,6 +22,8 @@ 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
|
+
|
|
25
27
|
from renderers.base import (
|
|
26
28
|
Message,
|
|
27
29
|
ParsedResponse,
|
|
@@ -783,13 +785,20 @@ class DeepSeekV4Renderer:
|
|
|
783
785
|
token_ids: list[int],
|
|
784
786
|
*,
|
|
785
787
|
tools: list[ToolSpec] | None = None, # noqa: ARG002
|
|
788
|
+
prompt_ids: list[int] | None = None,
|
|
786
789
|
) -> ParsedResponse:
|
|
787
790
|
return parse_deepseek_v4(
|
|
788
791
|
self._tokenizer,
|
|
789
792
|
token_ids,
|
|
790
793
|
stop_ids={self._eos},
|
|
791
|
-
thinking_enabled=
|
|
794
|
+
thinking_enabled=prompt_ends_in_reasoning(
|
|
795
|
+
self._tokenizer,
|
|
796
|
+
prompt_ids,
|
|
797
|
+
stop_ids=set(self.get_stop_token_ids()),
|
|
798
|
+
assistant_prefix="<|Assistant|>",
|
|
799
|
+
),
|
|
792
800
|
think_end_id=self._think_end,
|
|
801
|
+
think_start_id=self._think_start,
|
|
793
802
|
dsml_id=self._dsml,
|
|
794
803
|
)
|
|
795
804
|
|
|
@@ -810,6 +819,20 @@ class DeepSeekV4Renderer:
|
|
|
810
819
|
or reject_assistant_in_extension(new_messages)
|
|
811
820
|
):
|
|
812
821
|
return None
|
|
822
|
+
|
|
823
|
+
boundary = scan_reasoning(
|
|
824
|
+
self._tokenizer,
|
|
825
|
+
previous_completion_ids,
|
|
826
|
+
prompt_ids=previous_prompt_ids,
|
|
827
|
+
stop_ids=set(self.get_stop_token_ids()),
|
|
828
|
+
tool_start_id=self._dsml,
|
|
829
|
+
assistant_prefix="<|Assistant|>",
|
|
830
|
+
)
|
|
831
|
+
if boundary.is_open:
|
|
832
|
+
if any(t in self.get_stop_token_ids() for t in previous_completion_ids):
|
|
833
|
+
return None
|
|
834
|
+
previous_completion_ids = [*previous_completion_ids, self._think_end]
|
|
835
|
+
|
|
813
836
|
if should_rerender_for_thinking_retention(
|
|
814
837
|
self.effective_thinking_retention,
|
|
815
838
|
new_messages,
|
|
@@ -11,6 +11,8 @@ from __future__ import annotations
|
|
|
11
11
|
import json
|
|
12
12
|
from typing import Any
|
|
13
13
|
|
|
14
|
+
from renderers.reasoning import scan_reasoning, prompt_ends_in_reasoning
|
|
15
|
+
|
|
14
16
|
from renderers.base import (
|
|
15
17
|
ChatTemplateTokenizer,
|
|
16
18
|
Message,
|
|
@@ -20,8 +22,14 @@ from renderers.base import (
|
|
|
20
22
|
extract_message_tool_names,
|
|
21
23
|
resolve_thinking_retention,
|
|
22
24
|
)
|
|
25
|
+
from renderers.parsing import (
|
|
26
|
+
_reasoning_end_token_index,
|
|
27
|
+
_strip_stop_tokens,
|
|
28
|
+
)
|
|
29
|
+
|
|
23
30
|
from renderers.configs import DefaultRendererConfig
|
|
24
31
|
from renderers.parsers import (
|
|
32
|
+
ThinkTextReasoningParser,
|
|
25
33
|
get_reasoning_parser,
|
|
26
34
|
get_tool_parser,
|
|
27
35
|
)
|
|
@@ -176,50 +184,66 @@ class DefaultRenderer:
|
|
|
176
184
|
token_ids: list[int],
|
|
177
185
|
*,
|
|
178
186
|
tools: list[ToolSpec] | None = None, # noqa: ARG002 — DefaultRenderer relies on configured tool_parser, schema not consulted here
|
|
187
|
+
prompt_ids: list[int] | None = None,
|
|
179
188
|
) -> ParsedResponse:
|
|
189
|
+
ids = _strip_stop_tokens(token_ids, set(self.get_stop_token_ids()))
|
|
190
|
+
boundary = scan_reasoning(
|
|
191
|
+
self._tokenizer,
|
|
192
|
+
ids,
|
|
193
|
+
prefilled=prompt_ends_in_reasoning(
|
|
194
|
+
self._tokenizer,
|
|
195
|
+
prompt_ids,
|
|
196
|
+
stop_ids=set(self.get_stop_token_ids()),
|
|
197
|
+
),
|
|
198
|
+
)
|
|
199
|
+
if boundary.is_open:
|
|
200
|
+
return ParsedResponse(
|
|
201
|
+
content="", reasoning_content=boundary.text, reasoning_complete=False
|
|
202
|
+
)
|
|
203
|
+
reasoning_end = (
|
|
204
|
+
_reasoning_end_token_index(self._tokenizer, ids)
|
|
205
|
+
if boundary.text is not None
|
|
206
|
+
else 0
|
|
207
|
+
)
|
|
180
208
|
# 1. Extract tool calls while we still have token ids (most formats
|
|
181
209
|
# use special-token delimiters, so id-level matching is reliable).
|
|
182
210
|
if self._tool_parser is not None:
|
|
183
|
-
content_ids, tool_calls = self._tool_parser.extract(
|
|
211
|
+
content_ids, tool_calls = self._tool_parser.extract(ids[reasoning_end:])
|
|
212
|
+
content_ids = [*ids[:reasoning_end], *content_ids]
|
|
213
|
+
for tc in tool_calls:
|
|
214
|
+
if tc.token_span is not None:
|
|
215
|
+
start, end = tc.token_span
|
|
216
|
+
tc.token_span = (start + reasoning_end, end + reasoning_end)
|
|
184
217
|
else:
|
|
185
|
-
content_ids =
|
|
218
|
+
content_ids = ids
|
|
186
219
|
tool_calls = []
|
|
187
220
|
|
|
188
221
|
# 2. Decode (keep special tokens so a downstream reasoning parser can
|
|
189
222
|
# still see things like <think>/</think> when they're tokens).
|
|
190
223
|
text = self._tokenizer.decode(content_ids, skip_special_tokens=False)
|
|
191
224
|
|
|
192
|
-
#
|
|
193
|
-
#
|
|
194
|
-
#
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
# `</think>`, cascading through downstream tokenization and breaking
|
|
202
|
-
# the "extension property" in trajectory step tokenization.
|
|
203
|
-
if self._reasoning_parser is not None:
|
|
204
|
-
reasoning_content, text = self._reasoning_parser.extract(text)
|
|
225
|
+
# Use the same initial boundary for extraction and tool scanning.
|
|
226
|
+
# Preserve whitespace inside reasoning and after its close: templates
|
|
227
|
+
# may emit these fields back-to-back, so stripping shifts token offsets.
|
|
228
|
+
if self._reasoning_parser is None or isinstance(
|
|
229
|
+
self._reasoning_parser, ThinkTextReasoningParser
|
|
230
|
+
):
|
|
231
|
+
reasoning_content = boundary.text
|
|
232
|
+
if boundary.text is not None:
|
|
233
|
+
text = text.partition("</think>")[2]
|
|
205
234
|
else:
|
|
206
|
-
reasoning_content =
|
|
207
|
-
|
|
208
|
-
before, after = text.split("</think>", 1)
|
|
209
|
-
if "<think>" in before:
|
|
210
|
-
reasoning_content = before.split("<think>", 1)[-1]
|
|
211
|
-
else:
|
|
212
|
-
reasoning_content = before
|
|
213
|
-
text = after
|
|
214
|
-
|
|
215
|
-
# Strip any remaining special tokens from the final content (we kept
|
|
216
|
-
# them around for the reasoning parser above).
|
|
235
|
+
reasoning_content, text = self._reasoning_parser.extract(text)
|
|
236
|
+
|
|
217
237
|
text = _strip_special_tokens(self._tokenizer, text)
|
|
218
238
|
|
|
219
239
|
return ParsedResponse(
|
|
220
240
|
content=text,
|
|
221
241
|
reasoning_content=reasoning_content if reasoning_content else None,
|
|
222
242
|
tool_calls=tool_calls,
|
|
243
|
+
reasoning_complete=True
|
|
244
|
+
if isinstance(self._reasoning_parser, ThinkTextReasoningParser)
|
|
245
|
+
or reasoning_content is not None
|
|
246
|
+
else None,
|
|
223
247
|
)
|
|
224
248
|
|
|
225
249
|
def get_stop_token_ids(self) -> list[int]:
|
|
@@ -254,9 +278,9 @@ def _resolve_parser(value, tokenizer, factory):
|
|
|
254
278
|
|
|
255
279
|
|
|
256
280
|
def _strip_special_tokens(tokenizer, text: str) -> str:
|
|
257
|
-
"""Remove
|
|
281
|
+
"""Remove framing tokens while preserving literal think markers in content."""
|
|
258
282
|
specials = getattr(tokenizer, "all_special_tokens", None) or []
|
|
259
283
|
for token in specials:
|
|
260
|
-
if token and token in text:
|
|
284
|
+
if token and token not in {"<think>", "</think>"} and token in text:
|
|
261
285
|
text = text.replace(token, "")
|
|
262
286
|
return text
|