renderers 0.1.8.dev52__tar.gz → 0.1.8.dev53__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.
Files changed (69) hide show
  1. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/PKG-INFO +1 -1
  2. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/renderers/__init__.py +2 -0
  3. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/renderers/_version.py +2 -2
  4. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/renderers/base.py +60 -3
  5. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/tests/test_build_helpers.py +18 -2
  6. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/tests/test_is_content.py +2 -1
  7. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/.github/workflows/publish-dev.yml +0 -0
  8. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/.github/workflows/publish.yml +0 -0
  9. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/.github/workflows/style.yml +0 -0
  10. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/.github/workflows/test.yml +0 -0
  11. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/.gitignore +0 -0
  12. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/.pre-commit-config.yaml +0 -0
  13. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/LICENSE +0 -0
  14. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/README.md +0 -0
  15. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/docs/renderer-config.md +0 -0
  16. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/examples/README.md +0 -0
  17. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/examples/sglang/multiturn_generate_sglang.py +0 -0
  18. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/examples/sglang/online_multiturn_sglang.py +0 -0
  19. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/examples/tinker/multiturn_generate_tinker.py +0 -0
  20. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/examples/transformers/multiturn_generate_transformers.py +0 -0
  21. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/examples/vllm/multiturn_generate_vllm.py +0 -0
  22. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/pyproject.toml +0 -0
  23. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/renderers/client.py +0 -0
  24. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/renderers/configs.py +0 -0
  25. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/renderers/deepseek_r1.py +0 -0
  26. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/renderers/deepseek_v3.py +0 -0
  27. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/renderers/default.py +0 -0
  28. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/renderers/glm45.py +0 -0
  29. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/renderers/glm5.py +0 -0
  30. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/renderers/gpt_oss.py +0 -0
  31. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/renderers/kimi_k2.py +0 -0
  32. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/renderers/kimi_k25.py +0 -0
  33. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/renderers/laguna_xs2.py +0 -0
  34. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/renderers/llama_3.py +0 -0
  35. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/renderers/minimax_m2.py +0 -0
  36. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/renderers/nemotron3.py +0 -0
  37. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/renderers/parsers.py +0 -0
  38. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/renderers/parsing.py +0 -0
  39. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/renderers/qwen3.py +0 -0
  40. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/renderers/qwen35.py +0 -0
  41. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/renderers/qwen36.py +0 -0
  42. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/renderers/qwen3_vl.py +0 -0
  43. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/tests/conftest.py +0 -0
  44. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/tests/test_bridge.py +0 -0
  45. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/tests/test_client.py +0 -0
  46. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/tests/test_deepseek_r1.py +0 -0
  47. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/tests/test_gpt_oss_harmony_parity.py +0 -0
  48. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/tests/test_incremental.py +0 -0
  49. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/tests/test_kimi_k25_tool_schema.py +0 -0
  50. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/tests/test_llama_3.py +0 -0
  51. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/tests/test_load_tokenizer.py +0 -0
  52. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/tests/test_message_indices.py +0 -0
  53. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/tests/test_message_tool_names.py +0 -0
  54. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/tests/test_multimodal.py +0 -0
  55. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/tests/test_nemotron3_parity.py +0 -0
  56. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/tests/test_nemotron3_ultra.py +0 -0
  57. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/tests/test_parse_response.py +0 -0
  58. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/tests/test_parse_response_robustness.py +0 -0
  59. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/tests/test_parsers.py +0 -0
  60. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/tests/test_preserve_thinking.py +0 -0
  61. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/tests/test_qwen35_size_coverage.py +0 -0
  62. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/tests/test_render_ids.py +0 -0
  63. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/tests/test_renderer_config.py +0 -0
  64. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/tests/test_renderer_config_parity.py +0 -0
  65. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/tests/test_roundtrip.py +0 -0
  66. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/tests/test_sampled_mask.py +0 -0
  67. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/tests/test_tokens_per_message.py +0 -0
  68. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/tests/test_tool_arg_type_preservation.py +0 -0
  69. {renderers-0.1.8.dev52 → renderers-0.1.8.dev53}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: renderers
