renderers 0.1.10.dev4__tar.gz → 0.1.10.dev10__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 (79) hide show
  1. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/PKG-INFO +2 -2
  2. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/README.md +1 -1
  3. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/docs/renderer-config.md +2 -1
  4. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/renderers/__init__.py +4 -0
  5. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/renderers/_version.py +2 -2
  6. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/renderers/base.py +5 -2
  7. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/renderers/configs.py +34 -0
  8. renderers-0.1.10.dev10/renderers/laguna_s21.py +33 -0
  9. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/renderers/laguna_xs2.py +14 -2
  10. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/tests/conftest.py +4 -0
  11. renderers-0.1.10.dev10/tests/test_laguna_s21.py +128 -0
  12. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/tests/test_renderer_config_parity.py +1 -0
  13. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/.github/workflows/publish-dev.yml +0 -0
  14. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/.github/workflows/publish.yml +0 -0
  15. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/.github/workflows/style.yml +0 -0
  16. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/.github/workflows/test.yml +0 -0
  17. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/.gitignore +0 -0
  18. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/.pre-commit-config.yaml +0 -0
  19. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/LICENSE +0 -0
  20. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/examples/README.md +0 -0
  21. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/examples/sglang/multiturn_generate_sglang.py +0 -0
  22. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/examples/sglang/online_multiturn_sglang.py +0 -0
  23. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/examples/tinker/multiturn_generate_tinker.py +0 -0
  24. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/examples/transformers/multiturn_generate_transformers.py +0 -0
  25. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/examples/vllm/multiturn_generate_vllm.py +0 -0
  26. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/pyproject.toml +0 -0
  27. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/renderers/client.py +0 -0
  28. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/renderers/deepseek_r1.py +0 -0
  29. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/renderers/deepseek_v3.py +0 -0
  30. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/renderers/default.py +0 -0
  31. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/renderers/glm45.py +0 -0
  32. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/renderers/glm5.py +0 -0
  33. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/renderers/gpt_oss.py +0 -0
  34. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/renderers/hy3.py +0 -0
  35. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/renderers/kimi_k2.py +0 -0
  36. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/renderers/kimi_k25.py +0 -0
  37. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/renderers/llama_3.py +0 -0
  38. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/renderers/minimax_m2.py +0 -0
  39. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/renderers/nemotron3.py +0 -0
  40. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/renderers/parsers.py +0 -0
  41. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/renderers/parsing.py +0 -0
  42. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/renderers/prime_qwen3.py +0 -0
  43. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/renderers/qwen3.py +0 -0
  44. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/renderers/qwen35.py +0 -0
  45. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/renderers/qwen36.py +0 -0
  46. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/renderers/qwen3_vl.py +0 -0
  47. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/tests/test_bridge.py +0 -0
  48. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/tests/test_build_helpers.py +0 -0
  49. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/tests/test_client.py +0 -0
  50. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/tests/test_deepseek_r1.py +0 -0
  51. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/tests/test_disabled_thinking_stability.py +0 -0
  52. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/tests/test_glm_tool_name_validation.py +0 -0
  53. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/tests/test_gpt_oss_harmony_parity.py +0 -0
  54. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/tests/test_hy3.py +0 -0
  55. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/tests/test_incremental.py +0 -0
  56. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/tests/test_is_content.py +0 -0
  57. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/tests/test_kimi_k25_tool_schema.py +0 -0
  58. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/tests/test_laguna_m1.py +0 -0
  59. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/tests/test_laguna_xs21.py +0 -0
  60. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/tests/test_llama_3.py +0 -0
  61. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/tests/test_load_tokenizer.py +0 -0
  62. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/tests/test_message_indices.py +0 -0
  63. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/tests/test_message_tool_names.py +0 -0
  64. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/tests/test_multimodal.py +0 -0
  65. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/tests/test_nemotron3_parity.py +0 -0
  66. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/tests/test_nemotron3_ultra.py +0 -0
  67. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/tests/test_parse_response.py +0 -0
  68. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/tests/test_parse_response_robustness.py +0 -0
  69. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/tests/test_parsers.py +0 -0
  70. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/tests/test_preserve_thinking.py +0 -0
  71. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/tests/test_prime_qwen3_parity.py +0 -0
  72. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/tests/test_qwen35_size_coverage.py +0 -0
  73. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/tests/test_render_ids.py +0 -0
  74. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/tests/test_renderer_config.py +0 -0
  75. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/tests/test_roundtrip.py +0 -0
  76. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/tests/test_sampled_mask.py +0 -0
  77. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/tests/test_tokens_per_message.py +0 -0
  78. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/tests/test_tool_arg_type_preservation.py +0 -0
  79. {renderers-0.1.10.dev4 → renderers-0.1.10.dev10}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: renderers
