renderers 0.1.8.dev54__tar.gz → 0.1.8.dev56__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 (72) hide show
  1. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/PKG-INFO +2 -2
  2. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/README.md +1 -1
  3. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/docs/renderer-config.md +4 -1
  4. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/renderers/__init__.py +6 -0
  5. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/renderers/_version.py +2 -2
  6. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/renderers/base.py +37 -5
  7. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/renderers/configs.py +78 -13
  8. renderers-0.1.8.dev56/renderers/hy3.py +706 -0
  9. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/renderers/laguna_xs2.py +384 -13
  10. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/renderers/parsing.py +226 -16
  11. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/tests/conftest.py +3 -2
  12. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/tests/test_bridge.py +2 -0
  13. renderers-0.1.8.dev56/tests/test_hy3.py +486 -0
  14. renderers-0.1.8.dev56/tests/test_laguna_xs21.py +376 -0
  15. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/tests/test_renderer_config_parity.py +35 -1
  16. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/tests/test_roundtrip.py +5 -0
  17. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/.github/workflows/publish-dev.yml +0 -0
  18. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/.github/workflows/publish.yml +0 -0
  19. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/.github/workflows/style.yml +0 -0
  20. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/.github/workflows/test.yml +0 -0
  21. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/.gitignore +0 -0
  22. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/.pre-commit-config.yaml +0 -0
  23. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/LICENSE +0 -0
  24. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/examples/README.md +0 -0
  25. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/examples/sglang/multiturn_generate_sglang.py +0 -0
  26. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/examples/sglang/online_multiturn_sglang.py +0 -0
  27. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/examples/tinker/multiturn_generate_tinker.py +0 -0
  28. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/examples/transformers/multiturn_generate_transformers.py +0 -0
  29. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/examples/vllm/multiturn_generate_vllm.py +0 -0
  30. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/pyproject.toml +0 -0
  31. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/renderers/client.py +0 -0
  32. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/renderers/deepseek_r1.py +0 -0
  33. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/renderers/deepseek_v3.py +0 -0
  34. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/renderers/default.py +0 -0
  35. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/renderers/glm45.py +0 -0
  36. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/renderers/glm5.py +0 -0
  37. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/renderers/gpt_oss.py +0 -0
  38. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/renderers/kimi_k2.py +0 -0
  39. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/renderers/kimi_k25.py +0 -0
  40. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/renderers/llama_3.py +0 -0
  41. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/renderers/minimax_m2.py +0 -0
  42. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/renderers/nemotron3.py +0 -0
  43. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/renderers/parsers.py +0 -0
  44. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/renderers/qwen3.py +0 -0
  45. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/renderers/qwen35.py +0 -0
  46. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/renderers/qwen36.py +0 -0
  47. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/renderers/qwen3_vl.py +0 -0
  48. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/tests/test_build_helpers.py +0 -0
  49. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/tests/test_client.py +0 -0
  50. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/tests/test_deepseek_r1.py +0 -0
  51. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/tests/test_gpt_oss_harmony_parity.py +0 -0
  52. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/tests/test_incremental.py +0 -0
  53. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/tests/test_is_content.py +0 -0
  54. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/tests/test_kimi_k25_tool_schema.py +0 -0
  55. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/tests/test_llama_3.py +0 -0
  56. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/tests/test_load_tokenizer.py +0 -0
  57. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/tests/test_message_indices.py +0 -0
  58. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/tests/test_message_tool_names.py +0 -0
  59. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/tests/test_multimodal.py +0 -0
  60. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/tests/test_nemotron3_parity.py +0 -0
  61. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/tests/test_nemotron3_ultra.py +0 -0
  62. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/tests/test_parse_response.py +0 -0
  63. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/tests/test_parse_response_robustness.py +0 -0
  64. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/tests/test_parsers.py +0 -0
  65. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/tests/test_preserve_thinking.py +0 -0
  66. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/tests/test_qwen35_size_coverage.py +0 -0
  67. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/tests/test_render_ids.py +0 -0
  68. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/tests/test_renderer_config.py +0 -0
  69. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/tests/test_sampled_mask.py +0 -0
  70. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/tests/test_tokens_per_message.py +0 -0
  71. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/tests/test_tool_arg_type_preservation.py +0 -0
  72. {renderers-0.1.8.dev54 → renderers-0.1.8.dev56}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: renderers