3
- Version: 0.1.8.dev52
3
+ Version: 0.1.8.dev53
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
@@ -19,6 +19,7 @@ from renderers.base import (
19
19
  PlaceholderRange,
20
20
  RenderedConversation,
21
21
  RenderedTokens,
22
+ RenderedTrainingSample,
22
23
  Renderer,
23
24
  RendererPool,
24
25
  TextPart,
@@ -164,6 +165,7 @@ __all__ = [
164
165
  "Qwen3VLRendererConfig",
165
166
  "RenderedConversation",
166
167
  "RenderedTokens",
168
+ "RenderedTrainingSample",
167
169
  "Renderer",
168
170
  "RendererConfig",
169
171
  "RendererPool",
@@ -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.8.dev52'
22
- __version_tuple__ = version_tuple = (0, 1, 8, 'dev52')
21
+ __version__ = version = '0.1.8.dev53'
22
+ __version_tuple__ = version_tuple = (0, 1, 8, 'dev53')
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -1534,6 +1534,42 @@ def _resolve_auto_config(
1534
1534
  # ---------------------------------------------------------------------------
1535
1535
 
1536
1536
 
1537
+ # Match prime-rl's multimodal token type convention: 0=text, 1=image, 2=video.
1538
+ _MM_TYPE_ID: dict[str, int] = {"image": 1, "video": 2}
1539
+
1540
+
1541
+ @dataclass(frozen=True)
1542
+ class RenderedTrainingSample:
1543
+ """Output of :func:`build_training_sample`.
1544
+
1545
+ ``token_ids`` and ``loss_mask`` are always populated. ``multi_modal_data``
1546
+ and ``mm_token_type_ids`` are populated only when a multimodal renderer
1547
+ actually emitted media (both ``None`` for text-only renderers and for
1548
+ text-only samples through a VLM renderer), so the text path is unchanged.
1549
+ """
1550
+
1551
+ token_ids: list[int]
1552
+ loss_mask: list[bool]
1553
+ multi_modal_data: "MultiModalData | None" = None
1554
+ mm_token_type_ids: list[int] | None = None
1555
+
1556
+
1557
+ def _build_mm_token_type_ids(
1558
+ mm_placeholders: dict[str, list[PlaceholderRange]], length: int
1559
+ ) -> list[int]:
1560
+ """Per-token modality flags (0=text, 1=image, 2=video) from placeholder ranges."""
1561
+ ids = [0] * length
1562
+ for modality, ranges in mm_placeholders.items():
1563
+ type_id = _MM_TYPE_ID.get(modality, 0)
1564
+ if type_id == 0:
1565
+ continue
1566
+ for r in ranges:
1567
+ end = min(r.offset + r.length, length)
1568
+ for i in range(r.offset, end):
1569
+ ids[i] = type_id
1570
+ return ids
1571
+
1572
+
1537
1573
  def build_training_sample(
1538
1574
  renderer: Renderer,
1539
1575
  messages: list[Message],
@@ -1541,8 +1577,12 @@ def build_training_sample(
1541
1577
  role_to_mask: Callable[[Message], bool] | None = None,
1542
1578
  tools: list[ToolSpec] | None = None,
1543
1579
  content_sft_roles: "set[str] | frozenset[str] | None" = None,
1544
- ) -> tuple[list[int], list[bool]]:
1545
- """Build (token_ids, loss_mask) for supervised training.
1580
+ ) -> RenderedTrainingSample:
1581
+ """Build a :class:`RenderedTrainingSample` for supervised training.
1582
+
1583
+ Returns ``token_ids`` + ``loss_mask`` (always), plus ``multi_modal_data``
1584
+ and ``mm_token_type_ids`` when the renderer emitted media (``None`` for
1585
+ text — the text token_ids/loss_mask are byte-identical to before).
1546
1586
 
1547
1587
  Single render() call + message_indices → per-token mask.
1548
1588
  Replaces build_incremental_token_mask (O(N) renders → O(1)).
@@ -1631,7 +1671,24 @@ def build_training_sample(
1631
1671
  loss_mask.append(True)
1632
1672
  else:
1633
1673
  loss_mask.append(role_to_mask(msg))
1634
- return rendered.token_ids, loss_mask
1674
+
1675
+ # Surface the multimodal payload for VLM renderers. ``None`` for text
1676
+ # renderers and for text-only samples (empty media) so downstream
1677
+ # ``multi_modal_data is not None`` is a reliable "has media" check.
1678
+ mm = rendered.multi_modal_data
1679
+ if mm is not None and mm.is_empty():
1680
+ mm = None
1681
+ mm_token_type_ids = (
1682
+ _build_mm_token_type_ids(mm.mm_placeholders, len(rendered.token_ids))
1683
+ if mm is not None and mm.mm_placeholders
1684
+ else None
1685
+ )
1686
+ return RenderedTrainingSample(
1687
+ token_ids=rendered.token_ids,
1688
+ loss_mask=loss_mask,
1689
+ multi_modal_data=mm,
1690
+ mm_token_type_ids=mm_token_type_ids,
1691
+ )
1635
1692
 
1636
1693
 
1637
1694
  def _common_prefix_len(a: list[int], b: list[int]) -> int:
@@ -4,6 +4,17 @@ Runs against every (model, renderer) pair.
4
4
  """
5
5
 
6
6
  from renderers import build_training_sample, build_trajectory_step
7
+ from renderers.base import PlaceholderRange, _build_mm_token_type_ids
8
+
9
+
10
+ def test_build_mm_token_type_ids_marks_ranges():
11
+ """Image runs → 1, video runs → 2, everything else → 0; clips at length."""
12
+ placeholders = {
13
+ "image": [PlaceholderRange(offset=2, length=3)], # tokens 2,3,4
14
+ "video": [PlaceholderRange(offset=7, length=2)], # tokens 7,8
15
+ }
16
+ ids = _build_mm_token_type_ids(placeholders, length=10)
17
+ assert ids == [0, 0, 1, 1, 1, 0, 0, 2, 2, 0]
7
18
 
8
19
 
9
20
  def _expected(tokenizer, messages, **kwargs):
@@ -29,10 +40,14 @@ def test_build_training_sample_ids_match(model_name, tokenizer, renderer):
29
40
  {"role": "user", "content": "Hi"},
30
41
  {"role": "assistant", "content": "Hello!"},
31
42
  ]
32
- ids, mask = build_training_sample(
43
+ sample = build_training_sample(
33
44
  renderer, msgs, role_to_mask=lambda m: m["role"] == "assistant"
34
45
  )
46
+ ids = sample.token_ids
35
47
  assert ids == _expected(tokenizer, msgs)
48
+ # text-only sample carries no multimodal payload
49
+ assert sample.multi_modal_data is None
50
+ assert sample.mm_token_type_ids is None
36
51
 
37
52
 
38
53
  def test_build_training_sample_has_trainable_tokens(model_name, tokenizer, renderer):
@@ -41,9 +56,10 @@ def test_build_training_sample_has_trainable_tokens(model_name, tokenizer, rende
41
56
  {"role": "user", "content": "Hi"},
42
57
  {"role": "assistant", "content": "Hello!"},
43
58
  ]
44
- ids, mask = build_training_sample(
59
+ sample = build_training_sample(
45
60
  renderer, msgs, role_to_mask=lambda m: m["role"] == "assistant"
46
61
  )
62
+ ids, mask = sample.token_ids, sample.loss_mask
47
63
  assert sum(mask) > 0
48
64
  assert len(mask) == len(ids)
49
65
 
@@ -353,12 +353,13 @@ def test_build_training_sample_content_sft_roles_picks_up_tool_body(
353
353
  {"role": "tool", "content": "done", "tool_call_id": "call_z"},
354
354
  {"role": "assistant", "content": "OK."},
355
355
  ]
356
- ids, mask = build_training_sample(
356
+ sample = build_training_sample(
357
357
  renderer,
358
358
  msgs,
359
359
  role_to_mask=lambda m: m["role"] == "assistant",
360
360
  content_sft_roles={"tool"},
361
361
  )
362
+ ids, mask = sample.token_ids, sample.loss_mask
362
363
  assert len(mask) == len(ids)
363
364
 
364
365
  # We need at least one trainable tool-body token if the renderer
File without changes
File without changes