3
- Version: 0.1.10.dev4
3
+ Version: 0.1.10.dev10
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
@@ -56,7 +56,7 @@ next_prompt_ids = r.bridge_to_next_turn(
56
56
  )
57
57
  ```
58
58
 
59
- Hand-coded renderers ship for `qwen3`, `qwen3-vl`, `qwen3.5`, `qwen3.6`, `glm-5`, `glm-5.1`, `glm-4.5`, `minimax-m2`, `deepseek-v3`, `deepseek-r1`, `kimi-k2`, `kimi-k2.5` / `kimi-k2.6`, `laguna-xs.2`, `laguna-xs-2.1`, `laguna-m.1`, `nemotron-3`, `nemotron-3-ultra`, `nemotron-3.5`, `llama-3`, `gpt-oss`, `hy3`, and `prime-qwen3`. Anything else falls back to `DefaultRenderer`, a generic `apply_chat_template` wrapper.
59
+ Hand-coded renderers ship for `qwen3`, `qwen3-vl`, `qwen3.5`, `qwen3.6`, `glm-5`, `glm-5.1`, `glm-4.5`, `minimax-m2`, `deepseek-v3`, `deepseek-r1`, `kimi-k2`, `kimi-k2.5` / `kimi-k2.6`, `laguna-xs.2`, `laguna-xs-2.1`, `laguna-s-2.1`, `laguna-m.1`, `nemotron-3`, `nemotron-3-ultra`, `nemotron-3.5`, `llama-3`, `gpt-oss`, `hy3`, and `prime-qwen3`. Anything else falls back to `DefaultRenderer`, a generic `apply_chat_template` wrapper.
60
60
 
61
61
  ## API
62
62
 
@@ -40,7 +40,7 @@ next_prompt_ids = r.bridge_to_next_turn(
40
40
  )
41
41
  ```
42
42
 
43
- Hand-coded renderers ship for `qwen3`, `qwen3-vl`, `qwen3.5`, `qwen3.6`, `glm-5`, `glm-5.1`, `glm-4.5`, `minimax-m2`, `deepseek-v3`, `deepseek-r1`, `kimi-k2`, `kimi-k2.5` / `kimi-k2.6`, `laguna-xs.2`, `laguna-xs-2.1`, `laguna-m.1`, `nemotron-3`, `nemotron-3-ultra`, `nemotron-3.5`, `llama-3`, `gpt-oss`, `hy3`, and `prime-qwen3`. Anything else falls back to `DefaultRenderer`, a generic `apply_chat_template` wrapper.
43
+ Hand-coded renderers ship for `qwen3`, `qwen3-vl`, `qwen3.5`, `qwen3.6`, `glm-5`, `glm-5.1`, `glm-4.5`, `minimax-m2`, `deepseek-v3`, `deepseek-r1`, `kimi-k2`, `kimi-k2.5` / `kimi-k2.6`, `laguna-xs.2`, `laguna-xs-2.1`, `laguna-s-2.1`, `laguna-m.1`, `nemotron-3`, `nemotron-3-ultra`, `nemotron-3.5`, `llama-3`, `gpt-oss`, `hy3`, and `prime-qwen3`. Anything else falls back to `DefaultRenderer`, a generic `apply_chat_template` wrapper.
44
44
 
45
45
  ## API
46
46
 
@@ -39,6 +39,7 @@ chat-template kwargs. Those fields are covered by parity tests against
39
39
  | Laguna XS.2 | `LagunaXS2RendererConfig` | `enable_thinking`, `render_assistant_messages_raw` | - |
40
40
  | Laguna M.1 | `LagunaM1RendererConfig` | `enable_thinking`, `render_assistant_messages_raw` | - |
