renderers 0.1.12.dev16__tar.gz → 0.1.12.dev18__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.dev16 → renderers-0.1.12.dev18}/.github/workflows/test.yml +1 -1
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/PKG-INFO +4 -3
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/README.md +2 -1
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/examples/sglang/multiturn_generate_sglang.py +1 -1
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/examples/sglang/online_multiturn_sglang.py +1 -1
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/examples/transformers/multiturn_generate_transformers.py +1 -1
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/examples/vllm/multiturn_generate_vllm.py +1 -1
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/pyproject.toml +1 -1
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/renderers/_version.py +2 -2
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/renderers/base.py +98 -50
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/renderers/configs.py +16 -16
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/renderers/glm45.py +11 -35
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/renderers/glm5.py +21 -32
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/tests/test_build_helpers.py +104 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/tests/test_message_indices.py +63 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/tests/test_parsers.py +10 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/tests/test_renderer_config.py +6 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/tests/test_sampled_mask.py +2 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/uv.lock +5 -288
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/.github/workflows/publish-dev.yml +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/.github/workflows/publish.yml +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/.github/workflows/style.yml +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/.gitignore +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/.pre-commit-config.yaml +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/LICENSE +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/docs/renderer-config.md +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/examples/README.md +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/examples/tinker/multiturn_generate_tinker.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/renderers/__init__.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/renderers/client.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/renderers/custom.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/renderers/deepseek_r1.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/renderers/deepseek_v3.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/renderers/deepseek_v4.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/renderers/default.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/renderers/gemma4.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/renderers/gpt_oss.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/renderers/hy3.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/renderers/inkling.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/renderers/kimi_k2.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/renderers/kimi_k25.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/renderers/laguna_s21.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/renderers/laguna_xs2.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/renderers/llama_3.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/renderers/minimax_m2.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/renderers/nemotron3.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/renderers/parsers.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/renderers/parsing.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/renderers/prime_qwen3.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/renderers/qwen3.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/renderers/qwen35.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/renderers/qwen36.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/renderers/qwen38.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/renderers/qwen3_vl.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/renderers/reasoning.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/tests/conftest.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/tests/parity.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/tests/reference_rendering.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/tests/test_bridge.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/tests/test_client.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/tests/test_custom.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/tests/test_deepseek_r1.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/tests/test_deepseek_v4.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/tests/test_disabled_thinking_stability.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/tests/test_gemma4.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/tests/test_glm_tool_name_validation.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/tests/test_hy3.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/tests/test_incremental.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/tests/test_inkling.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/tests/test_is_content.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/tests/test_kimi_k25_tool_schema.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/tests/test_laguna_m1.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/tests/test_laguna_xs21.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/tests/test_llama_3.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/tests/test_load_tokenizer.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/tests/test_message_tool_names.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/tests/test_multimodal.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/tests/test_nemotron35_vlm.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/tests/test_nemotron3_ultra.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/tests/test_offsetless_tokenizers.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/tests/test_parity.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/tests/test_parse_response.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/tests/test_parse_response_robustness.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/tests/test_preserve_thinking.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/tests/test_qwen35_size_coverage.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/tests/test_qwen38.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/tests/test_reasoning_boundaries.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/tests/test_roundtrip.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/tests/test_tokens_per_message.py +0 -0
- {renderers-0.1.12.dev16 → renderers-0.1.12.dev18}/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.dev18
|
|
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
|
|
@@ -127,7 +127,8 @@ class Renderer(Protocol):
|
|
|
127
127
|
def bridge_to_next_turn(prev_prompt_ids, prev_completion_ids, new_messages, *, tools=None) -> list[int] | None: ...
|
|
128
128
|
```
|
|
129
129
|
|
|
130
|
-
- `RenderedTokens` carries `token_ids` **and** `message_indices` — one entry per token attributing each to its
|
|
130
|
+
- `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.
|
|
131
|
+
- `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.
|
|
131
132
|
- `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.
|
|
132
133
|
- 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`.
|
|
133
134
|
|
|
@@ -107,7 +107,8 @@ class Renderer(Protocol):
|
|
|
107
107
|
def bridge_to_next_turn(prev_prompt_ids, prev_completion_ids, new_messages, *, tools=None) -> list[int] | None: ...
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
-
- `RenderedTokens` carries `token_ids` **and** `message_indices` — one entry per token attributing each to its
|
|
110
|
+
- `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.
|
|
111
|
+
- `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.
|
|
111
112
|
- `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.
|
|
112
113
|
- 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`.
|
|
113
114
|
|
|
@@ -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.dev18'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 12, 'dev18')
|
|
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
|
|
@@ -224,8 +223,11 @@ class MultiModalData:
|
|
|
224
223
|
class RenderedTokens:
|
|
225
224
|
"""Result of rendering messages to tokens.
|
|
226
225
|
|
|
227
|
-
Each token carries
|
|
228
|
-
build per-token loss masks without re-rendering.
|
|
226
|
+
Each token carries its owning message's index into the original message
|
|
227
|
+
list so callers can build per-token loss masks without re-rendering.
|
|
228
|
+
Sampled closing tokens belong to the assistant that generated them, even
|
|
229
|
+
when emitted while rendering the next message's role marker. User/tool
|
|
230
|
+
bodies retain their own message indices. Tokens from structural
|
|
229
231
|
scaffolding the renderer adds outside any single message (e.g. the
|
|
230
232
|
trailing generation prompt) carry index ``-1``.
|
|
231
233
|
|
|
@@ -383,7 +385,10 @@ class RenderedTokens:
|
|
|
383
385
|
is not represented.
|
|
384
386
|
|
|
385
387
|
Hand-coded renderers emit each message's tokens contiguously,
|
|
386
|
-
so the span is well-defined.
|
|
388
|
+
so the span is well-defined. Spans follow input-message indices,
|
|
389
|
+
not necessarily token order: templates can reorder tool results.
|
|
390
|
+
Empty messages whose role marker closes the preceding assistant
|
|
391
|
+
may have no tokens of their own. The implementation tolerates
|
|
387
392
|
non-contiguous attribution by returning the outer span
|
|
388
393
|
``(first_k, last_k + 1)``; if you suspect interleaving, slice
|
|
389
394
|
``message_indices`` yourself to verify.
|
|
@@ -556,7 +561,7 @@ class RenderedTokens:
|
|
|
556
561
|
return mask
|
|
557
562
|
|
|
558
563
|
|
|
559
|
-
class ToolCallParseStatus(
|
|
564
|
+
class ToolCallParseStatus(StrEnum):
|
|
560
565
|
"""Per-attempt outcome of parsing a single ``<tool_call>`` block.
|
|
561
566
|
|
|
562
567
|
The renderer parser's job is JSON-syntax → ``dict`` (the parser-level
|
|
@@ -1304,15 +1309,19 @@ def load_tokenizer(model_name_or_path: str):
|
|
|
1304
1309
|
the requested Meta ID so auto-resolution still selects ``Llama3Renderer``.
|
|
1305
1310
|
Requires the ``renderers[transformers]`` extra.
|
|
1306
1311
|
"""
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1312
|
+
try:
|
|
1313
|
+
_require_transformers("Loading a tokenizer")
|
|
1314
|
+
load_name_or_path = _tokenizer_source_for(model_name_or_path)
|
|
1315
|
+
kwargs = _tokenizer_load_kwargs(load_name_or_path)
|
|
1316
|
+
tok = _load_tokenizer_via_auto(load_name_or_path, **kwargs)
|
|
1317
|
+
return _preserve_requested_tokenizer_name(
|
|
1318
|
+
tok,
|
|
1319
|
+
requested_name_or_path=model_name_or_path,
|
|
1320
|
+
loaded_name_or_path=load_name_or_path,
|
|
1321
|
+
)
|
|
1322
|
+
except Exception as exc:
|
|
1323
|
+
exc.add_note(f"While loading tokenizer {model_name_or_path!r}.")
|
|
1324
|
+
raise
|
|
1316
1325
|
|
|
1317
1326
|
|
|
1318
1327
|
def _populate_registry():
|
|
@@ -1387,7 +1396,7 @@ def _populate_registry():
|
|
|
1387
1396
|
|
|
1388
1397
|
def create_renderer(
|
|
1389
1398
|
tokenizer,
|
|
1390
|
-
config: RendererConfig | None = None,
|
|
1399
|
+
config: "RendererConfig | None" = None,
|
|
1391
1400
|
*,
|
|
1392
1401
|
chat_template_kwargs: Mapping[str, Any] | None = None,
|
|
1393
1402
|
) -> Renderer:
|
|
@@ -1419,26 +1428,34 @@ def create_renderer(
|
|
|
1419
1428
|
"""
|
|
1420
1429
|
_populate_registry()
|
|
1421
1430
|
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1431
|
+
try:
|
|
1432
|
+
config = _resolve_renderer_config(
|
|
1433
|
+
tokenizer,
|
|
1434
|
+
config,
|
|
1435
|
+
chat_template_kwargs=chat_template_kwargs,
|
|
1436
|
+
)
|
|
1437
|
+
from renderers.configs import CustomRendererConfig
|
|
1438
|
+
from renderers.custom import custom_renderer_config, load_custom_renderer
|
|
1439
|
+
|
|
1440
|
+
if isinstance(config, CustomRendererConfig):
|
|
1441
|
+
renderer_cls = load_custom_renderer(config.import_path)
|
|
1442
|
+
return renderer_cls(tokenizer, custom_renderer_config(config))
|
|
1443
|
+
cls = RENDERER_REGISTRY.get(config.name)
|
|
1444
|
+
if cls is None:
|
|
1445
|
+
raise ValueError(
|
|
1446
|
+
f"Unknown renderer {config.name!r}. Available: {', '.join(sorted(RENDERER_REGISTRY))}"
|
|
1447
|
+
)
|
|
1448
|
+
return cls(tokenizer, config)
|
|
1449
|
+
except Exception as exc:
|
|
1450
|
+
model_name = getattr(tokenizer, "name_or_path", "<unnamed tokenizer>")
|
|
1451
|
+
renderer_name = config.name if config is not None else "auto"
|
|
1452
|
+
exc.add_note(
|
|
1453
|
+
f"While creating renderer {renderer_name!r} for tokenizer {model_name!r}."
|
|
1437
1454
|
)
|
|
1438
|
-
|
|
1455
|
+
raise
|
|
1439
1456
|
|
|
1440
1457
|
|
|
1441
|
-
def template_field_names(config: RendererConfig) -> frozenset[str]:
|
|
1458
|
+
def template_field_names(config: "RendererConfig") -> frozenset[str]:
|
|
1442
1459
|
"""Chat-template kwargs that ``config``'s renderer accepts, custom renderers included."""
|
|
1443
1460
|
from renderers.configs import CustomRendererConfig
|
|
1444
1461
|
from renderers.custom import load_custom_renderer
|
|
@@ -1451,9 +1468,9 @@ def template_field_names(config: RendererConfig) -> frozenset[str]:
|
|
|
1451
1468
|
|
|
1452
1469
|
|
|
1453
1470
|
def merge_chat_template_kwargs(
|
|
1454
|
-
config: RendererConfig,
|
|
1471
|
+
config: "RendererConfig",
|
|
1455
1472
|
chat_template_kwargs: Mapping[str, Any] | None,
|
|
1456
|
-
) -> RendererConfig:
|
|
1473
|
+
) -> "RendererConfig":
|
|
1457
1474
|
"""Return ``config`` with the template kwargs applied, validated against its allowlist."""
|
|
1458
1475
|
if not chat_template_kwargs:
|
|
1459
1476
|
return config
|
|
@@ -1498,10 +1515,10 @@ def merge_chat_template_kwargs(
|
|
|
1498
1515
|
|
|
1499
1516
|
def _resolve_renderer_config(
|
|
1500
1517
|
tokenizer,
|
|
1501
|
-
config: RendererConfig | None,
|
|
1518
|
+
config: "RendererConfig | None",
|
|
1502
1519
|
*,
|
|
1503
1520
|
chat_template_kwargs: Mapping[str, Any] | None = None,
|
|
1504
|
-
) -> RendererConfig:
|
|
1521
|
+
) -> "RendererConfig":
|
|
1505
1522
|
"""Resolve auto/default config and merge chat-template kwargs."""
|
|
1506
1523
|
from renderers.configs import AutoRendererConfig
|
|
1507
1524
|
|
|
@@ -1520,10 +1537,10 @@ def _resolve_renderer_config(
|
|
|
1520
1537
|
|
|
1521
1538
|
def _resolve_auto_config(
|
|
1522
1539
|
tokenizer,
|
|
1523
|
-
auto: AutoRendererConfig,
|
|
1540
|
+
auto: "AutoRendererConfig",
|
|
1524
1541
|
*,
|
|
1525
1542
|
chat_template_kwargs: Mapping[str, Any] | None = None,
|
|
1526
|
-
) -> RendererConfig:
|
|
1543
|
+
) -> "RendererConfig":
|
|
1527
1544
|
"""Map ``AutoRendererConfig`` → concrete typed config via the
|
|
1528
1545
|
tokenizer's ``name_or_path``.
|
|
1529
1546
|
|
|
@@ -1640,6 +1657,7 @@ def build_training_sample(
|
|
|
1640
1657
|
tools: list[ToolSpec] | None = None,
|
|
1641
1658
|
content_sft_roles: "set[str] | frozenset[str] | None" = None,
|
|
1642
1659
|
ensure_final_stop: bool = False,
|
|
1660
|
+
message_loss_mask: list[bool | int] | None = None,
|
|
1643
1661
|
) -> RenderedTrainingSample:
|
|
1644
1662
|
"""Build a :class:`RenderedTrainingSample` for supervised training.
|
|
1645
1663
|
|
|
@@ -1656,11 +1674,8 @@ def build_training_sample(
|
|
|
1656
1674
|
it's attributed to. This is the recommended default for renderer
|
|
1657
1675
|
callers — the renderer owns the per-token "is this model output"
|
|
1658
1676
|
signal, so role-level filtering becomes a downstream constraint
|
|
1659
|
-
rather than a precondition.
|
|
1660
|
-
``<|user|>`` / ``<|observation
|
|
1661
|
-
turn — *are* sampled by the model at inference and live inside the
|
|
1662
|
-
next message's span; ``sampled_mask`` captures that, but a
|
|
1663
|
-
naive role filter would mask them out.)
|
|
1677
|
+
rather than a precondition. Sampled closing markers, including GLM's
|
|
1678
|
+
``<|user|>`` / ``<|observation|>``, are attributed to their assistant.
|
|
1664
1679
|
|
|
1665
1680
|
When ``role_to_mask`` is provided, ``loss_mask`` is the AND of the
|
|
1666
1681
|
role-based attribution and the sampled signal: only tokens the
|
|
@@ -1706,7 +1721,27 @@ def build_training_sample(
|
|
|
1706
1721
|
fires, the output intentionally diverges from ``apply_chat_template``.
|
|
1707
1722
|
Ignored for renderers without ``sampled_mask`` (``DefaultRenderer``) —
|
|
1708
1723
|
the close of an opaque template can't be located reliably.
|
|
1724
|
+
|
|
1725
|
+
``message_loss_mask`` optionally selects messages for supervision with
|
|
1726
|
+
one boolean or integer 0/1 per message. It only restricts the existing
|
|
1727
|
+
mask: selecting a message never enables targets disabled by the role,
|
|
1728
|
+
sampled, or content settings. Unselected messages remain context,
|
|
1729
|
+
including prefilled assistant turns at arbitrary positions. Sampled
|
|
1730
|
+
closing tokens follow their owning assistant, even when a template
|
|
1731
|
+
emits them at the next message boundary. A synthesized final stop
|
|
1732
|
+
follows the final message's selection. Token ids are unaffected.
|
|
1733
|
+
``None`` preserves the existing mask.
|
|
1709
1734
|
"""
|
|
1735
|
+
if message_loss_mask is not None:
|
|
1736
|
+
if len(message_loss_mask) != len(messages):
|
|
1737
|
+
raise ValueError("message_loss_mask must have one entry per message")
|
|
1738
|
+
if any(
|
|
1739
|
+
not isinstance(value, (bool, int)) or value not in (0, 1)
|
|
1740
|
+
for value in message_loss_mask
|
|
1741
|
+
):
|
|
1742
|
+
raise ValueError(
|
|
1743
|
+
"message_loss_mask entries must be booleans or integers 0/1"
|
|
1744
|
+
)
|
|
1710
1745
|
rendered = renderer.render(messages, tools=tools)
|
|
1711
1746
|
has_sampled_info = len(rendered.sampled_mask) == len(rendered.token_ids)
|
|
1712
1747
|
has_content_info = len(rendered.is_content) == len(rendered.token_ids)
|
|
@@ -1765,6 +1800,19 @@ def build_training_sample(
|
|
|
1765
1800
|
# stop is a training target, like any sampled token.
|
|
1766
1801
|
loss_mask.append(True)
|
|
1767
1802
|
|
|
1803
|
+
if message_loss_mask is not None:
|
|
1804
|
+
for k in range(len(loss_mask)):
|
|
1805
|
+
# Any synthesized stop after the rendered sequence belongs to
|
|
1806
|
+
# the final assistant, just like an in-message closing token.
|
|
1807
|
+
msg_idx = (
|
|
1808
|
+
rendered.message_indices[k]
|
|
1809
|
+
if k < len(rendered.message_indices)
|
|
1810
|
+
else len(messages) - 1
|
|
1811
|
+
)
|
|
1812
|
+
loss_mask[k] = (
|
|
1813
|
+
loss_mask[k] and msg_idx >= 0 and bool(message_loss_mask[msg_idx])
|
|
1814
|
+
)
|
|
1815
|
+
|
|
1768
1816
|
# Surface the multimodal payload for VLM renderers. ``None`` for text
|
|
1769
1817
|
# renderers and for text-only samples (empty media) so downstream
|
|
1770
1818
|
# ``multi_modal_data is not None`` is a reliable "has media" check.
|
|
@@ -2080,8 +2128,8 @@ def introduces_user_query(
|
|
|
2080
2128
|
|
|
2081
2129
|
def resolve_thinking_retention(
|
|
2082
2130
|
config: Any,
|
|
2083
|
-
implied: ResolvedThinkingRetention,
|
|
2084
|
-
) -> ResolvedThinkingRetention:
|
|
2131
|
+
implied: "ResolvedThinkingRetention",
|
|
2132
|
+
) -> "ResolvedThinkingRetention":
|
|
2085
2133
|
"""Resolve the effective bridge policy for a renderer instance.
|
|
2086
2134
|
|
|
2087
2135
|
``config.thinking_retention is None`` means "derive from template knobs";
|
|
@@ -2095,7 +2143,7 @@ def resolve_thinking_retention(
|
|
|
2095
2143
|
|
|
2096
2144
|
|
|
2097
2145
|
def should_rerender_for_thinking_retention(
|
|
2098
|
-
thinking_retention: ResolvedThinkingRetention,
|
|
2146
|
+
thinking_retention: "ResolvedThinkingRetention",
|
|
2099
2147
|
new_messages: list[Message],
|
|
2100
2148
|
*,
|
|
2101
2149
|
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
|
|
@@ -192,20 +192,8 @@ class GLM45Renderer:
|
|
|
192
192
|
role = msg["role"]
|
|
193
193
|
content = self._visible_text(msg.get("content"))
|
|
194
194
|
|
|
195
|
-
#
|
|
196
|
-
#
|
|
197
|
-
# the inference-time stop signal that closes the assistant's
|
|
198
|
-
# turn (see ``get_stop_token_ids``). Mark it
|
|
199
|
-
# ``is_sampled=True`` so the loss-mask pipeline trains the
|
|
200
|
-
# model to emit it after ``</tool_call>`` (instead of
|
|
201
|
-
# continuing with another ``<tool_call>`` block). The token
|
|
202
|
-
# stays attributed to this message (msg_idx=i) and remains
|
|
203
|
-
# ``is_content=False`` — it's a role-marker / scaffold, not
|
|
204
|
-
# body bytes, so ``content_mask_for_roles({"tool"})`` and
|
|
205
|
-
# ``content_token_spans_by_role()`` correctly exclude it
|
|
206
|
-
# from "tool body" views. Byte stream is unchanged.
|
|
207
|
-
# ``system`` only appears at the start of a GLM conversation,
|
|
208
|
-
# so its opener is never the closer of an assistant turn.
|
|
195
|
+
# A user/tool role marker that closes generation belongs to
|
|
196
|
+
# the preceding assistant's sampled output, not the next body.
|
|
209
197
|
closes_assistant_turn = i > 0 and messages[i - 1]["role"] == "assistant"
|
|
210
198
|
|
|
211
199
|
if role == "system":
|
|
@@ -219,9 +207,9 @@ class GLM45Renderer:
|
|
|
219
207
|
elif role == "user":
|
|
220
208
|
emit_special(
|
|
221
209
|
self._user,
|
|
222
|
-
i,
|
|
210
|
+
i - 1 if closes_assistant_turn else i,
|
|
223
211
|
is_sampled=closes_assistant_turn,
|
|
224
|
-
is_content=
|
|
212
|
+
is_content=closes_assistant_turn,
|
|
225
213
|
)
|
|
226
214
|
# ``\n`` is scaffold; ``content`` is body; the optional
|
|
227
215
|
# ``/nothink`` suffix is scaffold the renderer injects
|
|
@@ -508,15 +496,9 @@ class GLM45Renderer:
|
|
|
508
496
|
# (think block + content + tool calls) is the model-sampled
|
|
509
497
|
# portion.
|
|
510
498
|
#
|
|
511
|
-
#
|
|
512
|
-
#
|
|
513
|
-
|
|
514
|
-
# as the stop signal at inference, and those tokens are
|
|
515
|
-
# attributed to the *next* message (or are absent on the final
|
|
516
|
-
# turn). So no sampled stop-signal token lives inside this
|
|
517
|
-
# assistant span — content / think / tool_calls carry the
|
|
518
|
-
# is_sampled=True signal.
|
|
519
|
-
#
|
|
499
|
+
# The next user/tool role marker closes this assistant turn and
|
|
500
|
+
# is attributed here when that later message is rendered.
|
|
501
|
+
|
|
520
502
|
# Invariant on assistant tokens: ``is_content == sampled_mask``.
|
|
521
503
|
# Every scaffold token here gets ``is_sampled=False/is_content=False``;
|
|
522
504
|
# every model-sampled emit gets both True.
|
|
@@ -598,23 +580,17 @@ class GLM45Renderer:
|
|
|
598
580
|
emit_text,
|
|
599
581
|
emit_text_segments,
|
|
600
582
|
) -> None:
|
|
601
|
-
# Tool
|
|
602
|
-
#
|
|
603
|
-
# (``is_content=False`` so ``content_mask_for_roles({"tool"})``
|
|
604
|
-
# excludes it). When the previous message is an assistant it
|
|
605
|
-
# doubles as the inference stop signal for that assistant's
|
|
606
|
-
# turn — mark it ``is_sampled=True`` so SFT trains the model to
|
|
607
|
-
# emit it after ``</tool_call>``. The token stays attributed to
|
|
608
|
-
# this tool message; byte stream is unchanged.
|
|
583
|
+
# Tool bodies keep their own attribution; a sampled observation
|
|
584
|
+
# marker belongs to the assistant that generated it.
|
|
609
585
|
prev_role = messages[msg_idx - 1]["role"] if msg_idx > 0 else None
|
|
610
586
|
closes_assistant_turn = prev_role == "assistant"
|
|
611
587
|
|
|
612
588
|
if prev_role != "tool":
|
|
613
589
|
emit_special(
|
|
614
590
|
self._observation,
|
|
615
|
-
msg_idx,
|
|
591
|
+
msg_idx - 1 if closes_assistant_turn else msg_idx,
|
|
616
592
|
is_sampled=closes_assistant_turn,
|
|
617
|
-
is_content=
|
|
593
|
+
is_content=closes_assistant_turn,
|
|
618
594
|
)
|
|
619
595
|
|
|
620
596
|
emit_text_segments(
|