From cdc158e72fd223b2896f31089f989b002d1fc8aa Mon Sep 17 00:00:00 2001
From: eexwhyzee <13672394+eexwhyzee@users.noreply.github.com>
Date: Mon, 20 Jul 2026 20:38:24 -0700
Subject: [PATCH 1/2] feat(laguna): route poolside/Laguna-S-2.1 to the XS-2.1
renderer
S-2.1 is a larger sibling of XS-2.1 (48 layers, hidden 3072, top-10 of
256 experts) but ships a byte-identical chat_template.jinja and tokenizer
(same special tokens/ids, same vocab). It reuses LagunaXS21Renderer via an
exact-match MODEL_RENDERER_MAP entry; no new renderer/config/parser needed.
Added to the shared render-parity matrices (config-parity, is_content,
sampled_mask); token-exact parity against apply_chat_template verified for
both enable_thinking polarities across all conversation shapes.
---
renderers/base.py | 7 +++++--
tests/conftest.py | 3 +++
tests/test_renderer_config_parity.py | 1 +
3 files changed, 9 insertions(+), 2 deletions(-)
diff --git a/renderers/base.py b/renderers/base.py
index 304d6cc..f235fea 100644
--- a/renderers/base.py
+++ b/renderers/base.py
@@ -1064,10 +1064,13 @@ def bridge_to_next_turn(self, *args: Any, **kwargs: Any) -> "RenderedTokens | No
# construction to pin a different date.
"meta-llama/Llama-3.2-1B-Instruct": "llama-3",
"meta-llama/Llama-3.2-3B-Instruct": "llama-3",
- # Poolside Laguna. The two checkpoints ship different chat templates,
- # each mirrored by its own renderer class.
+ # Poolside Laguna. XS.2 and XS-2.1 ship different chat templates, each
+ # mirrored by its own renderer class. S-2.1 is a larger sibling of XS-2.1
+ # with a byte-identical chat template and tokenizer, so it reuses the
+ # XS-2.1 renderer.
"poolside/Laguna-XS.2": "laguna-xs.2",
"poolside/Laguna-XS-2.1": "laguna-xs-2.1",
+ "poolside/Laguna-S-2.1": "laguna-xs-2.1",
# GPT-OSS.
"openai/gpt-oss-20b": "gpt-oss",
"openai/gpt-oss-120b": "gpt-oss",
diff --git a/tests/conftest.py b/tests/conftest.py
index bd7672e..9fed2db 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -40,6 +40,9 @@
# XS-2.1 resolves to `LagunaXS21Renderer` via the model name — its
# chat template differs from XS.2's (see renderers/laguna_xs2.py).
("poolside/Laguna-XS-2.1", "auto"),
+ # S-2.1 is a larger sibling of XS-2.1 with a byte-identical chat template
+ # and tokenizer, so it reuses the XS-2.1 renderer.
+ ("poolside/Laguna-S-2.1", "auto"),
# DeepSeek-V3/R1 are intentionally NOT in this shared barrage: their
# chat templates can't render the barrage's tool-call fixtures (the
# templates require ``tool['type']`` and a string-serialized
diff --git a/tests/test_renderer_config_parity.py b/tests/test_renderer_config_parity.py
index 21e7c2a..ddd5a04 100644
--- a/tests/test_renderer_config_parity.py
+++ b/tests/test_renderer_config_parity.py
@@ -65,6 +65,7 @@
("nvidia/NVIDIA-Nemotron-3-Ultra-550B-A55B-BF16", "auto"),
("poolside/Laguna-XS.2", "auto"),
("poolside/Laguna-XS-2.1", "auto"),
+ ("poolside/Laguna-S-2.1", "auto"),
("tencent/Hy3", "auto"),
("openai/gpt-oss-20b", "gpt-oss"),
]
From 058218d62f4ca7fd5c4bcccfb5d0fe9199ed09e1 Mon Sep 17 00:00:00 2001
From: eexwhyzee <13672394+eexwhyzee@users.noreply.github.com>
Date: Fri, 24 Jul 2026 11:32:11 -0700
Subject: [PATCH 2/2] fix(laguna): give Laguna-S-2.1 its own renderer for its
distinct template
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The initial routing (cdc158e) assumed S-2.1 shipped a byte-identical chat
template to XS-2.1 and reused LagunaXS21Renderer. It doesn't: S-2.1 shares
XS-2.1's tokenizer (same vocab/merges/special tokens) but its template
defaults enable_thinking to True (XS-2.1 defaults False) and adds a
preserve_thinking kwarg, widening the reasoning-display gate to
`enable_thinking or preserve_thinking`. Reusing the XS-2.1 renderer mismatched
apply_chat_template on the default flag (bare `` vs empty
`` wrapper), failing test_render_ids / test_build_helpers.
The config-parity barrage missed this because it always sets enable_thinking
explicitly, never exercising the differing default.
Add LagunaS21RendererConfig (enable_thinking=True, preserve_thinking=False) and
LagunaS21Renderer, a thin LagunaXS21Renderer subclass that overrides only the
reasoning-display gate. Route poolside/Laguna-S-2.1 to it. The token format is
otherwise identical, so everything else is inherited.
Verified token-exact against apply_chat_template for all enable_thinking ×
preserve_thinking combinations; added tests/test_laguna_s21.py for the
S-2.1-specific behaviours the shared barrage can't reach.
Co-Authored-By: Claude Opus 4.8 (1M context)
---
renderers/__init__.py | 4 ++
renderers/base.py | 15 +++--
renderers/configs.py | 34 +++++++++++
renderers/laguna_xs2.py | 62 +++++++++++++++++--
tests/conftest.py | 5 +-
tests/test_laguna_s21.py | 128 +++++++++++++++++++++++++++++++++++++++
6 files changed, 238 insertions(+), 10 deletions(-)
create mode 100644 tests/test_laguna_s21.py
diff --git a/renderers/__init__.py b/renderers/__init__.py
index 7f7a75e..626d0cb 100644
--- a/renderers/__init__.py
+++ b/renderers/__init__.py
@@ -54,6 +54,7 @@
Hy3RendererConfig,
KimiK25RendererConfig,
KimiK2RendererConfig,
+ LagunaS21RendererConfig,
LagunaXS2RendererConfig,
LagunaXS21RendererConfig,
Llama3RendererConfig,
@@ -90,6 +91,7 @@
"Hy3Renderer": "renderers.hy3",
"KimiK25Renderer": "renderers.kimi_k25",
"KimiK2Renderer": "renderers.kimi_k2",
+ "LagunaS21Renderer": "renderers.laguna_xs2",
"LagunaXS21Renderer": "renderers.laguna_xs2",
"LagunaXS2Renderer": "renderers.laguna_xs2",
"Llama3Renderer": "renderers.llama_3",
@@ -145,6 +147,8 @@ def __dir__() -> list[str]:
"KimiK25RendererConfig",
"KimiK2Renderer",
"KimiK2RendererConfig",
+ "LagunaS21Renderer",
+ "LagunaS21RendererConfig",
"LagunaXS2Renderer",
"LagunaXS2RendererConfig",
"LagunaXS21Renderer",
diff --git a/renderers/base.py b/renderers/base.py
index f235fea..e922ab3 100644
--- a/renderers/base.py
+++ b/renderers/base.py
@@ -1066,11 +1066,13 @@ def bridge_to_next_turn(self, *args: Any, **kwargs: Any) -> "RenderedTokens | No
"meta-llama/Llama-3.2-3B-Instruct": "llama-3",
# Poolside Laguna. XS.2 and XS-2.1 ship different chat templates, each
# mirrored by its own renderer class. S-2.1 is a larger sibling of XS-2.1
- # with a byte-identical chat template and tokenizer, so it reuses the
- # XS-2.1 renderer.
+ # that shares its tokenizer but NOT its chat template: S-2.1 defaults
+ # ``enable_thinking`` to True and adds a ``preserve_thinking`` gate, so it
+ # gets its own renderer/config (``LagunaS21Renderer``), a thin subclass of
+ # the XS-2.1 renderer.
"poolside/Laguna-XS.2": "laguna-xs.2",
"poolside/Laguna-XS-2.1": "laguna-xs-2.1",
- "poolside/Laguna-S-2.1": "laguna-xs-2.1",
+ "poolside/Laguna-S-2.1": "laguna-s-2.1",
# GPT-OSS.
"openai/gpt-oss-20b": "gpt-oss",
"openai/gpt-oss-120b": "gpt-oss",
@@ -1318,7 +1320,11 @@ def _populate_registry():
from renderers.hy3 import Hy3Renderer
from renderers.kimi_k2 import KimiK2Renderer
from renderers.kimi_k25 import KimiK25Renderer
- from renderers.laguna_xs2 import LagunaXS2Renderer, LagunaXS21Renderer
+ from renderers.laguna_xs2 import (
+ LagunaS21Renderer,
+ LagunaXS2Renderer,
+ LagunaXS21Renderer,
+ )
from renderers.llama_3 import Llama3Renderer
from renderers.minimax_m2 import MiniMaxM2Renderer
from renderers.nemotron3 import Nemotron3Renderer, Nemotron3UltraRenderer
@@ -1347,6 +1353,7 @@ def _populate_registry():
"kimi-k2.5": KimiK25Renderer,
"laguna-xs.2": LagunaXS2Renderer,
"laguna-xs-2.1": LagunaXS21Renderer,
+ "laguna-s-2.1": LagunaS21Renderer,
"llama-3": Llama3Renderer,
"nemotron-3": Nemotron3Renderer,
"nemotron-3-ultra": Nemotron3UltraRenderer,
diff --git a/renderers/configs.py b/renderers/configs.py
index ad32044..6146e10 100644
--- a/renderers/configs.py
+++ b/renderers/configs.py
@@ -524,6 +524,37 @@ class LagunaXS21RendererConfig(BaseRendererConfig):
upstream default."""
+class LagunaS21RendererConfig(BaseRendererConfig):
+ """Laguna S-2.1 renderer config.
+
+ S-2.1 is a larger sibling of XS-2.1 sharing its tokenizer (same vocab,
+ special tokens, and merges), but its chat template is *not* byte-identical:
+ ``enable_thinking`` defaults to ``True`` (XS-2.1 defaults ``False``), and a
+ new ``preserve_thinking`` kwarg widens the reasoning-display gate to
+ ``enable_thinking or preserve_thinking``. The token format is otherwise
+ identical, so this is served by
+ :class:`renderers.laguna_xs2.LagunaS21Renderer`, a thin subclass of
+ ``LagunaXS21Renderer`` that only overrides that gate.
+ """
+
+ name: Literal["laguna-s-2.1"] = "laguna-s-2.1"
+
+ enable_thinking: bool = True
+ """When ``True``, the generation prompt ends with ```` and every
+ assistant turn renders ``{reasoning}``; when ``False``,
+ turns open with a bare ````. Mirrors the template's
+ ``enable_thinking`` kwarg — note S-2.1's upstream default is ``True``,
+ unlike XS-2.1's ``False``."""
+
+ preserve_thinking: bool = False
+ """When ``True``, assistant turns keep their ``{reasoning}``
+ block even while ``enable_thinking`` is ``False`` — the template gates
+ reasoning display on ``enable_thinking or preserve_thinking``. With the
+ default ``False`` the gate collapses to ``enable_thinking`` and the
+ renderer matches XS-2.1 turn-for-turn. Mirrors the template's
+ ``preserve_thinking`` kwarg and its upstream default."""
+
+
class Llama3RendererConfig(BaseRendererConfig):
"""Llama-3.x Instruct renderer config.
@@ -681,6 +712,7 @@ class DeepSeekR1RendererConfig(BaseRendererConfig):
KimiK25RendererConfig,
LagunaXS2RendererConfig,
LagunaXS21RendererConfig,
+ LagunaS21RendererConfig,
Llama3RendererConfig,
MiniMaxM2RendererConfig,
Nemotron3RendererConfig,
@@ -721,6 +753,7 @@ class DeepSeekR1RendererConfig(BaseRendererConfig):
"kimi-k2.5": KimiK25RendererConfig,
"laguna-xs.2": LagunaXS2RendererConfig,
"laguna-xs-2.1": LagunaXS21RendererConfig,
+ "laguna-s-2.1": LagunaS21RendererConfig,
"llama-3": Llama3RendererConfig,
"minimax-m2": MiniMaxM2RendererConfig,
"nemotron-3": Nemotron3RendererConfig,
@@ -767,6 +800,7 @@ def config_from_name(name: str) -> BaseRendererConfig | None:
"Hy3RendererConfig",
"KimiK25RendererConfig",
"KimiK2RendererConfig",
+ "LagunaS21RendererConfig",
"LagunaXS2RendererConfig",
"LagunaXS21RendererConfig",
"Llama3RendererConfig",
diff --git a/renderers/laguna_xs2.py b/renderers/laguna_xs2.py
index bd174f4..b3ec3b6 100644
--- a/renderers/laguna_xs2.py
+++ b/renderers/laguna_xs2.py
@@ -36,6 +36,13 @@
section ends at ````.
- The ```` block is emitted whenever there is system content,
tools, or ``enable_thinking`` — even if that leaves it empty.
+
+S-2.1 is a larger sibling of XS-2.1 served by :class:`LagunaS21Renderer`. It
+shares XS-2.1's tokenizer and token format, so the two differ only in the chat
+template's thinking knobs: ``enable_thinking`` defaults to ``True`` (XS-2.1
+defaults ``False``), and a new ``preserve_thinking`` kwarg widens the
+reasoning-display gate to ``enable_thinking or preserve_thinking``. The
+subclass overrides only that gate; everything else is inherited.
"""
from __future__ import annotations
@@ -56,7 +63,11 @@
resolve_thinking_retention,
should_rerender_for_thinking_retention,
)
-from renderers.configs import LagunaXS2RendererConfig, LagunaXS21RendererConfig
+from renderers.configs import (
+ LagunaS21RendererConfig,
+ LagunaXS2RendererConfig,
+ LagunaXS21RendererConfig,
+)
from renderers.parsing import parse_laguna_xs2
_DEFAULT_SYSTEM_MESSAGE = (
@@ -107,7 +118,10 @@ class LagunaXS2Renderer:
def __init__(
self,
tokenizer: PreTrainedTokenizer,
- config: LagunaXS2RendererConfig | LagunaXS21RendererConfig | None = None,
+ config: LagunaXS2RendererConfig
+ | LagunaXS21RendererConfig
+ | LagunaS21RendererConfig
+ | None = None,
):
self._tokenizer = tokenizer
self.config = config or LagunaXS2RendererConfig()
@@ -633,10 +647,17 @@ class LagunaXS21Renderer(LagunaXS2Renderer):
def __init__(
self,
tokenizer: PreTrainedTokenizer,
- config: LagunaXS21RendererConfig | None = None,
+ config: LagunaXS21RendererConfig | LagunaS21RendererConfig | None = None,
):
super().__init__(tokenizer, config or LagunaXS21RendererConfig())
+ def _render_history_reasoning(self) -> bool:
+ """Whether an assistant turn renders its ``{reasoning}``
+ block (vs opening with a bare ````). Mirrors the template's
+ reasoning-display gate; XS-2.1 gates this on ``enable_thinking`` alone.
+ """
+ return self.config.enable_thinking
+
def render(
self,
messages: list[Message],
@@ -920,7 +941,7 @@ def _render_assistant(
# scaffolding the model never samples.
emit_special(self._assistant, msg_idx, is_sampled=False, is_content=False)
- if self.config.enable_thinking:
+ if self._render_history_reasoning():
# ``{reasoning}`` renders verbatim, even when
# the reasoning is empty; the opener is the gen-prompt
# prefill, the rest the model sampled.
@@ -962,3 +983,36 @@ def _render_assistant(
emit_special(self._assistant_end, msg_idx, is_sampled=True, is_content=True)
emit_text("\n", msg_idx, is_sampled=False, is_content=False)
+
+
+class LagunaS21Renderer(LagunaXS21Renderer):
+ """Laguna-S-2.1 — a larger sibling of XS-2.1 that shares its tokenizer
+ and token format but ships a slightly different chat template.
+
+ The only template differences are the ``enable_thinking`` default (``True``
+ here, ``False`` for XS-2.1) and a new ``preserve_thinking`` kwarg that
+ widens the reasoning-display gate to ``enable_thinking or
+ preserve_thinking``. Everything else — role tags, tool-call packing,
+ system-block gate, generation prompt, and the whole parse skeleton — is
+ inherited unchanged from :class:`LagunaXS21Renderer`.
+ """
+
+ # Narrow the inherited ``config`` attribute so ``preserve_thinking`` (an
+ # S-2.1-only field) resolves; ``__init__`` always stores an S-2.1 config.
+ config: LagunaS21RendererConfig
+
+ def __init__(
+ self,
+ tokenizer: PreTrainedTokenizer,
+ config: LagunaS21RendererConfig | None = None,
+ ):
+ # ``LagunaXS21Renderer.__init__`` forwards any config straight through;
+ # pass an S-2.1 config so ``enable_thinking`` defaults to ``True`` and
+ # ``preserve_thinking`` is available.
+ super().__init__(tokenizer, config or LagunaS21RendererConfig())
+
+ def _render_history_reasoning(self) -> bool:
+ # S-2.1's template gates reasoning display on
+ # ``enable_thinking or preserve_thinking`` — the latter keeps
+ # historical ```` blocks even while thinking is off.
+ return self.config.enable_thinking or self.config.preserve_thinking
diff --git a/tests/conftest.py b/tests/conftest.py
index 9fed2db..79f81c9 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -40,8 +40,9 @@
# XS-2.1 resolves to `LagunaXS21Renderer` via the model name — its
# chat template differs from XS.2's (see renderers/laguna_xs2.py).
("poolside/Laguna-XS-2.1", "auto"),
- # S-2.1 is a larger sibling of XS-2.1 with a byte-identical chat template
- # and tokenizer, so it reuses the XS-2.1 renderer.
+ # S-2.1 is a larger sibling of XS-2.1 sharing its tokenizer but NOT its
+ # chat template (S-2.1 defaults enable_thinking=True and adds a
+ # preserve_thinking gate), so it resolves to LagunaS21Renderer.
("poolside/Laguna-S-2.1", "auto"),
# DeepSeek-V3/R1 are intentionally NOT in this shared barrage: their
# chat templates can't render the barrage's tool-call fixtures (the
diff --git a/tests/test_laguna_s21.py b/tests/test_laguna_s21.py
new file mode 100644
index 0000000..a5968ec
--- /dev/null
+++ b/tests/test_laguna_s21.py
@@ -0,0 +1,128 @@
+"""Laguna-S-2.1 focused tests.
+
+S-2.1 shares XS-2.1's tokenizer and token format, so the shared matrices
+(conftest / config-parity) already assert byte parity on the common shapes
+via :class:`LagunaS21Renderer`. This file pins the two behaviours that make
+S-2.1 *differ* from XS-2.1 — and that the shared barrage can't reach because
+it never sets the relevant kwargs:
+
+- ``enable_thinking`` defaults to ``True`` (XS-2.1 defaults ``False``), so the
+ auto-resolved renderer renders reasoning by default, and
+- the new ``preserve_thinking`` kwarg keeps historical ```` blocks even
+ while ``enable_thinking`` is ``False`` — the template gates reasoning display
+ on ``enable_thinking or preserve_thinking``.
+"""
+
+from __future__ import annotations
+
+from functools import lru_cache
+from itertools import product
+
+from renderers import create_renderer
+from renderers.base import load_tokenizer
+from renderers.configs import LagunaS21RendererConfig
+from renderers.laguna_xs2 import LagunaS21Renderer
+
+_MODEL = "poolside/Laguna-S-2.1"
+
+
+@lru_cache(maxsize=None)
+def _tok():
+ return load_tokenizer(_MODEL)
+
+
+def _renderer(**config_kwargs) -> LagunaS21Renderer:
+ renderer = create_renderer(_tok(), LagunaS21RendererConfig(**config_kwargs))
+ assert isinstance(renderer, LagunaS21Renderer)
+ return renderer
+
+
+def _expected(msgs, *, add_generation_prompt=False, **template_kwargs):
+ return list(
+ _tok().apply_chat_template(
+ msgs,
+ add_generation_prompt=add_generation_prompt,
+ tokenize=True,
+ return_dict=False,
+ **template_kwargs,
+ )
+ )
+
+
+def test_auto_resolves_to_s21_renderer_with_thinking_on_by_default():
+ """``poolside/Laguna-S-2.1`` resolves to :class:`LagunaS21Renderer`, and
+ its config defaults ``enable_thinking=True`` / ``preserve_thinking=False``
+ — matching S-2.1's template defaults (unlike XS-2.1's thinking-off)."""
+ r = create_renderer(_tok()) # auto-resolve via MODEL_RENDERER_MAP
+ assert isinstance(r, LagunaS21Renderer)
+ assert r.config.name == "laguna-s-2.1"
+ assert r.config.enable_thinking is True
+ assert r.config.preserve_thinking is False
+
+
+def test_default_renders_empty_think_wrapper():
+ """With thinking on by default, a reasoning-free assistant turn still
+ opens with the empty ```` wrapper (this is exactly the
+ render_ids divergence from the XS-2.1 renderer)."""
+ msgs = [
+ {"role": "user", "content": "What is 2+2?"},
+ {"role": "assistant", "content": "4"},
+ ]
+ r = _renderer() # defaults: enable_thinking=True
+ ours = r.render_ids(msgs)
+ assert ours == _expected(msgs)
+ assert "4" in _tok().decode(ours)
+
+
+def test_preserve_thinking_keeps_history_when_thinking_off():
+ """``enable_thinking=False`` alone drops reasoning, but with
+ ``preserve_thinking=True`` the historical ``{reasoning}``
+ survives — matching the template's ``enable_thinking or preserve_thinking``
+ gate."""
+ msgs = [
+ {"role": "user", "content": "What is 2+2?"},
+ {"role": "assistant", "reasoning_content": "Simple arithmetic", "content": "4"},
+ ]
+ r = _renderer(enable_thinking=False, preserve_thinking=True)
+ ours = r.render_ids(msgs)
+ assert ours == _expected(msgs, enable_thinking=False, preserve_thinking=True)
+ assert "Simple arithmetic" in _tok().decode(ours)
+
+
+def test_no_preserve_thinking_drops_history_when_thinking_off():
+ """With both flags off the gate collapses to ``enable_thinking`` and the
+ turn opens with a bare ````, dropping the reasoning — identical to
+ the XS-2.1 renderer's default."""
+ msgs = [
+ {"role": "user", "content": "What is 2+2?"},
+ {"role": "assistant", "reasoning_content": "Simple arithmetic", "content": "4"},
+ ]
+ r = _renderer(enable_thinking=False, preserve_thinking=False)
+ ours = r.render_ids(msgs)
+ assert ours == _expected(msgs, enable_thinking=False, preserve_thinking=False)
+ assert "Simple arithmetic" not in _tok().decode(ours)
+
+
+def test_all_thinking_flag_combos_match_template():
+ """Byte parity against ``apply_chat_template`` for every
+ ``enable_thinking`` × ``preserve_thinking`` combination, over a multi-turn
+ conversation carrying historical reasoning."""
+ msgs = [
+ {"role": "user", "content": "Q1"},
+ {"role": "assistant", "reasoning_content": "deduce", "content": "A1"},
+ {"role": "user", "content": "Q2"},
+ {"role": "assistant", "reasoning_content": "more", "content": "A2"},
+ ]
+ for enable_thinking, preserve_thinking in product((False, True), repeat=2):
+ r = _renderer(
+ enable_thinking=enable_thinking, preserve_thinking=preserve_thinking
+ )
+ for add_generation_prompt in (False, True):
+ assert r.render_ids(
+ msgs, add_generation_prompt=add_generation_prompt
+ ) == _expected(
+ msgs,
+ add_generation_prompt=add_generation_prompt,
+ enable_thinking=enable_thinking,
+ preserve_thinking=preserve_thinking,
+ ), (enable_thinking, preserve_thinking, add_generation_prompt)