renderers 0.1.10.dev4__tar.gz → 0.1.10.dev11__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 (81) hide show
  1. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/PKG-INFO +3 -3
  2. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/README.md +2 -2
  3. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/docs/renderer-config.md +4 -1
  4. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/renderers/__init__.py +8 -0
  5. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/renderers/_version.py +2 -2
  6. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/renderers/base.py +19 -2
  7. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/renderers/client.py +66 -0
  8. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/renderers/configs.py +64 -0
  9. renderers-0.1.10.dev11/renderers/gemma4.py +1369 -0
  10. renderers-0.1.10.dev11/renderers/laguna_s21.py +33 -0
  11. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/renderers/laguna_xs2.py +14 -2
  12. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/tests/conftest.py +5 -0
  13. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/tests/test_bridge.py +1 -0
  14. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/tests/test_build_helpers.py +14 -0
  15. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/tests/test_client.py +59 -0
  16. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/tests/test_disabled_thinking_stability.py +10 -2
  17. renderers-0.1.10.dev11/tests/test_gemma4.py +473 -0
  18. renderers-0.1.10.dev11/tests/test_laguna_s21.py +128 -0
  19. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/tests/test_multimodal.py +69 -14
  20. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/tests/test_render_ids.py +20 -0
  21. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/tests/test_renderer_config_parity.py +17 -8
  22. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/tests/test_roundtrip.py +1 -0
  23. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/.github/workflows/publish-dev.yml +0 -0
  24. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/.github/workflows/publish.yml +0 -0
  25. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/.github/workflows/style.yml +0 -0
  26. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/.github/workflows/test.yml +0 -0
  27. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/.gitignore +0 -0
  28. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/.pre-commit-config.yaml +0 -0
  29. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/LICENSE +0 -0
  30. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/examples/README.md +0 -0
  31. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/examples/sglang/multiturn_generate_sglang.py +0 -0
  32. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/examples/sglang/online_multiturn_sglang.py +0 -0
  33. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/examples/tinker/multiturn_generate_tinker.py +0 -0
  34. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/examples/transformers/multiturn_generate_transformers.py +0 -0
  35. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/examples/vllm/multiturn_generate_vllm.py +0 -0
  36. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/pyproject.toml +0 -0
  37. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/renderers/deepseek_r1.py +0 -0
  38. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/renderers/deepseek_v3.py +0 -0
  39. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/renderers/default.py +0 -0
  40. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/renderers/glm45.py +0 -0
  41. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/renderers/glm5.py +0 -0
  42. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/renderers/gpt_oss.py +0 -0
  43. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/renderers/hy3.py +0 -0
  44. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/renderers/kimi_k2.py +0 -0
  45. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/renderers/kimi_k25.py +0 -0
  46. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/renderers/llama_3.py +0 -0
  47. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/renderers/minimax_m2.py +0 -0
  48. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/renderers/nemotron3.py +0 -0
  49. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/renderers/parsers.py +0 -0
  50. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/renderers/parsing.py +0 -0
  51. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/renderers/prime_qwen3.py +0 -0
  52. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/renderers/qwen3.py +0 -0
  53. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/renderers/qwen35.py +0 -0
  54. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/renderers/qwen36.py +0 -0
  55. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/renderers/qwen3_vl.py +0 -0
  56. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/tests/test_deepseek_r1.py +0 -0
  57. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/tests/test_glm_tool_name_validation.py +0 -0
  58. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/tests/test_gpt_oss_harmony_parity.py +0 -0
  59. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/tests/test_hy3.py +0 -0
  60. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/tests/test_incremental.py +0 -0
  61. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/tests/test_is_content.py +0 -0
  62. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/tests/test_kimi_k25_tool_schema.py +0 -0
  63. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/tests/test_laguna_m1.py +0 -0
  64. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/tests/test_laguna_xs21.py +0 -0
  65. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/tests/test_llama_3.py +0 -0
  66. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/tests/test_load_tokenizer.py +0 -0
  67. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/tests/test_message_indices.py +0 -0
  68. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/tests/test_message_tool_names.py +0 -0
  69. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/tests/test_nemotron3_parity.py +0 -0
  70. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/tests/test_nemotron3_ultra.py +0 -0
  71. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/tests/test_parse_response.py +0 -0
  72. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/tests/test_parse_response_robustness.py +0 -0
  73. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/tests/test_parsers.py +0 -0
  74. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/tests/test_preserve_thinking.py +0 -0
  75. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/tests/test_prime_qwen3_parity.py +0 -0
  76. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/tests/test_qwen35_size_coverage.py +0 -0
  77. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/tests/test_renderer_config.py +0 -0
  78. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/tests/test_sampled_mask.py +0 -0
  79. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/tests/test_tokens_per_message.py +0 -0
  80. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/tests/test_tool_arg_type_preservation.py +0 -0
  81. {renderers-0.1.10.dev4 → renderers-0.1.10.dev11}/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.dev11
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`, `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`, and `prime-qwen3`. Anything else falls back to `DefaultRenderer`, a generic `apply_chat_template` wrapper.
60
60
 
61
61
  ## API
62
62
 
@@ -175,7 +175,7 @@ Fallback for unsupported text-only models. Wraps `apply_chat_template` and accep
175
175
 
176
176
  ## Roadmap
177
177
 
178
- - **VLM expansion.** `ImagePart` support exists for Qwen3-VL and Qwen3.5-family multimodal templates. Remaining work: video support, broader VLM coverage, and more RL validation.
178
+ - **VLM expansion.** `ImagePart` support exists for Qwen3-VL, Qwen3.5-family, Gemma 4, and Kimi K2.5 / K2.6 multimodal templates. Remaining work: audio/video support, broader VLM coverage, and more RL validation. Gemma 4 image preprocessing requires a Transformers release that provides `Gemma4Processor`.
179
179
  - **Patched chat templates.** Some shipped templates re-tokenize history or normalize JSON in ways that break token identity. Plan: a `use_patched` opt-in per renderer that renders the same surface form while avoiding known-bad patterns. (Auto-stripping thinking from past turns is *not* one of these — that's intended template behaviour the renderer reproduces; use `thinking_retention` to override it.)
180
180
 
181
181
  ## Testing
@@ -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`, `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`, and `prime-qwen3`. Anything else falls back to `DefaultRenderer`, a generic `apply_chat_template` wrapper.
44
44
 
