renderers 0.1.11.dev2__tar.gz → 0.1.11.dev3__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.dev2 → renderers-0.1.11.dev3}/PKG-INFO +8 -6
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/README.md +7 -5
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/pyproject.toml +2 -2
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/renderers/__init__.py +2 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/renderers/_version.py +2 -2
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/renderers/base.py +141 -34
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/renderers/deepseek_v3.py +5 -2
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/renderers/gemma4.py +3 -2
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/renderers/glm45.py +5 -2
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/renderers/glm5.py +5 -2
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/renderers/gpt_oss.py +5 -2
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/renderers/hy3.py +43 -11
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/renderers/inkling.py +3 -2
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/renderers/kimi_k2.py +5 -2
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/renderers/kimi_k25.py +4 -3
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/renderers/laguna_xs2.py +53 -7
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/renderers/llama_3.py +5 -2
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/renderers/minimax_m2.py +20 -6
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/renderers/nemotron3.py +5 -2
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/renderers/prime_qwen3.py +14 -4
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/renderers/qwen3.py +5 -2
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/renderers/qwen35.py +4 -3
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/renderers/qwen3_vl.py +5 -4
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/tests/test_load_tokenizer.py +16 -11
- renderers-0.1.11.dev3/tests/test_offsetless_tokenizers.py +223 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/.github/workflows/publish-dev.yml +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/.github/workflows/publish.yml +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/.github/workflows/style.yml +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/.github/workflows/test.yml +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/.gitignore +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/.pre-commit-config.yaml +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/LICENSE +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/docs/renderer-config.md +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/examples/README.md +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/examples/sglang/multiturn_generate_sglang.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/examples/sglang/online_multiturn_sglang.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/examples/tinker/multiturn_generate_tinker.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/examples/transformers/multiturn_generate_transformers.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/examples/vllm/multiturn_generate_vllm.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/renderers/client.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/renderers/configs.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/renderers/deepseek_r1.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/renderers/default.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/renderers/laguna_s21.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/renderers/parsers.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/renderers/parsing.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/renderers/qwen36.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/renderers/qwen38.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/tests/conftest.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/tests/test_bridge.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/tests/test_build_helpers.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/tests/test_client.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/tests/test_deepseek_r1.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/tests/test_disabled_thinking_stability.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/tests/test_gemma4.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/tests/test_glm_tool_name_validation.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/tests/test_gpt_oss_harmony_parity.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/tests/test_hy3.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/tests/test_incremental.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/tests/test_inkling.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/tests/test_is_content.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/tests/test_kimi_k25_tool_schema.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/tests/test_laguna_m1.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/tests/test_laguna_s21.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/tests/test_laguna_xs21.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/tests/test_llama_3.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/tests/test_message_indices.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/tests/test_message_tool_names.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/tests/test_multimodal.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/tests/test_nemotron3_parity.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/tests/test_nemotron3_ultra.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/tests/test_parse_response.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/tests/test_parse_response_robustness.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/tests/test_parsers.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/tests/test_preserve_thinking.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/tests/test_prime_qwen3_parity.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/tests/test_qwen35_size_coverage.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/tests/test_qwen38.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/tests/test_render_ids.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/tests/test_renderer_config.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/tests/test_renderer_config_parity.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/tests/test_roundtrip.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/tests/test_sampled_mask.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/tests/test_tokens_per_message.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/tests/test_tool_arg_type_preservation.py +0 -0
- {renderers-0.1.11.dev2 → renderers-0.1.11.dev3}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: renderers
|
|
3
|
-
Version: 0.1.11.
|
|
3
|
+
Version: 0.1.11.dev3
|
|
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
|
|
@@ -39,11 +39,13 @@ uv add 'renderers[transformers]'
|
|
|
39
39
|
uv add 'renderers[multimodal]'
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
A BYO tokenizer must expose `encode`, `decode`, `convert_tokens_to_ids`,
|
|
43
|
-
IDs such as `eos_token_id
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
A BYO tokenizer must expose `encode`, `decode`, `convert_tokens_to_ids`, and
|
|
43
|
+
token IDs such as `eos_token_id`. Character offsets are optional: tokenizers
|
|
44
|
+
supporting `return_offsets_mapping=True` also receive precise per-token
|
|
45
|
+
`is_content` attribution; without offsets, renderers return `is_content=[]`.
|
|
46
|
+
`DefaultRenderer` additionally requires `apply_chat_template`. This includes
|
|
47
|
+
text-only Inkling training: `InklingRenderer` loads its Transformers processor
|
|
48
|
+
only when image or audio content is actually rendered.
|
|
47
49
|
|
|
48
50
|
## At a glance
|
|
49
51
|
|
|
@@ -19,11 +19,13 @@ uv add 'renderers[transformers]'
|
|
|
19
19
|
uv add 'renderers[multimodal]'
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
A BYO tokenizer must expose `encode`, `decode`, `convert_tokens_to_ids`,
|
|
23
|
-
IDs such as `eos_token_id
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
A BYO tokenizer must expose `encode`, `decode`, `convert_tokens_to_ids`, and
|
|
23
|
+
token IDs such as `eos_token_id`. Character offsets are optional: tokenizers
|
|
24
|
+
supporting `return_offsets_mapping=True` also receive precise per-token
|
|
25
|
+
`is_content` attribution; without offsets, renderers return `is_content=[]`.
|
|
26
|
+
`DefaultRenderer` additionally requires `apply_chat_template`. This includes
|
|
27
|
+
text-only Inkling training: `InklingRenderer` loads its Transformers processor
|
|
28
|
+
only when image or audio content is actually rendered.
|
|
27
29
|
|
|
28
30
|
## At a glance
|
|
29
31
|
|
|
@@ -39,8 +39,8 @@ dependencies = [
|
|
|
39
39
|
|
|
40
40
|
[project.optional-dependencies]
|
|
41
41
|
# Tokenizer loading uses Hugging Face. Text-only renderers can instead be
|
|
42
|
-
# constructed with
|
|
43
|
-
# dependency.
|
|
42
|
+
# constructed with a compatible BYO tokenizer and do not import this
|
|
43
|
+
# dependency. Character offsets are optional.
|
|
44
44
|
transformers = [
|
|
45
45
|
# Keep this floor compatible with prime-rl's transformers pin. Inkling's
|
|
46
46
|
# tokenizer and text-only renderer work on older releases; image/audio
|
|
@@ -15,6 +15,7 @@ from renderers.base import (
|
|
|
15
15
|
Message,
|
|
16
16
|
MultiModalData,
|
|
17
17
|
MultimodalRenderer,
|
|
18
|
+
OffsetTokenizer,
|
|
18
19
|
ParsedResponse,
|
|
19
20
|
ParsedToolCall,
|
|
20
21
|
PlaceholderRange,
|
|
@@ -181,6 +182,7 @@ __all__ = [
|
|
|
181
182
|
"Nemotron3RendererConfig",
|
|
182
183
|
"Nemotron3UltraRenderer",
|
|
183
184
|
"Nemotron3UltraRendererConfig",
|
|
185
|
+
"OffsetTokenizer",
|
|
184
186
|
"OverlongPromptError",
|
|
185
187
|
"ParsedResponse",
|
|
186
188
|
"ParsedToolCall",
|
|
@@ -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.11.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 1, 11, '
|
|
21
|
+
__version__ = version = '0.1.11.dev3'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 11, 'dev3')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -11,6 +11,7 @@ from typing import (
|
|
|
11
11
|
Literal,
|
|
12
12
|
Protocol,
|
|
13
13
|
TypedDict,
|
|
14
|
+
cast,
|
|
14
15
|
runtime_checkable,
|
|
15
16
|
)
|
|
16
17
|
|
|
@@ -243,7 +244,9 @@ class RenderedTokens:
|
|
|
243
244
|
Empty ``sampled_mask`` (``[]``) means the renderer doesn't provide
|
|
244
245
|
this signal — consumers should fall back to attribution-only
|
|
245
246
|
masking. ``DefaultRenderer`` leaves it empty because the Jinja
|
|
246
|
-
template is opaque
|
|
247
|
+
template is opaque. Hand-coded renderers normally populate it; a
|
|
248
|
+
renderer whose sampled/scaffold boundary depends on character
|
|
249
|
+
attribution may leave it empty for an offsetless tokenizer.
|
|
247
250
|
|
|
248
251
|
``is_content`` is a per-token signal generalizing the "scaffold vs
|
|
249
252
|
body" distinction across all roles: ``True`` iff the token was
|
|
@@ -269,7 +272,8 @@ class RenderedTokens:
|
|
|
269
272
|
|
|
270
273
|
Empty ``is_content`` (``[]``) — like ``sampled_mask`` — means the
|
|
271
274
|
renderer doesn't provide the signal. ``DefaultRenderer`` leaves it
|
|
272
|
-
empty
|
|
275
|
+
empty because its Jinja template is opaque; all renderers leave it
|
|
276
|
+
empty when the supplied tokenizer cannot return character offsets.
|
|
273
277
|
|
|
274
278
|
``message_tool_names`` is the per-message tool function name list,
|
|
275
279
|
parallel to ``message_roles`` (same length). For tool-role
|
|
@@ -476,7 +480,7 @@ class RenderedTokens:
|
|
|
476
480
|
|
|
477
481
|
Returns an empty dict when :attr:`is_content` or
|
|
478
482
|
:attr:`message_roles` is empty (renderer didn't populate the
|
|
479
|
-
signal — e.g. ``DefaultRenderer``).
|
|
483
|
+
signal — e.g. ``DefaultRenderer`` or an offsetless tokenizer).
|
|
480
484
|
|
|
481
485
|
Intended for selective loss masking: SFT on tool response
|
|
482
486
|
bodies while RL acts only on assistant turns is the canonical
|
|
@@ -667,8 +671,8 @@ class Tokenizer(Protocol):
|
|
|
667
671
|
Hugging Face tokenizers satisfy this protocol, as can lightweight BYO
|
|
668
672
|
adapters around ``tokenizers.Tokenizer`` or another tokenizer backend.
|
|
669
673
|
Keeping the renderer-facing contract here makes ``transformers`` optional
|
|
670
|
-
for text rendering.
|
|
671
|
-
:
|
|
674
|
+
for text rendering. Character offsets are a separate optional capability;
|
|
675
|
+
see :class:`OffsetTokenizer`.
|
|
672
676
|
"""
|
|
673
677
|
|
|
674
678
|
name_or_path: str
|
|
@@ -681,6 +685,17 @@ class Tokenizer(Protocol):
|
|
|
681
685
|
|
|
682
686
|
def convert_tokens_to_ids(self, tokens: Any) -> Any: ...
|
|
683
687
|
|
|
688
|
+
|
|
689
|
+
@runtime_checkable
|
|
690
|
+
class OffsetTokenizer(Tokenizer, Protocol):
|
|
691
|
+
"""Tokenizer that can return character offsets alongside token IDs.
|
|
692
|
+
|
|
693
|
+
Hand-coded renderers use this optional capability to distinguish caller
|
|
694
|
+
content from adjacent template scaffold without changing the underlying
|
|
695
|
+
BPE pass. A basic :class:`Tokenizer` remains sufficient for rendering token
|
|
696
|
+
IDs; when offsets are unavailable, renderers leave ``is_content`` empty.
|
|
697
|
+
"""
|
|
698
|
+
|
|
684
699
|
def __call__(self, *args: Any, **kwargs: Any) -> Any: ...
|
|
685
700
|
|
|
686
701
|
|
|
@@ -1064,7 +1079,7 @@ _TRANSFORMERS_INSTALL_HINT = (
|
|
|
1064
1079
|
"Install the optional dependency with "
|
|
1065
1080
|
"`pip install 'renderers[transformers]'` (or "
|
|
1066
1081
|
"`uv add 'renderers[transformers]'`). Text-only renderers work without "
|
|
1067
|
-
"it when constructed with
|
|
1082
|
+
"it when constructed with a compatible tokenizer object."
|
|
1068
1083
|
)
|
|
1069
1084
|
|
|
1070
1085
|
|
|
@@ -1759,36 +1774,122 @@ def trim_to_turn_close(
|
|
|
1759
1774
|
return previous_ids
|
|
1760
1775
|
|
|
1761
1776
|
|
|
1762
|
-
|
|
1763
|
-
"""
|
|
1777
|
+
class AttributedTextSegments(list[tuple[int, bool]]):
|
|
1778
|
+
"""Token/content pairs with an explicit attribution-availability flag."""
|
|
1779
|
+
|
|
1780
|
+
def __init__(
|
|
1781
|
+
self,
|
|
1782
|
+
values=(),
|
|
1783
|
+
*,
|
|
1784
|
+
has_content_attribution: bool,
|
|
1785
|
+
) -> None:
|
|
1786
|
+
super().__init__(values)
|
|
1787
|
+
self.has_content_attribution = has_content_attribution
|
|
1788
|
+
|
|
1789
|
+
|
|
1790
|
+
def _get_offset_tokenizer(tokenizer: Tokenizer) -> OffsetTokenizer | None:
|
|
1791
|
+
"""Return ``tokenizer`` when it supports character offsets, else ``None``.
|
|
1764
1792
|
|
|
1765
1793
|
Hand-coded renderers concatenate scaffold + body in one BPE pass to
|
|
1766
1794
|
preserve cross-boundary merges, then attribute each resulting token
|
|
1767
1795
|
back to its source segment via the fast tokenizer's
|
|
1768
|
-
``offset_mapping`` (see :func:`attribute_text_segments`).
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
``PreTrainedTokenizerFast`` instances that satisfy this trivially.
|
|
1796
|
+
``offset_mapping`` (see :func:`attribute_text_segments`). Tokenizers
|
|
1797
|
+
loaded via :func:`load_tokenizer` are ``PreTrainedTokenizerFast``
|
|
1798
|
+
instances that satisfy this capability, but BYO tokenizers need not.
|
|
1772
1799
|
"""
|
|
1800
|
+
call = getattr(tokenizer, "__call__", None)
|
|
1801
|
+
if not callable(call):
|
|
1802
|
+
return None
|
|
1773
1803
|
try:
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1804
|
+
encoding = call("a", add_special_tokens=False, return_offsets_mapping=True)
|
|
1805
|
+
encoding["input_ids"]
|
|
1806
|
+
encoding["offset_mapping"]
|
|
1807
|
+
except (KeyError, NotImplementedError, TypeError, ValueError):
|
|
1808
|
+
return None
|
|
1809
|
+
return cast(OffsetTokenizer, tokenizer)
|
|
1810
|
+
|
|
1811
|
+
|
|
1812
|
+
def _infer_offsets_from_decode(
|
|
1813
|
+
tokenizer: Tokenizer,
|
|
1814
|
+
token_ids: list[int],
|
|
1815
|
+
text: str,
|
|
1816
|
+
) -> list[tuple[int, int]] | None:
|
|
1817
|
+
"""Recover token character spans from an exact decoder round-trip.
|
|
1818
|
+
|
|
1819
|
+
This is a narrow fallback for metadata that does not require exposing
|
|
1820
|
+
content attribution. Some renderers join text from multiple messages in a
|
|
1821
|
+
single BPE pass, so they still need to associate the resulting tokens with
|
|
1822
|
+
the right message when a BYO tokenizer has no native offset mapping.
|
|
1823
|
+
|
|
1824
|
+
Decoding individual tokens is linear and exact for the common BPE/SentencePiece
|
|
1825
|
+
backends. Byte-fallback tokenizers can require multiple tokens before text
|
|
1826
|
+
becomes valid, so a validated cumulative-prefix pass handles that case.
|
|
1827
|
+
If either strategy cannot reconstruct ``text`` exactly, callers must use a
|
|
1828
|
+
conservative renderer-specific message-index fallback. This helper never
|
|
1829
|
+
upgrades the tokenizer's content-attribution capability: ``is_content``
|
|
1830
|
+
remains unavailable without native offsets.
|
|
1831
|
+
"""
|
|
1832
|
+
|
|
1833
|
+
def decode(ids: list[int]) -> str | None:
|
|
1834
|
+
variants = (
|
|
1835
|
+
{"skip_special_tokens": False, "clean_up_tokenization_spaces": False},
|
|
1836
|
+
{"skip_special_tokens": False},
|
|
1837
|
+
{},
|
|
1838
|
+
)
|
|
1839
|
+
for kwargs in variants:
|
|
1840
|
+
try:
|
|
1841
|
+
decoded = tokenizer.decode(ids, **kwargs)
|
|
1842
|
+
except TypeError:
|
|
1843
|
+
continue
|
|
1844
|
+
except (KeyError, NotImplementedError, UnicodeError, ValueError):
|
|
1845
|
+
return None
|
|
1846
|
+
return decoded if isinstance(decoded, str) else None
|
|
1847
|
+
return None
|
|
1848
|
+
|
|
1849
|
+
pieces: list[str] = []
|
|
1850
|
+
for token_id in token_ids:
|
|
1851
|
+
piece = decode([token_id])
|
|
1852
|
+
if piece is None:
|
|
1853
|
+
break
|
|
1854
|
+
pieces.append(piece)
|
|
1855
|
+
if len(pieces) == len(token_ids) and "".join(pieces) == text:
|
|
1856
|
+
offsets: list[tuple[int, int]] = []
|
|
1857
|
+
position = 0
|
|
1858
|
+
for piece in pieces:
|
|
1859
|
+
end = position + len(piece)
|
|
1860
|
+
offsets.append((position, end))
|
|
1861
|
+
position = end
|
|
1862
|
+
return offsets
|
|
1863
|
+
|
|
1864
|
+
offsets = []
|
|
1865
|
+
previous_end = 0
|
|
1866
|
+
for end_index in range(1, len(token_ids) + 1):
|
|
1867
|
+
prefix = decode(token_ids[:end_index])
|
|
1868
|
+
if prefix is None or len(prefix) < previous_end or not text.startswith(prefix):
|
|
1869
|
+
return None
|
|
1870
|
+
current_end = len(prefix)
|
|
1871
|
+
offsets.append((previous_end, current_end))
|
|
1872
|
+
previous_end = current_end
|
|
1873
|
+
if previous_end != len(text):
|
|
1874
|
+
return None
|
|
1875
|
+
return offsets
|
|
1876
|
+
|
|
1877
|
+
|
|
1878
|
+
def _content_mask_or_empty(
|
|
1879
|
+
tokenizer: Tokenizer, content_mask: list[bool]
|
|
1880
|
+
) -> list[bool]:
|
|
1881
|
+
"""Return exact content attribution, or the empty-list unavailable sentinel."""
|
|
1882
|
+
if _get_offset_tokenizer(tokenizer) is None:
|
|
1883
|
+
return []
|
|
1884
|
+
return content_mask
|
|
1784
1885
|
|
|
1785
1886
|
|
|
1786
1887
|
def attribute_text_segments(
|
|
1787
|
-
tokenizer,
|
|
1888
|
+
tokenizer: Tokenizer,
|
|
1788
1889
|
segments: "list[tuple[str, bool]]",
|
|
1789
1890
|
*,
|
|
1790
1891
|
overlap_is_content: bool = False,
|
|
1791
|
-
) ->
|
|
1892
|
+
) -> AttributedTextSegments:
|
|
1792
1893
|
"""Tokenize concatenated segments as a single BPE pass and return
|
|
1793
1894
|
``(token_id, is_content)`` pairs.
|
|
1794
1895
|
|
|
@@ -1816,23 +1917,29 @@ def attribute_text_segments(
|
|
|
1816
1917
|
every body byte inside the ``is_content=True`` run at the cost of a
|
|
1817
1918
|
few adjacent wrap bytes.
|
|
1818
1919
|
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1920
|
+
When ``tokenizer`` implements :class:`OffsetTokenizer`, the result's
|
|
1921
|
+
``has_content_attribution`` flag is true and each bool is exact. For a
|
|
1922
|
+
basic :class:`Tokenizer`, the joined text is still encoded in one pass so
|
|
1923
|
+
token IDs remain identical, but the bools are placeholders and
|
|
1924
|
+
``has_content_attribution`` is false. Renderers propagate that state as an
|
|
1925
|
+
empty ``RenderedTokens.is_content`` list rather than exposing a partial or
|
|
1926
|
+
inaccurate mask.
|
|
1826
1927
|
|
|
1827
1928
|
Empty input or empty joined text returns an empty list.
|
|
1828
1929
|
"""
|
|
1829
1930
|
if not segments:
|
|
1830
|
-
return []
|
|
1931
|
+
return AttributedTextSegments([], has_content_attribution=True)
|
|
1831
1932
|
full_text = "".join(text for text, _ in segments)
|
|
1832
1933
|
if not full_text:
|
|
1833
|
-
return []
|
|
1934
|
+
return AttributedTextSegments([], has_content_attribution=True)
|
|
1834
1935
|
|
|
1835
1936
|
offset_tokenizer = _get_offset_tokenizer(tokenizer)
|
|
1937
|
+
if offset_tokenizer is None:
|
|
1938
|
+
token_ids = tokenizer.encode(full_text, add_special_tokens=False)
|
|
1939
|
+
return AttributedTextSegments(
|
|
1940
|
+
((token_id, False) for token_id in token_ids),
|
|
1941
|
+
has_content_attribution=False,
|
|
1942
|
+
)
|
|
1836
1943
|
encoding = offset_tokenizer(
|
|
1837
1944
|
full_text,
|
|
1838
1945
|
add_special_tokens=False,
|
|
@@ -1887,7 +1994,7 @@ def attribute_text_segments(
|
|
|
1887
1994
|
# the last non-empty segment's bit.
|
|
1888
1995
|
pass
|
|
1889
1996
|
out.append((tok_id, is_content))
|
|
1890
|
-
return out
|
|
1997
|
+
return AttributedTextSegments(out, has_content_attribution=True)
|
|
1891
1998
|
|
|
1892
1999
|
|
|
1893
2000
|
def reject_assistant_in_extension(new_messages: list[Message]) -> bool:
|
|
@@ -20,6 +20,7 @@ from renderers.base import (
|
|
|
20
20
|
RenderedTokens,
|
|
21
21
|
ToolSpec,
|
|
22
22
|
Tokenizer,
|
|
23
|
+
_content_mask_or_empty,
|
|
23
24
|
attribute_text_segments,
|
|
24
25
|
extract_message_tool_names,
|
|
25
26
|
reject_assistant_in_extension,
|
|
@@ -259,7 +260,7 @@ class DeepSeekV3Renderer:
|
|
|
259
260
|
token_ids=tokens,
|
|
260
261
|
message_indices=indices,
|
|
261
262
|
sampled_mask=sampled,
|
|
262
|
-
is_content=content_mask,
|
|
263
|
+
is_content=_content_mask_or_empty(self._tokenizer, content_mask),
|
|
263
264
|
message_roles=[m.get("role") or "" for m in messages],
|
|
264
265
|
message_tool_names=extract_message_tool_names(messages),
|
|
265
266
|
)
|
|
@@ -408,7 +409,9 @@ class DeepSeekV3Renderer:
|
|
|
408
409
|
token_ids=previous_ids + ext,
|
|
409
410
|
message_indices=[-1] * len(previous_ids) + ext_indices,
|
|
410
411
|
sampled_mask=[False] * total_len,
|
|
411
|
-
is_content=
|
|
412
|
+
is_content=_content_mask_or_empty(
|
|
413
|
+
self._tokenizer, [False] * len(previous_ids) + ext_content
|
|
414
|
+
),
|
|
412
415
|
message_roles=[m.get("role") or "" for m in new_messages],
|
|
413
416
|
message_tool_names=extract_message_tool_names(new_messages),
|
|
414
417
|
)
|
|
@@ -34,6 +34,7 @@ from renderers.base import (
|
|
|
34
34
|
ToolCallParseStatus,
|
|
35
35
|
ToolSpec,
|
|
36
36
|
Tokenizer,
|
|
37
|
+
_content_mask_or_empty,
|
|
37
38
|
_require_transformers,
|
|
38
39
|
attribute_text_segments,
|
|
39
40
|
extract_message_tool_names,
|
|
@@ -1025,7 +1026,7 @@ class Gemma4Renderer:
|
|
|
1025
1026
|
token_ids=em.token_ids,
|
|
1026
1027
|
message_indices=em.message_indices,
|
|
1027
1028
|
sampled_mask=em.sampled,
|
|
1028
|
-
is_content=em.is_content,
|
|
1029
|
+
is_content=_content_mask_or_empty(self._tokenizer, em.is_content),
|
|
1029
1030
|
message_roles=[m.get("role") or "" for m in messages],
|
|
1030
1031
|
message_tool_names=extract_message_tool_names(messages),
|
|
1031
1032
|
multi_modal_data=multi_modal_data,
|
|
@@ -1356,7 +1357,7 @@ class Gemma4Renderer:
|
|
|
1356
1357
|
token_ids=em.token_ids,
|
|
1357
1358
|
message_indices=em.message_indices,
|
|
1358
1359
|
sampled_mask=em.sampled,
|
|
1359
|
-
is_content=em.is_content,
|
|
1360
|
+
is_content=_content_mask_or_empty(self._tokenizer, em.is_content),
|
|
1360
1361
|
message_roles=[m.get("role") or "" for m in new_messages],
|
|
1361
1362
|
message_tool_names=extract_message_tool_names(new_messages),
|
|
1362
1363
|
multi_modal_data=self._merge_multi_modal_data(
|
|
@@ -19,6 +19,7 @@ from renderers.base import (
|
|
|
19
19
|
RenderedTokens,
|
|
20
20
|
ToolSpec,
|
|
21
21
|
Tokenizer,
|
|
22
|
+
_content_mask_or_empty,
|
|
22
23
|
attribute_text_segments,
|
|
23
24
|
extract_message_tool_names,
|
|
24
25
|
reject_assistant_in_extension,
|
|
@@ -261,7 +262,7 @@ class GLM45Renderer:
|
|
|
261
262
|
token_ids=tokens,
|
|
262
263
|
message_indices=indices,
|
|
263
264
|
sampled_mask=sampled,
|
|
264
|
-
is_content=content_mask,
|
|
265
|
+
is_content=_content_mask_or_empty(self._tokenizer, content_mask),
|
|
265
266
|
message_roles=[m.get("role") or "" for m in messages],
|
|
266
267
|
message_tool_names=extract_message_tool_names(messages),
|
|
267
268
|
)
|
|
@@ -448,7 +449,9 @@ class GLM45Renderer:
|
|
|
448
449
|
token_ids=previous_ids + ext,
|
|
449
450
|
message_indices=[-1] * len(previous_ids) + ext_indices,
|
|
450
451
|
sampled_mask=[False] * total_len,
|
|
451
|
-
is_content=
|
|
452
|
+
is_content=_content_mask_or_empty(
|
|
453
|
+
self._tokenizer, [False] * len(previous_ids) + ext_content
|
|
454
|
+
),
|
|
452
455
|
message_roles=[m.get("role") or "" for m in new_messages],
|
|
453
456
|
message_tool_names=extract_message_tool_names(new_messages),
|
|
454
457
|
)
|
|
@@ -20,6 +20,7 @@ from renderers.base import (
|
|
|
20
20
|
RenderedTokens,
|
|
21
21
|
ToolSpec,
|
|
22
22
|
Tokenizer,
|
|
23
|
+
_content_mask_or_empty,
|
|
23
24
|
attribute_text_segments,
|
|
24
25
|
extract_message_tool_names,
|
|
25
26
|
reject_assistant_in_extension,
|
|
@@ -283,7 +284,7 @@ class GLM5Renderer:
|
|
|
283
284
|
token_ids=tokens,
|
|
284
285
|
message_indices=indices,
|
|
285
286
|
sampled_mask=sampled,
|
|
286
|
-
is_content=content_mask,
|
|
287
|
+
is_content=_content_mask_or_empty(self._tokenizer, content_mask),
|
|
287
288
|
message_roles=[m.get("role") or "" for m in messages],
|
|
288
289
|
message_tool_names=extract_message_tool_names(messages),
|
|
289
290
|
)
|
|
@@ -465,7 +466,9 @@ class GLM5Renderer:
|
|
|
465
466
|
token_ids=previous_ids + ext,
|
|
466
467
|
message_indices=[-1] * len(previous_ids) + ext_indices,
|
|
467
468
|
sampled_mask=[False] * total_len,
|
|
468
|
-
is_content=
|
|
469
|
+
is_content=_content_mask_or_empty(
|
|
470
|
+
self._tokenizer, [False] * len(previous_ids) + ext_content
|
|
471
|
+
),
|
|
469
472
|
message_roles=[m.get("role") or "" for m in new_messages],
|
|
470
473
|
message_tool_names=extract_message_tool_names(new_messages),
|
|
471
474
|
)
|
|
@@ -55,6 +55,7 @@ from renderers.base import (
|
|
|
55
55
|
RenderedTokens,
|
|
56
56
|
ToolSpec,
|
|
57
57
|
Tokenizer,
|
|
58
|
+
_content_mask_or_empty,
|
|
58
59
|
extract_message_tool_names,
|
|
59
60
|
reject_assistant_in_extension,
|
|
60
61
|
resolve_thinking_retention,
|
|
@@ -461,7 +462,7 @@ class GptOssRenderer:
|
|
|
461
462
|
token_ids=tokens,
|
|
462
463
|
message_indices=indices,
|
|
463
464
|
sampled_mask=sampled,
|
|
464
|
-
is_content=content_mask,
|
|
465
|
+
is_content=_content_mask_or_empty(self._tokenizer, content_mask),
|
|
465
466
|
message_roles=[m.get("role") or "" for m in messages],
|
|
466
467
|
message_tool_names=extract_message_tool_names(messages),
|
|
467
468
|
)
|
|
@@ -597,7 +598,9 @@ class GptOssRenderer:
|
|
|
597
598
|
token_ids=previous_ids + ext,
|
|
598
599
|
message_indices=[-1] * len(previous_ids) + ext_indices,
|
|
599
600
|
sampled_mask=[False] * total_len,
|
|
600
|
-
is_content=
|
|
601
|
+
is_content=_content_mask_or_empty(
|
|
602
|
+
self._tokenizer, [False] * len(previous_ids) + ext_content
|
|
603
|
+
),
|
|
601
604
|
message_roles=[m.get("role") or "" for m in new_messages],
|
|
602
605
|
message_tool_names=extract_message_tool_names(new_messages),
|
|
603
606
|
)
|
|
@@ -33,6 +33,9 @@ from renderers.base import (
|
|
|
33
33
|
RenderedTokens,
|
|
34
34
|
ToolSpec,
|
|
35
35
|
Tokenizer,
|
|
36
|
+
_content_mask_or_empty,
|
|
37
|
+
_get_offset_tokenizer,
|
|
38
|
+
_infer_offsets_from_decode,
|
|
36
39
|
attribute_text_segments,
|
|
37
40
|
extract_message_tool_names,
|
|
38
41
|
reject_assistant_in_extension,
|
|
@@ -238,13 +241,34 @@ class Hy3Renderer:
|
|
|
238
241
|
if not segments:
|
|
239
242
|
return []
|
|
240
243
|
full_text = "".join(text for text, _, _ in segments)
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
244
|
+
offset_tokenizer = _get_offset_tokenizer(self._tokenizer)
|
|
245
|
+
if offset_tokenizer is None:
|
|
246
|
+
token_ids = self._encode(full_text)
|
|
247
|
+
offsets = _infer_offsets_from_decode(
|
|
248
|
+
self._tokenizer,
|
|
249
|
+
token_ids,
|
|
250
|
+
full_text,
|
|
251
|
+
)
|
|
252
|
+
if offsets is None:
|
|
253
|
+
# Token IDs remain exact even when a lossy decoder prevents
|
|
254
|
+
# reconstructing boundaries. Associate the opaque joined run
|
|
255
|
+
# with a contributing caller system message rather than
|
|
256
|
+
# silently classifying its body as global scaffold.
|
|
257
|
+
fallback_idx = next(
|
|
258
|
+
(msg_idx for text, _, msg_idx in segments if text and msg_idx >= 0),
|
|
259
|
+
-1,
|
|
260
|
+
)
|
|
261
|
+
return [(token_id, False, fallback_idx) for token_id in token_ids]
|
|
262
|
+
has_content_attribution = False
|
|
263
|
+
else:
|
|
264
|
+
encoding = offset_tokenizer(
|
|
265
|
+
full_text,
|
|
266
|
+
add_special_tokens=False,
|
|
267
|
+
return_offsets_mapping=True,
|
|
268
|
+
)
|
|
269
|
+
token_ids = list(encoding["input_ids"])
|
|
270
|
+
offsets = list(encoding["offset_mapping"])
|
|
271
|
+
has_content_attribution = True
|
|
248
272
|
|
|
249
273
|
spans: list[tuple[int, int, bool, int]] = []
|
|
250
274
|
pos = 0
|
|
@@ -254,13 +278,19 @@ class Hy3Renderer:
|
|
|
254
278
|
total_len = pos
|
|
255
279
|
|
|
256
280
|
out: list[tuple[int, bool, int]] = []
|
|
257
|
-
last = (
|
|
281
|
+
last = (
|
|
282
|
+
spans[-1][2] if has_content_attribution else False,
|
|
283
|
+
spans[-1][3],
|
|
284
|
+
)
|
|
258
285
|
for tok_id, (start, _end) in zip(token_ids, offsets):
|
|
259
286
|
attr = last
|
|
260
287
|
if start < total_len:
|
|
261
288
|
for seg_start, seg_end, seg_is_content, seg_idx in spans:
|
|
262
289
|
if seg_start <= start < seg_end:
|
|
263
|
-
attr = (
|
|
290
|
+
attr = (
|
|
291
|
+
seg_is_content if has_content_attribution else False,
|
|
292
|
+
seg_idx,
|
|
293
|
+
)
|
|
264
294
|
break
|
|
265
295
|
out.append((tok_id, attr[0], attr[1]))
|
|
266
296
|
return out
|
|
@@ -431,7 +461,7 @@ class Hy3Renderer:
|
|
|
431
461
|
token_ids=tokens,
|
|
432
462
|
message_indices=indices,
|
|
433
463
|
sampled_mask=sampled,
|
|
434
|
-
is_content=content_mask,
|
|
464
|
+
is_content=_content_mask_or_empty(self._tokenizer, content_mask),
|
|
435
465
|
message_roles=[m.get("role") or "" for m in messages],
|
|
436
466
|
message_tool_names=extract_message_tool_names(messages),
|
|
437
467
|
)
|
|
@@ -699,7 +729,9 @@ class Hy3Renderer:
|
|
|
699
729
|
token_ids=previous_ids + ext,
|
|
700
730
|
message_indices=[-1] * len(previous_ids) + ext_indices,
|
|
701
731
|
sampled_mask=[False] * total_len,
|
|
702
|
-
is_content=
|
|
732
|
+
is_content=_content_mask_or_empty(
|
|
733
|
+
self._tokenizer, [False] * len(previous_ids) + ext_content
|
|
734
|
+
),
|
|
703
735
|
message_roles=[m.get("role") or "" for m in new_messages],
|
|
704
736
|
message_tool_names=extract_message_tool_names(new_messages),
|
|
705
737
|
)
|
|
@@ -48,6 +48,7 @@ from renderers.base import (
|
|
|
48
48
|
RenderedTokens,
|
|
49
49
|
ToolSpec,
|
|
50
50
|
Tokenizer,
|
|
51
|
+
_content_mask_or_empty,
|
|
51
52
|
_require_transformers,
|
|
52
53
|
extract_message_tool_names,
|
|
53
54
|
reject_assistant_in_extension,
|
|
@@ -492,7 +493,7 @@ class InklingRenderer:
|
|
|
492
493
|
token_ids=tokens,
|
|
493
494
|
message_indices=indices,
|
|
494
495
|
sampled_mask=sampled,
|
|
495
|
-
is_content=content_mask,
|
|
496
|
+
is_content=_content_mask_or_empty(self._tokenizer, content_mask),
|
|
496
497
|
message_roles=[m.get("role") or "" for m in messages],
|
|
497
498
|
message_tool_names=tool_names,
|
|
498
499
|
multi_modal_data=mm_data,
|
|
@@ -989,7 +990,7 @@ class InklingRenderer:
|
|
|
989
990
|
token_ids=tokens,
|
|
990
991
|
message_indices=indices,
|
|
991
992
|
sampled_mask=sampled,
|
|
992
|
-
is_content=content_mask,
|
|
993
|
+
is_content=_content_mask_or_empty(self._tokenizer, content_mask),
|
|
993
994
|
message_roles=[m.get("role") or "" for m in new_messages],
|
|
994
995
|
message_tool_names=tool_names,
|
|
995
996
|
multi_modal_data=mm_data,
|
|
@@ -22,6 +22,7 @@ from renderers.base import (
|
|
|
22
22
|
RenderedTokens,
|
|
23
23
|
ToolSpec,
|
|
24
24
|
Tokenizer,
|
|
25
|
+
_content_mask_or_empty,
|
|
25
26
|
extract_message_tool_names,
|
|
26
27
|
reject_assistant_in_extension,
|
|
27
28
|
resolve_thinking_retention,
|
|
@@ -309,7 +310,7 @@ class KimiK2Renderer:
|
|
|
309
310
|
token_ids=token_ids,
|
|
310
311
|
message_indices=indices,
|
|
311
312
|
sampled_mask=sampled,
|
|
312
|
-
is_content=content_mask,
|
|
313
|
+
is_content=_content_mask_or_empty(self._tokenizer, content_mask),
|
|
313
314
|
message_roles=[m.get("role") or "" for m in caller_messages],
|
|
314
315
|
message_tool_names=extract_message_tool_names(caller_messages),
|
|
315
316
|
)
|
|
@@ -464,7 +465,9 @@ class KimiK2Renderer:
|
|
|
464
465
|
token_ids=previous_ids + ext,
|
|
465
466
|
message_indices=[-1] * len(previous_ids) + ext_indices,
|
|
466
467
|
sampled_mask=[False] * total_len,
|
|
467
|
-
is_content=
|
|
468
|
+
is_content=_content_mask_or_empty(
|
|
469
|
+
self._tokenizer, [False] * len(previous_ids) + ext_content
|
|
470
|
+
),
|
|
468
471
|
message_roles=[m.get("role") or "" for m in new_messages],
|
|
469
472
|
message_tool_names=extract_message_tool_names(new_messages),
|
|
470
473
|
)
|