3
- Version: 0.1.8.dev54
3
+ Version: 0.1.8.dev56
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`, `nemotron-3`, `nemotron-3-ultra`, `llama-3`, and `gpt-oss`. 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`, `nemotron-3`, `nemotron-3-ultra`, `llama-3`, `gpt-oss`, and `hy3`. 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`, `nemotron-3`, `nemotron-3-ultra`, `llama-3`, and `gpt-oss`. 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`, `nemotron-3`, `nemotron-3-ultra`, `llama-3`, `gpt-oss`, and `hy3`. Anything else falls back to `DefaultRenderer`, a generic `apply_chat_template` wrapper.
44
44
 
45
45
  ## API
46
46
 
@@ -32,9 +32,11 @@ chat-template kwargs. Those fields are covered by parity tests against
32
32
  | GLM-5 / 5.1 | `GLM5RendererConfig` / `GLM51RendererConfig` | `enable_thinking`, `clear_thinking` | - |
33
33
  | GLM-4.5 | `GLM45RendererConfig` | `enable_thinking` | - |
34
34
  | gpt-oss | `GptOssRendererConfig` | `reasoning_effort`, `conversation_start_date` | `use_system_prompt`, `knowledge_cutoff`, `model_identity`, `auto_drop_analysis` |
35
+ | Hy3 | `Hy3RendererConfig` | `reasoning_effort`, `preserved_thinking`, `is_training`, `raw_last_assistant`, `fallback_strategy` | - |
35
36
  | Kimi K2 | `KimiK2RendererConfig` | - | `enable_thinking` |
36
37
  | Kimi K2.5 / 2.6 | `KimiK25RendererConfig` | `thinking` | `image_cache_max` |
37
38
  | Laguna XS.2 | `LagunaXS2RendererConfig` | `enable_thinking`, `render_assistant_messages_raw` | - |
39
+ | Laguna XS-2.1 | `LagunaXS21RendererConfig` | `enable_thinking` | - |
38
40
  | Llama 3 | `Llama3RendererConfig` | `date_string`, `tools_in_user_message` | - |
39
41
  | MiniMax M2 | `MiniMaxM2RendererConfig` | `model_identity` | - |
40
42
  | Nemotron-3 Nano / Super | `Nemotron3RendererConfig` | `enable_thinking`, `truncate_history_thinking`, `low_effort` | - |
@@ -128,11 +130,12 @@ the knobs its template actually exposes:
128
130
  | GLM-5 / 5.1 | `clear_thinking=False -> all`; else `enable_thinking=False -> all`; else `tool_cycle` |
129
131
  | GLM-4.5 | `enable_thinking=False -> all`, else `tool_cycle` |
130
132
  | gpt-oss | `auto_drop_analysis=False -> all`, else `tool_cycle` |
133
+ | Hy3 | `preserved_thinking=True -> all`, else `tool_cycle` |
131
134
  | Kimi K2.5 / 2.6 | `thinking=False -> all`, else `tool_cycle` |
132
135
  | Nemotron-3 | `truncate_history_thinking=False -> all`; else `enable_thinking=False -> all`; else `tool_cycle` |
133
136
  | DeepSeek R1 | `template` |
134
137
  | MiniMax M2 | `tool_cycle` |
135
- | DeepSeek V3, Qwen3-VL, Kimi K2, Laguna XS.2, Llama 3 | `all` |
138
+ | DeepSeek V3, Qwen3-VL, Kimi K2, Laguna XS.2 / XS-2.1, Llama 3 | `all` |
136
139
 
137
140
  Config construction raises when an explicit template knob directly contradicts
138
141
  an explicit generic bridge policy. For example:
@@ -51,6 +51,7 @@ from renderers.configs import (
51
51
  GLM51RendererConfig,
52
52
  GLM5RendererConfig,
53
53
  GptOssRendererConfig,
54
+ Hy3RendererConfig,
54
55
  KimiK25RendererConfig,
55
56
  KimiK2RendererConfig,
56
57
  LagunaXS2RendererConfig,
@@ -85,8 +86,10 @@ _LAZY_RENDERERS: dict[str, str] = {
85
86
  "GLM51Renderer": "renderers.glm5",
86
87
  "GLM5Renderer": "renderers.glm5",
87
88
  "GptOssRenderer": "renderers.gpt_oss",
89
+ "Hy3Renderer": "renderers.hy3",
88
90
  "KimiK25Renderer": "renderers.kimi_k25",
89
91
  "KimiK2Renderer": "renderers.kimi_k2",
92
+ "LagunaXS21Renderer": "renderers.laguna_xs2",
90
93
  "LagunaXS2Renderer": "renderers.laguna_xs2",
91
94
  "Llama3Renderer": "renderers.llama_3",
92
95
  "MiniMaxM2Renderer": "renderers.minimax_m2",
@@ -133,6 +136,8 @@ __all__ = [
133
136
  "GLM5RendererConfig",
134
137
  "GptOssRenderer",
135
138
  "GptOssRendererConfig",
139
+ "Hy3Renderer",
140
+ "Hy3RendererConfig",
136
141
  "ImagePart",
137
142
  "KimiK25Renderer",
138
143
  "KimiK25RendererConfig",
@@ -140,6 +145,7 @@ __all__ = [
140
145
  "KimiK2RendererConfig",
141
146
  "LagunaXS2Renderer",
142
147
  "LagunaXS2RendererConfig",
148
+ "LagunaXS21Renderer",
143
149
  "LagunaXS21RendererConfig",
144
150
  "Llama3Renderer",
145
151
  "Llama3RendererConfig",
@@ -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.dev54'
22
- __version_tuple__ = version_tuple = (0, 1, 8, 'dev54')
21
+ __version__ = version = '0.1.8.dev56'
22
+ __version_tuple__ = version_tuple = (0, 1, 8, 'dev56')
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -1060,13 +1060,19 @@ 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. XS-2.1's template is byte-identical to XS.2's minus
1064
- # the default system message; the config name selects the variant.
1063
+ # Poolside Laguna. The two checkpoints ship different chat templates,
1064
+ # each mirrored by its own renderer class.
1065
1065
  "poolside/Laguna-XS.2": "laguna-xs.2",
1066
1066
  "poolside/Laguna-XS-2.1": "laguna-xs-2.1",
1067
1067
  # GPT-OSS.
1068
1068
  "openai/gpt-oss-20b": "gpt-oss",
1069
1069
  "openai/gpt-oss-120b": "gpt-oss",
1070
+ # Tencent Hunyuan Hy3 (295B-A21B MoE). The FP8 checkpoint shares the same
1071
+ # tokenizer and chat template. Hy3-preview is deliberately unmapped: it
1072
+ # ships an older, incompatible template (un-suffixed special tokens,
1073
+ # ``interleaved_thinking`` instead of ``preserved_thinking``).
1074
+ "tencent/Hy3": "hy3",
1075
+ "tencent/Hy3-FP8": "hy3",
1070
1076
  }
1071
1077
 
1072
1078
 
@@ -1302,9 +1308,10 @@ def _populate_registry():
1302
1308
  from renderers.glm5 import GLM5Renderer, GLM51Renderer
1303
1309
  from renderers.glm45 import GLM45Renderer
1304
1310
  from renderers.gpt_oss import GptOssRenderer
1311
+ from renderers.hy3 import Hy3Renderer
1305
1312
  from renderers.kimi_k2 import KimiK2Renderer
1306
1313
  from renderers.kimi_k25 import KimiK25Renderer
1307
- from renderers.laguna_xs2 import LagunaXS2Renderer
1314
+ from renderers.laguna_xs2 import LagunaXS2Renderer, LagunaXS21Renderer
1308
1315
  from renderers.llama_3 import Llama3Renderer
1309
1316
  from renderers.minimax_m2 import MiniMaxM2Renderer
1310
1317
  from renderers.nemotron3 import Nemotron3Renderer, Nemotron3UltraRenderer
@@ -1326,10 +1333,11 @@ def _populate_registry():
1326
1333
  "minimax-m2": MiniMaxM2Renderer,
1327
1334
  "deepseek-v3": DeepSeekV3Renderer,
1328
1335
  "deepseek-r1": DeepSeekR1Renderer,
1336
+ "hy3": Hy3Renderer,
1329
1337
  "kimi-k2": KimiK2Renderer,
1330
1338
  "kimi-k2.5": KimiK25Renderer,
1331
1339
  "laguna-xs.2": LagunaXS2Renderer,
1332
- "laguna-xs-2.1": LagunaXS2Renderer,
1340
+ "laguna-xs-2.1": LagunaXS21Renderer,
1333
1341
  "llama-3": Llama3Renderer,
1334
1342
  "nemotron-3": Nemotron3Renderer,
1335
1343
  "nemotron-3-ultra": Nemotron3UltraRenderer,
@@ -1764,6 +1772,8 @@ def _get_offset_tokenizer(tokenizer):
1764
1772
  def attribute_text_segments(
1765
1773
  tokenizer,
1766
1774
  segments: "list[tuple[str, bool]]",
1775
+ *,
1776
+ overlap_is_content: bool = False,
1767
1777
  ) -> "list[tuple[int, bool]]":
1768
1778
  """Tokenize concatenated segments as a single BPE pass and return
1769
1779
  ``(token_id, is_content)`` pairs.
@@ -1782,6 +1792,16 @@ def attribute_text_segments(
1782
1792
  tokens (rare; usually pre-tokenizer artefacts) are attributed to
1783
1793
  the most recently entered segment.
1784
1794
 
1795
+ ``overlap_is_content=True`` widens the content bit: a token counts
1796
+ as content when *any* of its source characters fall in a content
1797
+ segment, not just its first. Templates whose wrap glues directly
1798
+ onto the body with no whitespace (e.g. ``<user>{content}</user>``)
1799
+ can merge wrap and body bytes into one token; under the first-char
1800
+ policy such a token would land on the wrap side and the body would
1801
+ no longer be recoverable from the content run. Over-inclusion keeps
1802
+ every body byte inside the ``is_content=True`` run at the cost of a
1803
+ few adjacent wrap bytes.
1804
+
1785
1805
  Requires a HuggingFace fast tokenizer with offset tracking. Every
1786
1806
  model in ``MODEL_RENDERER_MAP`` ships one, so the offset lookup
1787
1807
  always succeeds for tokenizers obtained via :func:`load_tokenizer`.
@@ -1818,13 +1838,25 @@ def attribute_text_segments(
1818
1838
 
1819
1839
  out: list[tuple[int, bool]] = []
1820
1840
  last_is_content = spans[-1][2] if spans else False
1821
- for tok_id, (start, _end) in zip(token_ids, offsets):
1841
+ for tok_id, (start, end) in zip(token_ids, offsets):
1822
1842
  if start >= total_len:
1823
1843
  # Token's character offset is past every segment (shouldn't
1824
1844
  # normally happen for add_special_tokens=False, but defensive
1825
1845
  # against tokenizer-specific edge cases).
1826
1846
  out.append((tok_id, last_is_content))
1827
1847
  continue
1848
+ if overlap_is_content and end > start:
1849
+ out.append(
1850
+ (
1851
+ tok_id,
1852
+ any(
1853
+ seg_is_content
1854
+ for seg_start, seg_end, seg_is_content in spans
1855
+ if seg_start < end and start < seg_end
1856
+ ),
1857
+ )
1858
+ )
1859
+ continue
1828
1860
  # Find the segment that contains `start`. Segments are
1829
1861
  # contiguous and ordered, so a linear scan is fine — the inner
1830
1862
  # loop runs at most len(segments) times per token and segments
@@ -310,6 +310,70 @@ class GLM45RendererConfig(BaseRendererConfig):
310
310
  the chat template's ``enable_thinking`` kwarg."""
311
311
 
312
312
 
313
+ class Hy3RendererConfig(BaseRendererConfig):
314
+ """Tencent Hunyuan Hy3 renderer config.
315
+
316
+ Hy3 reasons via a ``reasoning_effort`` gate rather than a boolean
317
+ ``enable_thinking``. ``"no_think"`` (the template default) prefills an
318
+ empty ``<think></think>`` at the generation prompt so the model answers
319
+ directly; ``"low"`` / ``"high"`` prefill only the ``<think>`` opener so
320
+ the model streams reasoning up to a ``</think>`` it emits itself.
321
+
322
+ ``preserved_thinking`` mirrors the template kwarg of the same name:
323
+ ``True`` keeps ``<think>{reasoning}</think>`` on every historical
324
+ assistant turn; ``False`` collapses past-cycle reasoning to
325
+ ``<think></think>``, keeping it only on the in-flight turn (after the
326
+ last user query). ``None`` (default) follows the template's own
327
+ default — ``True`` when ``tools`` are supplied at render time, ``False``
328
+ otherwise. Bridge policy tracks the same resolution: ``"all"`` whenever
329
+ ``preserved_thinking`` resolves to ``True`` for the tools at hand,
330
+ ``"tool_cycle"`` otherwise.
331
+ """
332
+
333
+ name: Literal["hy3"] = "hy3"
334
+
335
+ reasoning_effort: Literal["no_think", "low", "high"] = "no_think"
336
+ """Reasoning gate. Mirrors the chat template's ``reasoning_effort`` kwarg.
337
+ ``"no_think"`` prefills ``<think></think>`` at the generation prompt;
338
+ ``"low"`` / ``"high"`` prefill just ``<think>``."""
339
+
340
+ preserved_thinking: bool | None = None
341
+ """Keep historical assistant reasoning. Mirrors the template's
342
+ ``preserved_thinking`` kwarg. ``None`` defers to the template default
343
+ (``True`` with tools, ``False`` without). Bridge policy is ``"all"``
344
+ whenever this resolves to ``True`` for the tools at hand."""
345
+
346
+ is_training: bool = False
347
+ """Mirrors the template's ``is_training`` kwarg. ``True`` renders SFT
348
+ targets: reasoning is kept on every assistant turn (regardless of
349
+ ``preserved_thinking`` / position) and the final assistant is terminated
350
+ with ``<|hy_eos|>``. Leave ``False`` for inference-faithful renders;
351
+ the training loss mask is normally derived via ``build_training_sample``
352
+ rather than this flag."""
353
+
354
+ raw_last_assistant: bool = False
355
+ """Mirrors the template's ``raw_last_assistant`` kwarg. When ``True`` a
356
+ trailing non-tool assistant message is emitted as raw visible content —
357
+ no ``<think>`` wrap, no ``<|hy_eos|>`` — for prefill / continuation."""
358
+
359
+ fallback_strategy: Literal["reasoning_toolcall_retry"] | None = None
360
+ """Mirrors the template's ``fallback_strategy`` kwarg. The sole active
361
+ value, ``"reasoning_toolcall_retry"``, forces ``reasoning_effort="high"``
362
+ and suppresses the generation prompt (``add_generation_prompt=False``)."""
363
+
364
+ @model_validator(mode="after")
365
+ def _check_thinking_retention(self):
366
+ if self.preserved_thinking is not None and self.thinking_retention is not None:
367
+ implied = "all" if self.preserved_thinking else "tool_cycle"
368
+ if self.thinking_retention != implied:
369
+ raise ValueError(
370
+ f"preserved_thinking={self.preserved_thinking!r} implies "
371
+ f"thinking_retention={implied!r}, which conflicts with "
372
+ f"explicit thinking_retention={self.thinking_retention!r}."
373
+ )
374
+ return self
375
+
376
+
313
377
  class GptOssRendererConfig(BaseRendererConfig):
314
378
  """OpenAI gpt-oss (harmony) renderer config.
315
379
 
@@ -423,24 +487,22 @@ class LagunaXS2RendererConfig(BaseRendererConfig):
423
487
 
424
488
 
425
489
  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``.
490
+ """Laguna XS-2.1 renderer config.
491
+
492
+ XS-2.1's chat template reads a single kwarg, ``enable_thinking``,
493
+ which gates both the generation prompt (``<think>`` vs ``</think>``)
494
+ and whether assistant reasoning is rendered into the history at all.
495
+ Served by :class:`renderers.laguna_xs2.LagunaXS21Renderer`.
435
496
  """
436
497
 
437
498
  name: Literal["laguna-xs-2.1"] = "laguna-xs-2.1"
438
499
 
439
500
  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`."""
501
+ """When ``True``, the generation prompt ends with ``<think>`` and
502
+ every assistant turn renders ``<think>{reasoning}</think>``; when
503
+ ``False``, turns open with a bare ``</think>`` and reasoning is not
504
+ rendered. Mirrors the template's ``enable_thinking`` kwarg and its
505
+ upstream default."""
444
506
 
445
507
 
446
508
  class Llama3RendererConfig(BaseRendererConfig):
@@ -594,6 +656,7 @@ RendererConfig = Annotated[
594
656
  GLM51RendererConfig,
595
657
  GLM45RendererConfig,
596
658
  GptOssRendererConfig,
659
+ Hy3RendererConfig,
597
660
  KimiK2RendererConfig,
598
661
  KimiK25RendererConfig,
599
662
  LagunaXS2RendererConfig,
@@ -632,6 +695,7 @@ _CONFIG_BY_NAME: dict[str, type[BaseRendererConfig]] = {
632
695
  "glm-5.1": GLM51RendererConfig,
633
696
  "glm-4.5": GLM45RendererConfig,
634
697
  "gpt-oss": GptOssRendererConfig,
698
+ "hy3": Hy3RendererConfig,
635
699
  "kimi-k2": KimiK2RendererConfig,
636
700
  "kimi-k2.5": KimiK25RendererConfig,
637
701
  "laguna-xs.2": LagunaXS2RendererConfig,
@@ -679,6 +743,7 @@ __all__ = [
679
743
  "GLM51RendererConfig",
680
744
  "GLM5RendererConfig",
681
745
  "GptOssRendererConfig",
746
+ "Hy3RendererConfig",
682
747
  "KimiK25RendererConfig",
683
748
  "KimiK2RendererConfig",
684
749
  "LagunaXS2RendererConfig",