45
45
  ## API
46
46
 
@@ -159,7 +159,7 @@ Fallback for unsupported text-only models. Wraps `apply_chat_template` and accep
159
159
 
160
160
  ## Roadmap
161
161
 
162
- - **VLM expansion.** `ImagePart` support exists for Qwen3-VL and Qwen3.5-family multimodal templates. Remaining work: video support, broader VLM coverage, and more RL validation.
162
+ - **VLM expansion.** `ImagePart` support exists for Qwen3-VL, Qwen3.5-family, Gemma 4, and Kimi K2.5 / K2.6 multimodal templates. Remaining work: audio/video support, broader VLM coverage, and more RL validation. Gemma 4 image preprocessing requires a Transformers release that provides `Gemma4Processor`.
163
163
  - **Patched chat templates.** Some shipped templates re-tokenize history or normalize JSON in ways that break token identity. Plan: a `use_patched` opt-in per renderer that renders the same surface form while avoiding known-bad patterns. (Auto-stripping thinking from past turns is *not* one of these — that's intended template behaviour the renderer reproduces; use `thinking_retention` to override it.)
164
164
 
165
165
  ## Testing
@@ -30,6 +30,7 @@ chat-template kwargs. Those fields are covered by parity tests against
30
30
  | Qwen3.5 | `Qwen35RendererConfig` | `enable_thinking`, `add_vision_id` | `image_cache_max` |
31
31
  | Qwen3.6 | `Qwen36RendererConfig` | `enable_thinking`, `add_vision_id`, `preserve_thinking` | `image_cache_max` |
32
32
  | Qwen3-VL | `Qwen3VLRendererConfig` | `add_vision_id` | `image_cache_max` |
33
+ | Gemma 4 | `Gemma4RendererConfig` | `enable_thinking`, `preserve_thinking` | `image_cache_max` |
33
34
  | GLM-5 / 5.1 | `GLM5RendererConfig` / `GLM51RendererConfig` | `enable_thinking`, `clear_thinking` | - |
34
35
  | GLM-4.5 | `GLM45RendererConfig` | `enable_thinking` | - |
35
36
  | gpt-oss | `GptOssRendererConfig` | `reasoning_effort`, `conversation_start_date` | `use_system_prompt`, `knowledge_cutoff`, `model_identity`, `auto_drop_analysis` |
@@ -39,6 +40,7 @@ chat-template kwargs. Those fields are covered by parity tests against
39
40
  | Laguna XS.2 | `LagunaXS2RendererConfig` | `enable_thinking`, `render_assistant_messages_raw` | - |
40
41
  | Laguna M.1 | `LagunaM1RendererConfig` | `enable_thinking`, `render_assistant_messages_raw` | - |
41
42
  | Laguna XS-2.1 | `LagunaXS21RendererConfig` | `enable_thinking` | - |
43
+ | Laguna S-2.1 | `LagunaS21RendererConfig` | `enable_thinking`, `preserve_thinking` | - |
42
44
  | Llama 3 | `Llama3RendererConfig` | `date_string`, `tools_in_user_message` | - |
43
45
  | MiniMax M2 | `MiniMaxM2RendererConfig` | `model_identity` | - |
44
46
  | Nemotron-3 Nano / Super | `Nemotron3RendererConfig` | `enable_thinking`, `truncate_history_thinking`, `low_effort` | - |
@@ -130,6 +132,7 @@ the knobs its template actually exposes:
130
132
  | Qwen3 | `enable_thinking=False -> all`, else `tool_cycle` |
131
133
  | Qwen3.5 | `enable_thinking=False -> all`, else `tool_cycle` |
132
134
  | Qwen3.6 | `preserve_thinking=True -> all`; else `enable_thinking=False -> all`; else `tool_cycle` |
135
+ | Gemma 4 | `preserve_thinking=True -> all`; else `enable_thinking=False -> all`; else `tool_cycle` |
133
136
  | GLM-5 / 5.1 | `clear_thinking=False -> all`; else `enable_thinking=False -> all`; else `tool_cycle` |
134
137
  | GLM-4.5 | `enable_thinking=False -> all`, else `tool_cycle` |
135
138
  | gpt-oss | `auto_drop_analysis=False -> all`, else `tool_cycle` |
@@ -138,7 +141,7 @@ the knobs its template actually exposes:
138
141
  | Nemotron-3 / 3.5 | `truncate_history_thinking=False -> all`; else `enable_thinking=False -> all`; else `tool_cycle` |
139
142
  | DeepSeek R1 | `template` |
140
143
  | MiniMax M2 | `tool_cycle` |
141
- | DeepSeek V3, Qwen3-VL, Kimi K2, Laguna XS.2 / M.1 / XS-2.1, Llama 3 | `all` |
144
+ | DeepSeek V3, Qwen3-VL, Kimi K2, Laguna XS.2 / M.1 / XS-2.1 / S-2.1, Llama 3 | `all` |
142
145
  | PrimeIntellect Qwen3 | `all` |
143
146
 
144
147
  Config construction raises when an explicit template knob directly contradicts
@@ -50,11 +50,13 @@ from renderers.configs import (
50
50
  GLM45RendererConfig,
51
51
  GLM51RendererConfig,
52
52
  GLM5RendererConfig,
53
+ Gemma4RendererConfig,
53
54
  GptOssRendererConfig,
54
55
  Hy3RendererConfig,
55
56
  KimiK25RendererConfig,
56
57
  KimiK2RendererConfig,
57
58
  LagunaM1RendererConfig,
59
+ LagunaS21RendererConfig,
58
60
  LagunaXS2RendererConfig,
59
61
  LagunaXS21RendererConfig,
60
62
  Llama3RendererConfig,
@@ -89,10 +91,12 @@ _LAZY_RENDERERS: dict[str, str] = {
89
91
  "GLM51Renderer": "renderers.glm5",
90
92
  "GLM5Renderer": "renderers.glm5",
91
93
  "GptOssRenderer": "renderers.gpt_oss",
94
+ "Gemma4Renderer": "renderers.gemma4",
92
95
  "Hy3Renderer": "renderers.hy3",
93
96
  "KimiK25Renderer": "renderers.kimi_k25",
94
97
  "KimiK2Renderer": "renderers.kimi_k2",
95
98
  "LagunaM1Renderer": "renderers.laguna_xs2",
99
+ "LagunaS21Renderer": "renderers.laguna_s21",
96
100
  "LagunaXS21Renderer": "renderers.laguna_xs2",
97
101
  "LagunaXS2Renderer": "renderers.laguna_xs2",
98
102
  "Llama3Renderer": "renderers.llama_3",
@@ -140,6 +144,8 @@ __all__ = [
140
144
  "GLM51RendererConfig",
141
145
  "GLM5Renderer",
142
146
  "GLM5RendererConfig",
147
+ "Gemma4Renderer",
148
+ "Gemma4RendererConfig",
143
149
  "GptOssRenderer",
144
150
  "GptOssRendererConfig",
145
151
  "Hy3Renderer",
@@ -151,6 +157,8 @@ __all__ = [
151
157
  "KimiK2RendererConfig",
152
158
  "LagunaM1Renderer",
153
159
  "LagunaM1RendererConfig",
160
+ "LagunaS21Renderer",
161
+ "LagunaS21RendererConfig",
154
162
  "LagunaXS2Renderer",
155
163
  "LagunaXS2RendererConfig",
156
164
  "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.dev11'
22
+ __version_tuple__ = version_tuple = (0, 1, 10, 'dev11')
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -1035,6 +1035,14 @@ MODEL_RENDERER_MAP: dict[str, str] = {
1035
1035
  "Qwen/Qwen3-VL-4B-Instruct": "qwen3-vl",
1036
1036
  "Qwen/Qwen3-VL-8B-Instruct": "qwen3-vl",
1037
1037
  "Qwen/Qwen3-VL-30B-A3B-Instruct": "qwen3-vl",
1038
+ # Gemma 4 instruction checkpoints share Google's canonical turn/tool
1039
+ # grammar and dynamic Gemma4Processor image expansion. E2B/E4B omit the
1040
+ # disabled-thinking empty-channel prefill used by the 26B/31B revision;
1041
+ # Gemma4Renderer detects that small template variant per tokenizer.
1042
+ "google/gemma-4-E2B-it": "gemma4",
1043
+ "google/gemma-4-E4B-it": "gemma4",
1044
+ "google/gemma-4-26B-A4B-it": "gemma4",
1045
+ "google/gemma-4-31B-it": "gemma4",
1038
1046
  # GLM-5 family (GLM-4.7 reuses the GLM-5 template).
1039
1047
  "zai-org/GLM-5": "glm-5",
1040
1048
  "zai-org/GLM-5-FP8": "glm-5",
@@ -1076,11 +1084,12 @@ MODEL_RENDERER_MAP: dict[str, str] = {
1076
1084
  # construction to pin a different date.
1077
1085
  "meta-llama/Llama-3.2-1B-Instruct": "llama-3",
1078
1086
  "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.
1087
+ # Poolside Laguna. These checkpoints ship distinct chat templates, each
1088
+ # mirrored by its own renderer class/config discriminator.
1081
1089
  "poolside/Laguna-XS.2": "laguna-xs.2",
1082
1090
  "poolside/Laguna-M.1": "laguna-m.1",
1083
1091
  "poolside/Laguna-XS-2.1": "laguna-xs-2.1",
1092
+ "poolside/Laguna-S-2.1": "laguna-s-2.1",
1084
1093
  # GPT-OSS.
1085
1094
  "openai/gpt-oss-20b": "gpt-oss",
1086
1095
  "openai/gpt-oss-120b": "gpt-oss",
@@ -1106,6 +1115,10 @@ MULTIMODAL_MODELS: dict[str, set[str]] = {
1106
1115
  "Qwen/Qwen3-VL-4B-Instruct": {"image"},
1107
1116
  "Qwen/Qwen3-VL-8B-Instruct": {"image"},
1108
1117
  "Qwen/Qwen3-VL-30B-A3B-Instruct": {"image"},
1118
+ "google/gemma-4-E2B-it": {"image"},
1119
+ "google/gemma-4-E4B-it": {"image"},
1120
+ "google/gemma-4-26B-A4B-it": {"image"},
1121
+ "google/gemma-4-31B-it": {"image"},
1109
1122
  # Qwen3.5 is itself a VLM family (HF tag ``image-text-to-text``,
1110
1123
  # processor class ``Qwen3VLProcessor``) — same vision tokens and
1111
1124
  # image-processor as Qwen3-VL, with a different tool-call format.
@@ -1325,9 +1338,11 @@ def _populate_registry():
1325
1338
  from renderers.glm5 import GLM5Renderer, GLM51Renderer
1326
1339
  from renderers.glm45 import GLM45Renderer
1327
1340
  from renderers.gpt_oss import GptOssRenderer
1341
+ from renderers.gemma4 import Gemma4Renderer
1328
1342
  from renderers.hy3 import Hy3Renderer
1329
1343
  from renderers.kimi_k2 import KimiK2Renderer
1330
1344
  from renderers.kimi_k25 import KimiK25Renderer
1345
+ from renderers.laguna_s21 import LagunaS21Renderer
1331
1346
  from renderers.laguna_xs2 import (
1332
1347
  LagunaM1Renderer,
1333
1348
  LagunaXS2Renderer,
@@ -1352,6 +1367,7 @@ def _populate_registry():
1352
1367
  "qwen3": Qwen3Renderer,
1353
1368
  "prime-qwen3": PrimeQwen3Renderer,
1354
1369
  "qwen3-vl": Qwen3VLRenderer,
1370
+ "gemma4": Gemma4Renderer,
1355
1371
  "qwen3.5": Qwen35Renderer,
1356
1372
  "qwen3.6": Qwen36Renderer,
1357
1373
  "glm-5": GLM5Renderer,
@@ -1366,6 +1382,7 @@ def _populate_registry():
1366
1382
  "laguna-xs.2": LagunaXS2Renderer,
1367
1383
  "laguna-m.1": LagunaM1Renderer,
1368
1384
  "laguna-xs-2.1": LagunaXS21Renderer,
1385
+ "laguna-s-2.1": LagunaS21Renderer,
1369
1386
  "llama-3": Llama3Renderer,
1370
1387
  "nemotron-3": Nemotron3Renderer,
1371
1388
  "nemotron-3-ultra": Nemotron3UltraRenderer,
@@ -428,6 +428,7 @@ def _build_mm_features(
428
428
  (``MultiModalData``) is already framework-agnostic and does not need
429
429
  to change. Don't pre-build the abstraction with one engine in tree.
430
430
  """
431
+ from renderers.gemma4 import Gemma4Renderer
431
432
  from renderers.qwen3_vl import Qwen3VLRenderer
432
433
  from renderers.qwen35 import Qwen35Renderer
433
434
 
@@ -443,6 +444,8 @@ def _build_mm_features(
443
444
  # the family default and matches every Qwen-VL processor in tree.
444
445
  if issubclass(renderer_cls, (Qwen3VLRenderer, Qwen35Renderer)):
445
446
  return _build_qwen_vl_features(mm_data, spatial_merge_size=2)
447
+ if issubclass(renderer_cls, Gemma4Renderer):
448
+ return _build_gemma4_features(mm_data)
446
449
 
447
450
  raise NotImplementedError(
448
451
  f"Multimodal serialization not implemented for {renderer_cls.__name__}. "
@@ -450,6 +453,69 @@ def _build_mm_features(
450
453
  )
451
454
 
452
455
 
456
+ def _build_gemma4_features(mm_data: MultiModalData) -> dict[str, Any]:
457
+ """vLLM features payload for Gemma 4 image inputs.
458
+
459
+ Hugging Face names the position field ``image_position_ids`` while
460
+ vLLM's Gemma 4 processor schema calls it ``pixel_position_ids``. Keep
461
+ renderer output faithful to the HF processor and translate at this
462
+ engine-specific boundary.
463
+ """
464
+ try:
465
+ import torch
466
+ from transformers.feature_extraction_utils import BatchFeature
467
+ from vllm.entrypoints.scale_out.token_in_token_out.mm_serde import (
468
+ encode_mm_kwargs_item,
469
+ )
470
+ from vllm.multimodal.inputs import (
471
+ MultiModalFieldConfig,
472
+ MultiModalKwargsItems,
473
+ )
474
+ except ImportError as exc:
475
+ raise RuntimeError(
476
+ "Gemma 4 multimodal generate via /inference/v1/generate requires "
477
+ "a vLLM release with Gemma 4 support and `torch`."
478
+ ) from exc
479
+
480
+ out: dict[str, Any] = {
481
+ "mm_hashes": {},
482
+ "mm_placeholders": {},
483
+ "kwargs_data": {},
484
+ }
485
+ image_items = mm_data.mm_items.get("image") or []
486
+ if image_items:
487
+ pixel_values = torch.cat(
488
+ [torch.as_tensor(item["pixel_values"]) for item in image_items], dim=0
489
+ )
490
+ pixel_position_ids = torch.cat(
491
+ [torch.as_tensor(item["image_position_ids"]) for item in image_items],
492
+ dim=0,
493
+ )
494
+ hf_inputs = BatchFeature(
495
+ data={
496
+ "pixel_values": pixel_values,
497
+ "pixel_position_ids": pixel_position_ids,
498
+ }
499
+ )
500
+ field_config = {
501
+ "pixel_values": MultiModalFieldConfig.batched("image"),
502
+ "pixel_position_ids": MultiModalFieldConfig.batched("image"),
503
+ }
504
+ kwargs_items = MultiModalKwargsItems.from_hf_inputs(hf_inputs, field_config)
505
+ out["kwargs_data"]["image"] = [
506
+ encode_mm_kwargs_item(item) for item in kwargs_items["image"]
507
+ ]
508
+ out["mm_hashes"]["image"] = list(mm_data.mm_hashes.get("image") or [])
509
+ out["mm_placeholders"]["image"] = [
510
+ {"offset": placeholder.offset, "length": placeholder.length}
511
+ for placeholder in mm_data.mm_placeholders.get("image") or []
512
+ ]
513
+
514
+ if not any(out["kwargs_data"].values()):
515
+ out["kwargs_data"] = None
516
+ return out
517
+
518
+
453
519
  def _build_qwen_vl_features(
454
520
  mm_data: MultiModalData, *, spatial_merge_size: int
455
521
  ) -> dict[str, Any]:
@@ -270,6 +270,33 @@ class Qwen3VLRendererConfig(BaseRendererConfig):
270
270
  _internal_fields = frozenset({"image_cache_max"})
271
271
 
272
272
 
273
+ class Gemma4RendererConfig(BaseRendererConfig):
274
+ """Gemma 4 renderer config."""
275
+
276
+ name: Literal["gemma4"] = "gemma4"
277
+
278
+ enable_thinking: bool = False
279
+ """Enable Gemma 4's thinking mode. Mirrors the canonical template kwarg."""
280
+
281
+ preserve_thinking: bool = False
282
+ """Keep thinking on historical tool-call turns when the template permits it."""
283
+
284
+ image_cache_max: int = 256
285
+ """FIFO bound on processed image entries. Renderer-internal."""
286
+
287
+ _internal_fields = frozenset({"image_cache_max"})
288
+
289
+ @model_validator(mode="after")
290
+ def _check_thinking_retention(self):
291
+ _reject_thinking_retention_conflict(
292
+ self,
293
+ "preserve_thinking",
294
+ true_implies="all",
295
+ false_implies="tool_cycle",
296
+ )
297
+ return self
298
+
299
+
273
300
  class GLM5RendererConfig(BaseRendererConfig):
274
301
  """GLM-5 renderer config."""
275
302
 
@@ -546,6 +573,37 @@ class LagunaXS21RendererConfig(BaseRendererConfig):
546
573
  upstream default."""
547
574
 
548
575
 
576
+ class LagunaS21RendererConfig(BaseRendererConfig):
577
+ """Laguna S-2.1 renderer config.
578
+
579
+ S-2.1 is a larger sibling of XS-2.1 sharing its tokenizer (same vocab,
580
+ special tokens, and merges), but its chat template is *not* byte-identical:
581
+ ``enable_thinking`` defaults to ``True`` (XS-2.1 defaults ``False``), and a
582
+ new ``preserve_thinking`` kwarg widens the reasoning-display gate to
583
+ ``enable_thinking or preserve_thinking``. The token format is otherwise
584
+ identical, so this is served by
585
+ :class:`renderers.laguna_s21.LagunaS21Renderer`, a thin subclass of
586
+ ``LagunaXS21Renderer`` that only overrides that gate.
587
+ """
588
+
589
+ name: Literal["laguna-s-2.1"] = "laguna-s-2.1"
590
+
591
+ enable_thinking: bool = True
592
+ """When ``True``, the generation prompt ends with ``<think>`` and every
593
+ assistant turn renders ``<think>{reasoning}</think>``; when ``False``,
594
+ turns open with a bare ``</think>``. Mirrors the template's
595
+ ``enable_thinking`` kwarg — note S-2.1's upstream default is ``True``,
596
+ unlike XS-2.1's ``False``."""
597
+
598
+ preserve_thinking: bool = False
599
+ """When ``True``, assistant turns keep their ``<think>{reasoning}</think>``
600
+ block even while ``enable_thinking`` is ``False`` — the template gates
601
+ reasoning display on ``enable_thinking or preserve_thinking``. With the
602
+ default ``False`` the gate collapses to ``enable_thinking`` and the
603
+ renderer matches XS-2.1 turn-for-turn. Mirrors the template's
604
+ ``preserve_thinking`` kwarg and its upstream default."""
605
+
606
+
549
607
  class Llama3RendererConfig(BaseRendererConfig):
550
608
  """Llama-3.x Instruct renderer config.
551
609
 
@@ -725,6 +783,7 @@ RendererConfig = Annotated[
725
783
  Qwen35RendererConfig,
726
784
  Qwen36RendererConfig,
727
785
  Qwen3VLRendererConfig,
786
+ Gemma4RendererConfig,
728
787
  GLM5RendererConfig,
729
788
  GLM51RendererConfig,
730
789
  GLM45RendererConfig,
@@ -735,6 +794,7 @@ RendererConfig = Annotated[
735
794
  LagunaXS2RendererConfig,
736
795
  LagunaM1RendererConfig,
737
796
  LagunaXS21RendererConfig,
797
+ LagunaS21RendererConfig,
738
798
  Llama3RendererConfig,
739
799
  MiniMaxM2RendererConfig,
740
800
  Nemotron3RendererConfig,
@@ -767,6 +827,7 @@ _CONFIG_BY_NAME: dict[str, type[BaseRendererConfig]] = {
767
827
  "qwen3.5": Qwen35RendererConfig,
768
828
  "qwen3.6": Qwen36RendererConfig,
769
829
  "qwen3-vl": Qwen3VLRendererConfig,
830
+ "gemma4": Gemma4RendererConfig,
770
831
  "glm-5": GLM5RendererConfig,
771
832
  "glm-5.1": GLM51RendererConfig,
772
833
  "glm-4.5": GLM45RendererConfig,
@@ -777,6 +838,7 @@ _CONFIG_BY_NAME: dict[str, type[BaseRendererConfig]] = {
777
838
  "laguna-xs.2": LagunaXS2RendererConfig,
778
839
  "laguna-m.1": LagunaM1RendererConfig,
779
840
  "laguna-xs-2.1": LagunaXS21RendererConfig,
841
+ "laguna-s-2.1": LagunaS21RendererConfig,
780
842
  "llama-3": Llama3RendererConfig,
781
843
  "minimax-m2": MiniMaxM2RendererConfig,
782
844
  "nemotron-3": Nemotron3RendererConfig,
@@ -820,11 +882,13 @@ __all__ = [
820
882
  "GLM45RendererConfig",
821
883
  "GLM51RendererConfig",
822
884
  "GLM5RendererConfig",
885
+ "Gemma4RendererConfig",
823
886
  "GptOssRendererConfig",
824
887
  "Hy3RendererConfig",
825
888
  "KimiK25RendererConfig",
826
889
  "KimiK2RendererConfig",
827
890
  "LagunaM1RendererConfig",
891
+ "LagunaS21RendererConfig",
828
892
  "LagunaXS2RendererConfig",
829
893
  "LagunaXS21RendererConfig",
830
894
  "Llama3RendererConfig",