41
41
  | Laguna XS-2.1 | `LagunaXS21RendererConfig` | `enable_thinking` | - |
42
+ | Laguna S-2.1 | `LagunaS21RendererConfig` | `enable_thinking`, `preserve_thinking` | - |
42
43
  | Llama 3 | `Llama3RendererConfig` | `date_string`, `tools_in_user_message` | - |
43
44
  | MiniMax M2 | `MiniMaxM2RendererConfig` | `model_identity` | - |
44
45
  | Nemotron-3 Nano / Super | `Nemotron3RendererConfig` | `enable_thinking`, `truncate_history_thinking`, `low_effort` | - |
@@ -138,7 +139,7 @@ the knobs its template actually exposes:
138
139
  | Nemotron-3 / 3.5 | `truncate_history_thinking=False -> all`; else `enable_thinking=False -> all`; else `tool_cycle` |
139
140
  | DeepSeek R1 | `template` |
140
141
  | MiniMax M2 | `tool_cycle` |
141
- | DeepSeek V3, Qwen3-VL, Kimi K2, Laguna XS.2 / M.1 / XS-2.1, Llama 3 | `all` |
142
+ | DeepSeek V3, Qwen3-VL, Kimi K2, Laguna XS.2 / M.1 / XS-2.1 / S-2.1, Llama 3 | `all` |
142
143
  | PrimeIntellect Qwen3 | `all` |
143
144
 
144
145
  Config construction raises when an explicit template knob directly contradicts
