Skip to content
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ next_prompt_ids = r.bridge_to_next_turn(
)
```

Hand-coded renderers ship for `qwen3`, `qwen3-vl`, `qwen3.5`, `qwen3.6`, `qwen3.8`, `gemma4`, `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`, `inkling` / `inkling-small`, and `prime-qwen3`. Anything else falls back to `DefaultRenderer`, a generic `apply_chat_template` wrapper. `qwen3-vl`, `qwen3.5`, `qwen3.6`, `qwen3.8`, `gemma4`, `kimi-k2.5` / `kimi-k2.6`, and the Inkling checkpoints are multimodal (Inkling handles both image **and** audio).
Hand-coded renderers ship for `qwen3`, `qwen3-vl`, `qwen3.5`, `qwen3.6`, `qwen3.8`, `gemma4`, `glm-5`, `glm-5.1`, `glm-4.5`, `minimax-m2`, `deepseek-v3`, `deepseek-r1`, `deepseek-v4` (V4 Flash 0731), `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`, `inkling` / `inkling-small`, and `prime-qwen3`. Anything else falls back to `DefaultRenderer`, a generic `apply_chat_template` wrapper. `qwen3-vl`, `qwen3.5`, `qwen3.6`, `qwen3.8`, `gemma4`, `kimi-k2.5` / `kimi-k2.6`, and the Inkling checkpoints are multimodal (Inkling handles both image **and** audio).

## API

Expand Down
2 changes: 2 additions & 0 deletions docs/renderer-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ definition time. Template fields are covered by parity tests against
| Nemotron-3.5 Lightning | `Nemotron35RendererConfig` | `enable_thinking`, `truncate_history_thinking` | - |
| DeepSeek V3 | `DeepSeekV3RendererConfig` | - | - |
| DeepSeek R1 | `DeepSeekR1RendererConfig` | - | - |
| DeepSeek V4 Flash 0731 | `DeepSeekV4RendererConfig` | `enable_thinking`, `drop_thinking`, `reasoning_effort` | - |

Configs are frozen value objects. To override a field, construct a new instance
or call `config.model_copy(update={...})`.
Expand Down Expand Up @@ -145,6 +146,7 @@ the knobs its template actually exposes:
| Kimi K2.5 / 2.6 | `thinking=False -> all`, else `tool_cycle` |
| Nemotron-3 / 3.5 | `truncate_history_thinking=False -> all`; else `enable_thinking=False -> all`; else `tool_cycle` |
| DeepSeek R1 | `template` |
| DeepSeek V4 Flash 0731 | `enable_thinking=False` or `drop_thinking=False -> all`, else `tool_cycle` |
| MiniMax M2 | `tool_cycle` |
| DeepSeek V3, Qwen3-VL, Kimi K2, Laguna XS.2 / M.1 / XS-2.1 / S-2.1, Llama 3, Inkling | `all` |
| PrimeIntellect Qwen3 | `all` |
Expand Down
4 changes: 4 additions & 0 deletions renderers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
DefaultRendererConfig,
DeepSeekR1RendererConfig,
DeepSeekV3RendererConfig,
DeepSeekV4RendererConfig,
GLM45RendererConfig,
GLM51RendererConfig,
GLM5RendererConfig,
Expand Down Expand Up @@ -85,6 +86,7 @@
_LAZY_RENDERERS: dict[str, str] = {
"DeepSeekR1Renderer": "renderers.deepseek_r1",
"DeepSeekV3Renderer": "renderers.deepseek_v3",
"DeepSeekV4Renderer": "renderers.deepseek_v4",
"DefaultRenderer": "renderers.default",
"GLM45Renderer": "renderers.glm45",
"GLM51Renderer": "renderers.glm5",
Expand Down Expand Up @@ -138,6 +140,8 @@ def __dir__() -> list[str]:
"DeepSeekR1RendererConfig",
"DeepSeekV3Renderer",
"DeepSeekV3RendererConfig",
"DeepSeekV4Renderer",
"DeepSeekV4RendererConfig",
"DefaultRenderer",
"DefaultRendererConfig",
"GLM45Renderer",
Expand Down
5 changes: 5 additions & 0 deletions renderers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -982,6 +982,9 @@ def is_multimodal(r: object) -> bool:
# DeepSeek R1 (reasoning).
"deepseek-ai/DeepSeek-R1": "deepseek-r1",
"deepseek-ai/DeepSeek-R1-0528": "deepseek-r1",
# DeepSeek V4 Flash 0731 uses the repository's Python DSML encoder (the
# tokenizer intentionally ships no Jinja chat_template).
"deepseek-ai/DeepSeek-V4-Flash-0731": "deepseek-v4",
# Kimi K2 (K2.5 and K2.6 share the K2.5 template, distinct from K2).
"moonshotai/Kimi-K2-Instruct": "kimi-k2",
"moonshotai/Kimi-K2.5": "kimi-k2.5",
Expand Down Expand Up @@ -1299,6 +1302,7 @@ def _populate_registry():
return
from renderers.deepseek_r1 import DeepSeekR1Renderer
from renderers.deepseek_v3 import DeepSeekV3Renderer
from renderers.deepseek_v4 import DeepSeekV4Renderer
from renderers.default import DefaultRenderer
from renderers.glm5 import GLM5Renderer, GLM51Renderer
from renderers.glm45 import GLM45Renderer
Expand Down Expand Up @@ -1344,6 +1348,7 @@ def _populate_registry():
"minimax-m2": MiniMaxM2Renderer,
"deepseek-v3": DeepSeekV3Renderer,
"deepseek-r1": DeepSeekR1Renderer,
"deepseek-v4": DeepSeekV4Renderer,
"hy3": Hy3Renderer,
"inkling": InklingRenderer,
"kimi-k2": KimiK2Renderer,
Expand Down
44 changes: 44 additions & 0 deletions renderers/configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -930,6 +930,47 @@ class DeepSeekR1RendererConfig(BaseRendererConfig):
_template_fields = frozenset()


class DeepSeekV4RendererConfig(BaseRendererConfig):
"""DeepSeek-V4-Flash-0731 reference-encoder configuration.

The checkpoint ships a Python encoder rather than a Jinja template. These
fields mirror its public controls: chat vs thinking mode, historical
reasoning dropping, and the opt-in thinking-effort prefix.
"""

name: Literal["deepseek-v4"] = "deepseek-v4"
_template_fields = frozenset(
{"enable_thinking", "drop_thinking", "reasoning_effort"}
)

enable_thinking: bool = False
"""Select thinking mode. ``False`` matches the official inference script."""

drop_thinking: bool = True
"""Drop reasoning before the latest user query when no tools are present.

The reference encoder automatically preserves all reasoning whenever tools
are supplied, regardless of this value.
"""

reasoning_effort: Literal["low", "high", "max"] = "low"
"""Thinking-only effort prefix; ``low`` adds no text.

``low`` is the checkpoint Python encoder's default. DeepSeek's hosted API
independently defaults its thinking effort to ``high``.
"""

@model_validator(mode="after")
def _check_thinking_retention(self):
_reject_thinking_retention_conflict(
self,
"drop_thinking",
true_implies="tool_cycle",
false_implies="all",
)
return self


RendererConfig = Annotated[
Union[
AutoRendererConfig,
Expand Down Expand Up @@ -960,6 +1001,7 @@ class DeepSeekR1RendererConfig(BaseRendererConfig):
Nemotron35RendererConfig,
DeepSeekV3RendererConfig,
DeepSeekR1RendererConfig,
DeepSeekV4RendererConfig,
],
Field(discriminator="name"),
]
Expand Down Expand Up @@ -1006,6 +1048,7 @@ class DeepSeekR1RendererConfig(BaseRendererConfig):
"nemotron-3.5": Nemotron35RendererConfig,
"deepseek-v3": DeepSeekV3RendererConfig,
"deepseek-r1": DeepSeekR1RendererConfig,
"deepseek-v4": DeepSeekV4RendererConfig,
}


Expand Down Expand Up @@ -1039,6 +1082,7 @@ def config_from_name(name: str) -> BaseRendererConfig | None:
"DefaultRendererConfig",
"DeepSeekR1RendererConfig",
"DeepSeekV3RendererConfig",
"DeepSeekV4RendererConfig",
"GLM45RendererConfig",
"GLM51RendererConfig",
"GLM5RendererConfig",
Expand Down
Loading
Loading