renderers 0.1.8.dev52__tar.gz → 0.1.8.dev54__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.dev54}/PKG-INFO +1 -1
  2. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/renderers/__init__.py +4 -0
  3. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/renderers/_version.py +2 -2
  4. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/renderers/base.py +64 -4
  5. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/renderers/configs.py +24 -0
  6. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/renderers/laguna_xs2.py +26 -17
  7. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/tests/conftest.py +3 -0
  8. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/tests/test_build_helpers.py +18 -2
  9. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/tests/test_is_content.py +2 -1
  10. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/.github/workflows/publish-dev.yml +0 -0
  11. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/.github/workflows/publish.yml +0 -0
  12. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/.github/workflows/style.yml +0 -0
  13. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/.github/workflows/test.yml +0 -0
  14. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/.gitignore +0 -0
  15. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/.pre-commit-config.yaml +0 -0
  16. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/LICENSE +0 -0
  17. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/README.md +0 -0
  18. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/docs/renderer-config.md +0 -0
  19. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/examples/README.md +0 -0
  20. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/examples/sglang/multiturn_generate_sglang.py +0 -0
  21. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/examples/sglang/online_multiturn_sglang.py +0 -0
  22. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/examples/tinker/multiturn_generate_tinker.py +0 -0
  23. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/examples/transformers/multiturn_generate_transformers.py +0 -0
  24. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/examples/vllm/multiturn_generate_vllm.py +0 -0
  25. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/pyproject.toml +0 -0
  26. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/renderers/client.py +0 -0
  27. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/renderers/deepseek_r1.py +0 -0
  28. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/renderers/deepseek_v3.py +0 -0
  29. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/renderers/default.py +0 -0
  30. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/renderers/glm45.py +0 -0
  31. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/renderers/glm5.py +0 -0
  32. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/renderers/gpt_oss.py +0 -0
  33. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/renderers/kimi_k2.py +0 -0
  34. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/renderers/kimi_k25.py +0 -0
  35. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/renderers/llama_3.py +0 -0
  36. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/renderers/minimax_m2.py +0 -0
  37. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/renderers/nemotron3.py +0 -0
  38. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/renderers/parsers.py +0 -0
  39. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/renderers/parsing.py +0 -0
  40. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/renderers/qwen3.py +0 -0
  41. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/renderers/qwen35.py +0 -0
  42. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/renderers/qwen36.py +0 -0
  43. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/renderers/qwen3_vl.py +0 -0
  44. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/tests/test_bridge.py +0 -0
  45. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/tests/test_client.py +0 -0
  46. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/tests/test_deepseek_r1.py +0 -0
  47. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/tests/test_gpt_oss_harmony_parity.py +0 -0
  48. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/tests/test_incremental.py +0 -0
  49. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/tests/test_kimi_k25_tool_schema.py +0 -0
  50. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/tests/test_llama_3.py +0 -0
  51. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/tests/test_load_tokenizer.py +0 -0
  52. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/tests/test_message_indices.py +0 -0
  53. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/tests/test_message_tool_names.py +0 -0
  54. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/tests/test_multimodal.py +0 -0
  55. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/tests/test_nemotron3_parity.py +0 -0
  56. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/tests/test_nemotron3_ultra.py +0 -0
  57. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/tests/test_parse_response.py +0 -0
  58. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/tests/test_parse_response_robustness.py +0 -0
  59. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/tests/test_parsers.py +0 -0
  60. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/tests/test_preserve_thinking.py +0 -0
  61. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/tests/test_qwen35_size_coverage.py +0 -0
  62. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/tests/test_render_ids.py +0 -0
  63. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/tests/test_renderer_config.py +0 -0
  64. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/tests/test_renderer_config_parity.py +0 -0
  65. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/tests/test_roundtrip.py +0 -0
  66. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/tests/test_sampled_mask.py +0 -0
  67. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/tests/test_tokens_per_message.py +0 -0
  68. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/tests/test_tool_arg_type_preservation.py +0 -0
  69. {renderers-0.1.8.dev52 → renderers-0.1.8.dev54}/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.dev54
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,
@@ -53,6 +54,7 @@ from renderers.configs import (
53
54
  KimiK25RendererConfig,
54
55
  KimiK2RendererConfig,
55
56
  LagunaXS2RendererConfig,
57
+ LagunaXS21RendererConfig,
56
58
  Llama3RendererConfig,
57
59
  MiniMaxM2RendererConfig,
58
60
  Nemotron3RendererConfig,
@@ -138,6 +140,7 @@ __all__ = [
138
140
  "KimiK2RendererConfig",
139
141
  "LagunaXS2Renderer",
140
142
  "LagunaXS2RendererConfig",
143
+ "LagunaXS21RendererConfig",
141
144
  "Llama3Renderer",
142
145
  "Llama3RendererConfig",
143
146
  "MULTIMODAL_MODELS",
@@ -164,6 +167,7 @@ __all__ = [
164
167
  "Qwen3VLRendererConfig",
165
168
  "RenderedConversation",
166
169
  "RenderedTokens",
170
+ "RenderedTrainingSample",
167
171
  "Renderer",
168
172
  "RendererConfig",
169
173
  "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.dev54'
22
+ __version_tuple__ = version_tuple = (0, 1, 8, 'dev54')
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -1060,8 +1060,10 @@ MODEL_RENDERER_MAP: dict[str, str] = {
1060
1060
  # construction to pin a different date.
1061
1061
  "meta-llama/Llama-3.2-1B-Instruct": "llama-3",
1062
1062
  "meta-llama/Llama-3.2-3B-Instruct": "llama-3",
1063
- # Poolside Laguna.
1063
+ # Poolside Laguna. XS-2.1's template is byte-identical to XS.2's minus
1064
+ # the default system message; the config name selects the variant.
1064
1065
  "poolside/Laguna-XS.2": "laguna-xs.2",
1066
+ "poolside/Laguna-XS-2.1": "laguna-xs-2.1",
1065
1067
  # GPT-OSS.
1066
1068
  "openai/gpt-oss-20b": "gpt-oss",
1067
1069
  "openai/gpt-oss-120b": "gpt-oss",
@@ -1327,6 +1329,7 @@ def _populate_registry():
1327
1329
  "kimi-k2": KimiK2Renderer,
1328
1330
  "kimi-k2.5": KimiK25Renderer,
1329
1331
  "laguna-xs.2": LagunaXS2Renderer,
1332
+ "laguna-xs-2.1": LagunaXS2Renderer,
1330
1333
  "llama-3": Llama3Renderer,
1331
1334
  "nemotron-3": Nemotron3Renderer,
1332
1335
  "nemotron-3-ultra": Nemotron3UltraRenderer,
@@ -1534,6 +1537,42 @@ def _resolve_auto_config(
1534
1537
  # ---------------------------------------------------------------------------
1535
1538
 
1536
1539
 
1540
+ # Match prime-rl's multimodal token type convention: 0=text, 1=image, 2=video.
1541
+ _MM_TYPE_ID: dict[str, int] = {"image": 1, "video": 2}
1542
+
1543
+
1544
+ @dataclass(frozen=True)
1545
+ class RenderedTrainingSample:
1546
+ """Output of :func:`build_training_sample`.
1547
+
1548
+ ``token_ids`` and ``loss_mask`` are always populated. ``multi_modal_data``
1549
+ and ``mm_token_type_ids`` are populated only when a multimodal renderer
1550
+ actually emitted media (both ``None`` for text-only renderers and for
1551
+ text-only samples through a VLM renderer), so the text path is unchanged.
1552
+ """
1553
+
1554
+ token_ids: list[int]
1555
+ loss_mask: list[bool]
1556
+ multi_modal_data: "MultiModalData | None" = None
1557
+ mm_token_type_ids: list[int] | None = None
1558
+
1559
+
1560
+ def _build_mm_token_type_ids(
1561
+ mm_placeholders: dict[str, list[PlaceholderRange]], length: int
1562
+ ) -> list[int]:
1563
+ """Per-token modality flags (0=text, 1=image, 2=video) from placeholder ranges."""
1564
+ ids = [0] * length
1565
+ for modality, ranges in mm_placeholders.items():
1566
+ type_id = _MM_TYPE_ID.get(modality, 0)
1567
+ if type_id == 0:
1568
+ continue
1569
+ for r in ranges:
1570
+ end = min(r.offset + r.length, length)
1571
+ for i in range(r.offset, end):
1572
+ ids[i] = type_id
1573
+ return ids
1574
+
1575
+
1537
1576
  def build_training_sample(
1538
1577
  renderer: Renderer,
1539
1578
  messages: list[Message],
@@ -1541,8 +1580,12 @@ def build_training_sample(
1541
1580
  role_to_mask: Callable[[Message], bool] | None = None,
1542
1581
  tools: list[ToolSpec] | None = None,
1543
1582
  content_sft_roles: "set[str] | frozenset[str] | None" = None,
1544
- ) -> tuple[list[int], list[bool]]:
1545
- """Build (token_ids, loss_mask) for supervised training.
1583
+ ) -> RenderedTrainingSample:
1584
+ """Build a :class:`RenderedTrainingSample` for supervised training.
1585
+
1586
+ Returns ``token_ids`` + ``loss_mask`` (always), plus ``multi_modal_data``
1587
+ and ``mm_token_type_ids`` when the renderer emitted media (``None`` for
1588
+ text — the text token_ids/loss_mask are byte-identical to before).
1546
1589
 
1547
1590
  Single render() call + message_indices → per-token mask.
1548
1591
  Replaces build_incremental_token_mask (O(N) renders → O(1)).
@@ -1631,7 +1674,24 @@ def build_training_sample(
1631
1674
  loss_mask.append(True)
1632
1675
  else:
1633
1676
  loss_mask.append(role_to_mask(msg))
1634
- return rendered.token_ids, loss_mask
1677
+
1678
+ # Surface the multimodal payload for VLM renderers. ``None`` for text
1679
+ # renderers and for text-only samples (empty media) so downstream
1680
+ # ``multi_modal_data is not None`` is a reliable "has media" check.
1681
+ mm = rendered.multi_modal_data
1682
+ if mm is not None and mm.is_empty():
1683
+ mm = None
1684
+ mm_token_type_ids = (
1685
+ _build_mm_token_type_ids(mm.mm_placeholders, len(rendered.token_ids))
1686
+ if mm is not None and mm.mm_placeholders
1687
+ else None
1688
+ )
1689
+ return RenderedTrainingSample(
1690
+ token_ids=rendered.token_ids,
1691
+ loss_mask=loss_mask,
1692
+ multi_modal_data=mm,
1693
+ mm_token_type_ids=mm_token_type_ids,
1694
+ )
1635
1695
 
1636
1696
 
1637
1697
  def _common_prefix_len(a: list[int], b: list[int]) -> int:
@@ -422,6 +422,27 @@ class LagunaXS2RendererConfig(BaseRendererConfig):
422
422
  chat template's ``render_assistant_messages_raw`` gate."""
423
423
 
424
424
 
425
+ class LagunaXS21RendererConfig(BaseRendererConfig):
426
+ """Laguna XS-2.1 renderer config — distinct discriminator so auto
427
+ resolution gives XS-2.1 checkpoints the no-default-system-message
428
+ template variant.
429
+
430
+ XS-2.1's chat template is byte-identical to XS.2's except it ships
431
+ no default system message: when the caller provides none (and no
432
+ tools), the ``<system>`` block is omitted entirely. Shares
433
+ :class:`renderers.laguna_xs2.LagunaXS2Renderer`, which selects the
434
+ variant from ``config.name``.
435
+ """
436
+
437
+ name: Literal["laguna-xs-2.1"] = "laguna-xs-2.1"
438
+
439
+ enable_thinking: bool = False
440
+ """See :class:`LagunaXS2RendererConfig.enable_thinking`."""
441
+
442
+ render_assistant_messages_raw: bool = False
443
+ """See :class:`LagunaXS2RendererConfig.render_assistant_messages_raw`."""
444
+
445
+
425
446
  class Llama3RendererConfig(BaseRendererConfig):
426
447
  """Llama-3.x Instruct renderer config.
427
448
 
@@ -576,6 +597,7 @@ RendererConfig = Annotated[
576
597
  KimiK2RendererConfig,
577
598
  KimiK25RendererConfig,
578
599
  LagunaXS2RendererConfig,
600
+ LagunaXS21RendererConfig,
579
601
  Llama3RendererConfig,
580
602
  MiniMaxM2RendererConfig,
581
603
  Nemotron3RendererConfig,
@@ -613,6 +635,7 @@ _CONFIG_BY_NAME: dict[str, type[BaseRendererConfig]] = {
613
635
  "kimi-k2": KimiK2RendererConfig,
614
636
  "kimi-k2.5": KimiK25RendererConfig,
615
637
  "laguna-xs.2": LagunaXS2RendererConfig,
638
+ "laguna-xs-2.1": LagunaXS21RendererConfig,
616
639
  "llama-3": Llama3RendererConfig,
617
640
  "minimax-m2": MiniMaxM2RendererConfig,
618
641
  "nemotron-3": Nemotron3RendererConfig,
@@ -659,6 +682,7 @@ __all__ = [
659
682
  "KimiK25RendererConfig",
660
683
  "KimiK2RendererConfig",
661
684
  "LagunaXS2RendererConfig",
685
+ "LagunaXS21RendererConfig",
662
686
  "Llama3RendererConfig",
663
687
  "MiniMaxM2RendererConfig",
664
688
  "Nemotron3RendererConfig",
@@ -1,4 +1,4 @@
1
- """Laguna-XS.2 Renderer.
1
+ """Laguna-XS.2 / XS-2.1 Renderer.
2
2
 
3
3
  Main properties:
4
4
  - Prefix is the single token ``〈|EOS|〉`` (also the EOS / stop token).
@@ -12,10 +12,13 @@ Main properties:
12
12
  - Tool calls: ``<tool_call>`` / ``</tool_call>`` ARE single tokens, but the
13
13
  inner ``<arg_key>`` / ``</arg_key>`` / ``<arg_value>`` / ``</arg_value>``
14
14
  markers are plain text — parsed via regex on the decoded inner block.
15
- - The template bakes in a default system prompt when ``messages[0]`` is not
16
- a system message. The system block also contains the tools section (under
17
- a ``### Tools`` header with an ``<available_tools>`` listing and prose
18
- format instructions that vary on ``enable_thinking``).
15
+ - XS.2's template bakes in a default system prompt when ``messages[0]`` is
16
+ not a system message; XS-2.1's (otherwise byte-identical) template
17
+ removed it, so no system message and no tools means no ``<system>``
18
+ block at all. The config's ``name`` selects the variant. The system
19
+ block also contains the tools section (under a ``### Tools`` header
20
+ with an ``<available_tools>`` listing and prose format instructions
21
+ that vary on ``enable_thinking``).
19
22
  - Reasoning is rendered for every assistant message — no last-user-index
20
23
  gating. ``thinking_retention`` is accepted for protocol uniformity but
21
24
  is effectively a no-op since past reasoning is preserved by default.
@@ -39,7 +42,7 @@ from renderers.base import (
39
42
  resolve_thinking_retention,
40
43
  should_rerender_for_thinking_retention,
41
44
  )
42
- from renderers.configs import LagunaXS2RendererConfig
45
+ from renderers.configs import LagunaXS2RendererConfig, LagunaXS21RendererConfig
43
46
  from renderers.parsing import parse_laguna_xs2
44
47
 
45
48
  _DEFAULT_SYSTEM_MESSAGE = (
@@ -81,7 +84,7 @@ class LagunaXS2Renderer:
81
84
  def __init__(
82
85
  self,
83
86
  tokenizer: PreTrainedTokenizer,
84
- config: LagunaXS2RendererConfig | None = None,
87
+ config: LagunaXS2RendererConfig | LagunaXS21RendererConfig | None = None,
85
88
  ):
86
89
  self._tokenizer = tokenizer
87
90
  self.config = config or LagunaXS2RendererConfig()
@@ -89,6 +92,11 @@ class LagunaXS2Renderer:
89
92
  self.config,
90
93
  "all",
91
94
  )
95
+ # XS.2's template bakes in a default system prompt; XS-2.1's
96
+ # (otherwise byte-identical) template removed it.
97
+ self._default_system_message = (
98
+ _DEFAULT_SYSTEM_MESSAGE if self.config.name == "laguna-xs.2" else ""
99
+ )
92
100
 
93
101
  self._eos = self._token_id("〈|EOS|〉")
94
102
  self._think = self._token_id("<think>")
@@ -184,7 +192,7 @@ class LagunaXS2Renderer:
184
192
  emit_special(self._eos, -1, is_sampled=False, is_content=False)
185
193
 
186
194
  # ── System header (absorbs messages[0] if it's a system message) ──
187
- system_content = _DEFAULT_SYSTEM_MESSAGE
195
+ system_content = self._default_system_message
188
196
  system_msg_idx = -1
189
197
  caller_has_system = bool(messages and messages[0].get("role") == "system")
190
198
  if caller_has_system:
@@ -196,16 +204,12 @@ class LagunaXS2Renderer:
196
204
  # gate: when the caller passes an empty system message and no tools,
197
205
  # the whole ``<system>...</system>`` block is omitted.
198
206
  if has_sys_content or tools:
199
- # The template emits ``<system>\n`` then conditionally a second
200
- # ``\n``. Bundle those into one emit so BPE merges ``\n\n`` into
201
- # its single-token form (rather than two ``\n`` atoms).
202
- emit_text(
203
- "<system>\n\n" if has_sys_content else "<system>\n",
204
- -1,
205
- is_sampled=False,
206
- is_content=False,
207
- )
208
207
  if has_sys_content:
208
+ # The template emits ``<system>\n`` then a second ``\n``
209
+ # before the system body. Bundle those into one emit so BPE
210
+ # merges ``\n\n`` into its single-token form (rather than
211
+ # two ``\n`` atoms).
212
+ emit_text("<system>\n\n", -1, is_sampled=False, is_content=False)
209
213
  # If the caller provided system content, it's body bytes;
210
214
  # otherwise this is the default system prompt (scaffold).
211
215
  sys_is_content = caller_has_system
@@ -224,6 +228,11 @@ class LagunaXS2Renderer:
224
228
  if self.config.enable_thinking
225
229
  else _TOOLS_FOOTER_NO_THINKING
226
230
  )
231
+ if not has_sys_content:
232
+ # No system body: ``<system>\n`` runs straight into the
233
+ # tools header's ``\n\n`` — encode them together so BPE
234
+ # merges the ``\n\n\n`` seam as the template does.
235
+ tool_text = "<system>\n" + tool_text
227
236
  emit_text(tool_text, -1, is_sampled=False, is_content=False)
228
237
  emit_text("\n</system>\n", -1, is_sampled=False, is_content=False)
229
238
 
@@ -37,6 +37,9 @@ RENDERER_MODELS = [
37
37
  # name (auto → MODEL_RENDERER_MAP → nemotron-3-ultra).
38
38
  ("nvidia/NVIDIA-Nemotron-3-Ultra-550B-A55B-BF16", "auto"),
39
39
  ("poolside/Laguna-XS.2", "auto"),
40
+ # XS-2.1 resolves to the `laguna-xs-2.1` config variant (no default
41
+ # system message) via the model name.
42
+ ("poolside/Laguna-XS-2.1", "auto"),
40
43
  # DeepSeek-V3/R1 are intentionally NOT in this shared barrage: their
41
44
  # chat templates can't render the barrage's tool-call fixtures (the
42
45
  # templates require ``tool['type']`` and a string-serialized
@@ -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