@@ -55,6 +55,7 @@ from renderers.configs import (
55
55
  KimiK25RendererConfig,
56
56
  KimiK2RendererConfig,
57
57
  LagunaM1RendererConfig,
58
+ LagunaS21RendererConfig,
58
59
  LagunaXS2RendererConfig,
59
60
  LagunaXS21RendererConfig,
60
61
  Llama3RendererConfig,
@@ -93,6 +94,7 @@ _LAZY_RENDERERS: dict[str, str] = {
93
94
  "KimiK25Renderer": "renderers.kimi_k25",
94
95
  "KimiK2Renderer": "renderers.kimi_k2",
95
96
  "LagunaM1Renderer": "renderers.laguna_xs2",
97
+ "LagunaS21Renderer": "renderers.laguna_s21",
96
98
  "LagunaXS21Renderer": "renderers.laguna_xs2",
97
99
  "LagunaXS2Renderer": "renderers.laguna_xs2",
98
100
  "Llama3Renderer": "renderers.llama_3",
@@ -151,6 +153,8 @@ __all__ = [
151
153
  "KimiK2RendererConfig",
152
154
  "LagunaM1Renderer",
153
155
  "LagunaM1RendererConfig",
156
+ "LagunaS21Renderer",
157
+ "LagunaS21RendererConfig",
154
158
  "LagunaXS2Renderer",
155
159
  "LagunaXS2RendererConfig",
156
160
  "LagunaXS21Renderer",
@@ -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.10.dev4'
22
- __version_tuple__ = version_tuple = (0, 1, 10, 'dev4')
21
+ __version__ = version = '0.1.10.dev10'
22
+ __version_tuple__ = version_tuple = (0, 1, 10, 'dev10')
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -1076,11 +1076,12 @@ MODEL_RENDERER_MAP: dict[str, str] = {
1076
1076
  # construction to pin a different date.
1077
1077
  "meta-llama/Llama-3.2-1B-Instruct": "llama-3",
1078
1078
  "meta-llama/Llama-3.2-3B-Instruct": "llama-3",
1079
- # Poolside Laguna. These checkpoints ship distinct chat templates,
1080
- # each mirrored by its own renderer class/config discriminator.
1079
+ # Poolside Laguna. These checkpoints ship distinct chat templates, each
1080
+ # mirrored by its own renderer class/config discriminator.
1081
1081
  "poolside/Laguna-XS.2": "laguna-xs.2",
1082
1082
  "poolside/Laguna-M.1": "laguna-m.1",
1083
1083
  "poolside/Laguna-XS-2.1": "laguna-xs-2.1",
1084
+ "poolside/Laguna-S-2.1": "laguna-s-2.1",
1084
1085
  # GPT-OSS.
1085
1086
  "openai/gpt-oss-20b": "gpt-oss",
1086
1087
  "openai/gpt-oss-120b": "gpt-oss",
@@ -1328,6 +1329,7 @@ def _populate_registry():
1328
1329
  from renderers.hy3 import Hy3Renderer
1329
1330
  from renderers.kimi_k2 import KimiK2Renderer
1330
1331
  from renderers.kimi_k25 import KimiK25Renderer
1332
+ from renderers.laguna_s21 import LagunaS21Renderer
1331
1333
  from renderers.laguna_xs2 import (
1332
1334
  LagunaM1Renderer,
1333
1335
  LagunaXS2Renderer,
@@ -1366,6 +1368,7 @@ def _populate_registry():
1366
1368
  "laguna-xs.2": LagunaXS2Renderer,
1367
1369
  "laguna-m.1": LagunaM1Renderer,
1368
1370
  "laguna-xs-2.1": LagunaXS21Renderer,
1371
+ "laguna-s-2.1": LagunaS21Renderer,
1369
1372
  "llama-3": Llama3Renderer,
1370
1373
  "nemotron-3": Nemotron3Renderer,
1371
1374
  "nemotron-3-ultra": Nemotron3UltraRenderer,
@@ -546,6 +546,37 @@ class LagunaXS21RendererConfig(BaseRendererConfig):
546
546
  upstream default."""
547
547
 
548
548
 
549
+ class LagunaS21RendererConfig(BaseRendererConfig):
550
+ """Laguna S-2.1 renderer config.
551
+
552
+ S-2.1 is a larger sibling of XS-2.1 sharing its tokenizer (same vocab,
553
+ special tokens, and merges), but its chat template is *not* byte-identical:
554
+ ``enable_thinking`` defaults to ``True`` (XS-2.1 defaults ``False``), and a
555
+ new ``preserve_thinking`` kwarg widens the reasoning-display gate to
556
+ ``enable_thinking or preserve_thinking``. The token format is otherwise
557
+ identical, so this is served by
558
+ :class:`renderers.laguna_s21.LagunaS21Renderer`, a thin subclass of
559
+ ``LagunaXS21Renderer`` that only overrides that gate.
560
+ """
561
+
562
+ name: Literal["laguna-s-2.1"] = "laguna-s-2.1"
563
+
564
+ enable_thinking: bool = True
565
+ """When ``True``, the generation prompt ends with ``<think>`` and every
566
+ assistant turn renders ``<think>{reasoning}</think>``; when ``False``,
567
+ turns open with a bare ``</think>``. Mirrors the template's
568
+ ``enable_thinking`` kwarg — note S-2.1's upstream default is ``True``,
569
+ unlike XS-2.1's ``False``."""
570
+
571
+ preserve_thinking: bool = False
572
+ """When ``True``, assistant turns keep their ``<think>{reasoning}</think>``
573
+ block even while ``enable_thinking`` is ``False`` — the template gates
574
+ reasoning display on ``enable_thinking or preserve_thinking``. With the
575
+ default ``False`` the gate collapses to ``enable_thinking`` and the
576
+ renderer matches XS-2.1 turn-for-turn. Mirrors the template's
577
+ ``preserve_thinking`` kwarg and its upstream default."""
578
+
579
+
549
580
  class Llama3RendererConfig(BaseRendererConfig):
550
581
  """Llama-3.x Instruct renderer config.
551
582
 
@@ -735,6 +766,7 @@ RendererConfig = Annotated[
735
766
  LagunaXS2RendererConfig,
736
767
  LagunaM1RendererConfig,
737
768
  LagunaXS21RendererConfig,
769
+ LagunaS21RendererConfig,
738
770
  Llama3RendererConfig,
739
771
  MiniMaxM2RendererConfig,
740
772
  Nemotron3RendererConfig,
@@ -777,6 +809,7 @@ _CONFIG_BY_NAME: dict[str, type[BaseRendererConfig]] = {
777
809
  "laguna-xs.2": LagunaXS2RendererConfig,
778
810
  "laguna-m.1": LagunaM1RendererConfig,
779
811
  "laguna-xs-2.1": LagunaXS21RendererConfig,
812
+ "laguna-s-2.1": LagunaS21RendererConfig,
780
813
  "llama-3": Llama3RendererConfig,
781
814
  "minimax-m2": MiniMaxM2RendererConfig,
782
815
  "nemotron-3": Nemotron3RendererConfig,
@@ -825,6 +858,7 @@ __all__ = [
825
858
  "KimiK25RendererConfig",
826
859
  "KimiK2RendererConfig",
827
860
  "LagunaM1RendererConfig",
861
+ "LagunaS21RendererConfig",
828
862
  "LagunaXS2RendererConfig",
829
863
  "LagunaXS21RendererConfig",
830
864
  "Llama3RendererConfig",
@@ -0,0 +1,33 @@
1
+ """Laguna S-2.1 Renderer — a larger sibling of Laguna XS-2.1.
2
+
3
+ S-2.1 shares XS-2.1's tokenizer and token format, so this is a thin subclass of
4
+ :class:`renderers.laguna_xs2.LagunaXS21Renderer`; see that module for the shared
5
+ format. The template delta is two thinking kwargs: ``enable_thinking`` defaults
6
+ to ``True`` (XS-2.1 defaults ``False``), and ``preserve_thinking`` widens the
7
+ reasoning-display gate to ``enable_thinking or preserve_thinking``.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from transformers.tokenization_utils import PreTrainedTokenizer
13
+
14
+ from renderers.configs import LagunaS21RendererConfig
15
+ from renderers.laguna_xs2 import LagunaXS21Renderer
16
+
17
+
18
+ class LagunaS21Renderer(LagunaXS21Renderer):
19
+ """Mirrors the ``poolside/Laguna-S-2.1`` chat template."""
20
+
21
+ # Narrows the inherited attribute so the S-2.1-only ``preserve_thinking``
22
+ # resolves; ``__init__`` always stores an S-2.1 config.
23
+ config: LagunaS21RendererConfig
24
+
25
+ def __init__(
26
+ self,
27
+ tokenizer: PreTrainedTokenizer,
28
+ config: LagunaS21RendererConfig | None = None,
29
+ ):
30
+ super().__init__(tokenizer, config or LagunaS21RendererConfig())
31
+
32
+ def _render_history_reasoning(self) -> bool:
33
+ return self.config.enable_thinking or self.config.preserve_thinking
@@ -41,6 +41,9 @@ Laguna M.1's official template (revision ``2bf8a4ab``) is served by
41
41
  :class:`LagunaM1Renderer`. It shares XS.2's byte layout, tool syntax, and
42
42
  generation prompt, but has no fallback system prompt and reads assistant
43
43
  reasoning from ``reasoning`` before falling back to ``reasoning_content``.
44
+
45
+ S-2.1 subclasses :class:`LagunaXS21Renderer` from :mod:`renderers.laguna_s21`,
46
+ overriding only the ``_render_history_reasoning`` seam.
44
47
  """
45
48
 
46
49
  from __future__ import annotations
@@ -63,6 +66,7 @@ from renderers.base import (
63
66
  )
64
67
  from renderers.configs import (
65
68
  LagunaM1RendererConfig,
69
+ LagunaS21RendererConfig,
66
70
  LagunaXS2RendererConfig,
67
71
  LagunaXS21RendererConfig,
68
72
  )
@@ -120,6 +124,7 @@ class LagunaXS2Renderer:
120
124
  LagunaXS2RendererConfig
121
125
  | LagunaM1RendererConfig
122
126
  | LagunaXS21RendererConfig
127
+ | LagunaS21RendererConfig
123
128
  | None
124
129
  ) = None,
125
130
  ):
@@ -688,10 +693,17 @@ class LagunaXS21Renderer(LagunaXS2Renderer):
688
693
  def __init__(
689
694
  self,
690
695
  tokenizer: PreTrainedTokenizer,
691
- config: LagunaXS21RendererConfig | None = None,
696
+ config: LagunaXS21RendererConfig | LagunaS21RendererConfig | None = None,
692
697
  ):
693
698
  super().__init__(tokenizer, config or LagunaXS21RendererConfig())
694
699
 
700
+ def _render_history_reasoning(self) -> bool:
701
+ """Whether an assistant turn renders its ``<think>{reasoning}</think>``
702
+ block (vs opening with a bare ``</think>``). Mirrors the template's
703
+ reasoning-display gate; XS-2.1 gates this on ``enable_thinking`` alone.
704
+ """
705
+ return self.config.enable_thinking
706
+
695
707
  def render(
696
708
  self,
697
709
  messages: list[Message],
@@ -975,7 +987,7 @@ class LagunaXS21Renderer(LagunaXS2Renderer):
975
987
  # scaffolding the model never samples.
976
988
  emit_special(self._assistant, msg_idx, is_sampled=False, is_content=False)
977
989
 
978
- if self.config.enable_thinking:
990
+ if self._render_history_reasoning():
979
991
  # ``<think>{reasoning}</think>`` renders verbatim, even when
980
992
  # the reasoning is empty; the opener is the gen-prompt
981
993
  # prefill, the rest the model sampled.
@@ -44,6 +44,10 @@ RENDERER_MODELS = [
44
44
  # XS-2.1 resolves to `LagunaXS21Renderer` via the model name — its
45
45
  # chat template differs from XS.2's (see renderers/laguna_xs2.py).
46
46
  ("poolside/Laguna-XS-2.1", "auto"),
47
+ # S-2.1 is a larger sibling of XS-2.1 sharing its tokenizer but NOT its
48
+ # chat template (S-2.1 defaults enable_thinking=True and adds a
49
+ # preserve_thinking gate), so it resolves to LagunaS21Renderer.
50
+ ("poolside/Laguna-S-2.1", "auto"),
47
51
  # DeepSeek-V3/R1 are intentionally NOT in this shared barrage: their
48
52
  # chat templates can't render the barrage's tool-call fixtures (the
49
53
  # templates require ``tool['type']`` and a string-serialized
@@ -0,0 +1,128 @@
1
+ """Laguna-S-2.1 focused tests.
2
+
3
+ S-2.1 shares XS-2.1's tokenizer and token format, so the shared matrices
4
+ (conftest / config-parity) already assert byte parity on the common shapes
5
+ via :class:`LagunaS21Renderer`. This file pins the two behaviours that make
6
+ S-2.1 *differ* from XS-2.1 — and that the shared barrage can't reach because
7
+ it never sets the relevant kwargs:
8
+
9
+ - ``enable_thinking`` defaults to ``True`` (XS-2.1 defaults ``False``), so the
10
+ auto-resolved renderer renders reasoning by default, and
11
+ - the new ``preserve_thinking`` kwarg keeps historical ``<think>`` blocks even
12
+ while ``enable_thinking`` is ``False`` — the template gates reasoning display
13
+ on ``enable_thinking or preserve_thinking``.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from functools import lru_cache
19
+ from itertools import product
20
+
21
+ from renderers import create_renderer
22
+ from renderers.base import load_tokenizer
23
+ from renderers.configs import LagunaS21RendererConfig
24
+ from renderers.laguna_s21 import LagunaS21Renderer
25
+
26
+ _MODEL = "poolside/Laguna-S-2.1"
27
+
28
+
29
+ @lru_cache(maxsize=None)
30
+ def _tok():
31
+ return load_tokenizer(_MODEL)
32
+
33
+
34
+ def _renderer(**config_kwargs) -> LagunaS21Renderer:
35
+ renderer = create_renderer(_tok(), LagunaS21RendererConfig(**config_kwargs))
36
+ assert isinstance(renderer, LagunaS21Renderer)
37
+ return renderer
38
+
39
+
40
+ def _expected(msgs, *, add_generation_prompt=False, **template_kwargs):
41
+ return list(
42
+ _tok().apply_chat_template(
43
+ msgs,
44
+ add_generation_prompt=add_generation_prompt,
45
+ tokenize=True,
46
+ return_dict=False,
47
+ **template_kwargs,
48
+ )
49
+ )
50
+
51
+
52
+ def test_auto_resolves_to_s21_renderer_with_thinking_on_by_default():
53
+ """``poolside/Laguna-S-2.1`` resolves to :class:`LagunaS21Renderer`, and
54
+ its config defaults ``enable_thinking=True`` / ``preserve_thinking=False``
55
+ — matching S-2.1's template defaults (unlike XS-2.1's thinking-off)."""
56
+ r = create_renderer(_tok()) # auto-resolve via MODEL_RENDERER_MAP
57
+ assert isinstance(r, LagunaS21Renderer)
58
+ assert r.config.name == "laguna-s-2.1"
59
+ assert r.config.enable_thinking is True
60
+ assert r.config.preserve_thinking is False
61
+
62
+
63
+ def test_default_renders_empty_think_wrapper():
64
+ """With thinking on by default, a reasoning-free assistant turn still
65
+ opens with the empty ``<think></think>`` wrapper (this is exactly the
66
+ render_ids divergence from the XS-2.1 renderer)."""
67
+ msgs = [
68
+ {"role": "user", "content": "What is 2+2?"},
69
+ {"role": "assistant", "content": "4"},
70
+ ]
71
+ r = _renderer() # defaults: enable_thinking=True
72
+ ours = r.render_ids(msgs)
73
+ assert ours == _expected(msgs)
74
+ assert "<assistant><think></think>4</assistant>" in _tok().decode(ours)
75
+
76
+
77
+ def test_preserve_thinking_keeps_history_when_thinking_off():
78
+ """``enable_thinking=False`` alone drops reasoning, but with
79
+ ``preserve_thinking=True`` the historical ``<think>{reasoning}</think>``
80
+ survives — matching the template's ``enable_thinking or preserve_thinking``
81
+ gate."""
82
+ msgs = [
83
+ {"role": "user", "content": "What is 2+2?"},
84
+ {"role": "assistant", "reasoning_content": "Simple arithmetic", "content": "4"},
85
+ ]
86
+ r = _renderer(enable_thinking=False, preserve_thinking=True)
87
+ ours = r.render_ids(msgs)
88
+ assert ours == _expected(msgs, enable_thinking=False, preserve_thinking=True)
89
+ assert "<think>Simple arithmetic</think>" in _tok().decode(ours)
90
+
91
+
92
+ def test_no_preserve_thinking_drops_history_when_thinking_off():
93
+ """With both flags off the gate collapses to ``enable_thinking`` and the
94
+ turn opens with a bare ``</think>``, dropping the reasoning — identical to
95
+ the XS-2.1 renderer's default."""
96
+ msgs = [
97
+ {"role": "user", "content": "What is 2+2?"},
98
+ {"role": "assistant", "reasoning_content": "Simple arithmetic", "content": "4"},
99
+ ]
100
+ r = _renderer(enable_thinking=False, preserve_thinking=False)
101
+ ours = r.render_ids(msgs)
102
+ assert ours == _expected(msgs, enable_thinking=False, preserve_thinking=False)
103
+ assert "Simple arithmetic" not in _tok().decode(ours)
104
+
105
+
106
+ def test_all_thinking_flag_combos_match_template():
107
+ """Byte parity against ``apply_chat_template`` for every
108
+ ``enable_thinking`` × ``preserve_thinking`` combination, over a multi-turn
109
+ conversation carrying historical reasoning."""
110
+ msgs = [
111
+ {"role": "user", "content": "Q1"},
112
+ {"role": "assistant", "reasoning_content": "deduce", "content": "A1"},
113
+ {"role": "user", "content": "Q2"},
114
+ {"role": "assistant", "reasoning_content": "more", "content": "A2"},
115
+ ]
116
+ for enable_thinking, preserve_thinking in product((False, True), repeat=2):
117
+ r = _renderer(
118
+ enable_thinking=enable_thinking, preserve_thinking=preserve_thinking
119
+ )
120
+ for add_generation_prompt in (False, True):
121
+ assert r.render_ids(
122
+ msgs, add_generation_prompt=add_generation_prompt
123
+ ) == _expected(
124
+ msgs,
125
+ add_generation_prompt=add_generation_prompt,
126
+ enable_thinking=enable_thinking,
127
+ preserve_thinking=preserve_thinking,
128
+ ), (enable_thinking, preserve_thinking, add_generation_prompt)
@@ -69,6 +69,7 @@ _RENDERER_MODELS = [
69
69
  ("poolside/Laguna-XS.2", "auto"),
70
70
  ("poolside/Laguna-M.1", "auto"),
71
71
  ("poolside/Laguna-XS-2.1", "auto"),
72
+ ("poolside/Laguna-S-2.1", "auto"),
72
73
  ("tencent/Hy3", "auto"),
73
74
  ("openai/gpt-oss-20b", "gpt-oss"),
74
75
  ]