renderers 0.1.10.dev10__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.dev10 → renderers-0.1.10.dev11}/PKG-INFO +3 -3
  2. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/README.md +2 -2
  3. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/docs/renderer-config.md +2 -0
  4. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/renderers/__init__.py +4 -0
  5. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/renderers/_version.py +2 -2
  6. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/renderers/base.py +14 -0
  7. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/renderers/client.py +66 -0
  8. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/renderers/configs.py +30 -0
  9. renderers-0.1.10.dev11/renderers/gemma4.py +1369 -0
  10. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/tests/conftest.py +1 -0
  11. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/tests/test_bridge.py +1 -0
  12. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/tests/test_build_helpers.py +14 -0
  13. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/tests/test_client.py +59 -0
  14. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/tests/test_disabled_thinking_stability.py +10 -2
  15. renderers-0.1.10.dev11/tests/test_gemma4.py +473 -0
  16. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/tests/test_multimodal.py +69 -14
  17. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/tests/test_render_ids.py +20 -0
  18. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/tests/test_renderer_config_parity.py +16 -8
  19. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/tests/test_roundtrip.py +1 -0
  20. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/.github/workflows/publish-dev.yml +0 -0
  21. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/.github/workflows/publish.yml +0 -0
  22. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/.github/workflows/style.yml +0 -0
  23. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/.github/workflows/test.yml +0 -0
  24. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/.gitignore +0 -0
  25. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/.pre-commit-config.yaml +0 -0
  26. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/LICENSE +0 -0
  27. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/examples/README.md +0 -0
  28. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/examples/sglang/multiturn_generate_sglang.py +0 -0
  29. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/examples/sglang/online_multiturn_sglang.py +0 -0
  30. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/examples/tinker/multiturn_generate_tinker.py +0 -0
  31. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/examples/transformers/multiturn_generate_transformers.py +0 -0
  32. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/examples/vllm/multiturn_generate_vllm.py +0 -0
  33. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/pyproject.toml +0 -0
  34. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/renderers/deepseek_r1.py +0 -0
  35. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/renderers/deepseek_v3.py +0 -0
  36. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/renderers/default.py +0 -0
  37. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/renderers/glm45.py +0 -0
  38. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/renderers/glm5.py +0 -0
  39. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/renderers/gpt_oss.py +0 -0
  40. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/renderers/hy3.py +0 -0
  41. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/renderers/kimi_k2.py +0 -0
  42. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/renderers/kimi_k25.py +0 -0
  43. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/renderers/laguna_s21.py +0 -0
  44. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/renderers/laguna_xs2.py +0 -0
  45. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/renderers/llama_3.py +0 -0
  46. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/renderers/minimax_m2.py +0 -0
  47. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/renderers/nemotron3.py +0 -0
  48. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/renderers/parsers.py +0 -0
  49. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/renderers/parsing.py +0 -0
  50. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/renderers/prime_qwen3.py +0 -0
  51. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/renderers/qwen3.py +0 -0
  52. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/renderers/qwen35.py +0 -0
  53. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/renderers/qwen36.py +0 -0
  54. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/renderers/qwen3_vl.py +0 -0
  55. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/tests/test_deepseek_r1.py +0 -0
  56. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/tests/test_glm_tool_name_validation.py +0 -0
  57. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/tests/test_gpt_oss_harmony_parity.py +0 -0
  58. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/tests/test_hy3.py +0 -0
  59. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/tests/test_incremental.py +0 -0
  60. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/tests/test_is_content.py +0 -0
  61. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/tests/test_kimi_k25_tool_schema.py +0 -0
  62. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/tests/test_laguna_m1.py +0 -0
  63. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/tests/test_laguna_s21.py +0 -0
  64. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/tests/test_laguna_xs21.py +0 -0
  65. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/tests/test_llama_3.py +0 -0
  66. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/tests/test_load_tokenizer.py +0 -0
  67. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/tests/test_message_indices.py +0 -0
  68. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/tests/test_message_tool_names.py +0 -0
  69. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/tests/test_nemotron3_parity.py +0 -0
  70. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/tests/test_nemotron3_ultra.py +0 -0
  71. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/tests/test_parse_response.py +0 -0
  72. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/tests/test_parse_response_robustness.py +0 -0
  73. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/tests/test_parsers.py +0 -0
  74. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/tests/test_preserve_thinking.py +0 -0
  75. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/tests/test_prime_qwen3_parity.py +0 -0
  76. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/tests/test_qwen35_size_coverage.py +0 -0
  77. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/tests/test_renderer_config.py +0 -0
  78. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/tests/test_sampled_mask.py +0 -0
  79. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/tests/test_tokens_per_message.py +0 -0
  80. {renderers-0.1.10.dev10 → renderers-0.1.10.dev11}/tests/test_tool_arg_type_preservation.py +0 -0
  81. {renderers-0.1.10.dev10 → 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.dev10
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-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.
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-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.
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` |
@@ -131,6 +132,7 @@ the knobs its template actually exposes:
131
132
  | Qwen3 | `enable_thinking=False -> all`, else `tool_cycle` |
132
133
  | Qwen3.5 | `enable_thinking=False -> all`, else `tool_cycle` |
133
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` |
134
136
  | GLM-5 / 5.1 | `clear_thinking=False -> all`; else `enable_thinking=False -> all`; else `tool_cycle` |
135
137
  | GLM-4.5 | `enable_thinking=False -> all`, else `tool_cycle` |
136
138
  | gpt-oss | `auto_drop_analysis=False -> all`, else `tool_cycle` |
@@ -50,6 +50,7 @@ from renderers.configs import (
50
50
  GLM45RendererConfig,
51
51
  GLM51RendererConfig,
52
52
  GLM5RendererConfig,
53
+ Gemma4RendererConfig,
53
54
  GptOssRendererConfig,
54
55
  Hy3RendererConfig,
55
56
  KimiK25RendererConfig,
@@ -90,6 +91,7 @@ _LAZY_RENDERERS: dict[str, str] = {
90
91
  "GLM51Renderer": "renderers.glm5",
91
92
  "GLM5Renderer": "renderers.glm5",
92
93
  "GptOssRenderer": "renderers.gpt_oss",
94
+ "Gemma4Renderer": "renderers.gemma4",
93
95
  "Hy3Renderer": "renderers.hy3",
94
96
  "KimiK25Renderer": "renderers.kimi_k25",
95
97
  "KimiK2Renderer": "renderers.kimi_k2",
@@ -142,6 +144,8 @@ __all__ = [
142
144
  "GLM51RendererConfig",
143
145
  "GLM5Renderer",
144
146
  "GLM5RendererConfig",
147
+ "Gemma4Renderer",
148
+ "Gemma4RendererConfig",
145
149
  "GptOssRenderer",
146
150
  "GptOssRendererConfig",
147
151
  "Hy3Renderer",
@@ -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.dev10'
22
- __version_tuple__ = version_tuple = (0, 1, 10, 'dev10')
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",
@@ -1107,6 +1115,10 @@ MULTIMODAL_MODELS: dict[str, set[str]] = {
1107
1115
  "Qwen/Qwen3-VL-4B-Instruct": {"image"},
1108
1116
  "Qwen/Qwen3-VL-8B-Instruct": {"image"},
1109
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"},
1110
1122
  # Qwen3.5 is itself a VLM family (HF tag ``image-text-to-text``,
1111
1123
  # processor class ``Qwen3VLProcessor``) — same vision tokens and
1112
1124
  # image-processor as Qwen3-VL, with a different tool-call format.
@@ -1326,6 +1338,7 @@ def _populate_registry():
1326
1338
  from renderers.glm5 import GLM5Renderer, GLM51Renderer
1327
1339
  from renderers.glm45 import GLM45Renderer
1328
1340
  from renderers.gpt_oss import GptOssRenderer
1341
+ from renderers.gemma4 import Gemma4Renderer
1329
1342
  from renderers.hy3 import Hy3Renderer
1330
1343
  from renderers.kimi_k2 import KimiK2Renderer
1331
1344
  from renderers.kimi_k25 import KimiK25Renderer
@@ -1354,6 +1367,7 @@ def _populate_registry():
1354
1367
  "qwen3": Qwen3Renderer,
1355
1368
  "prime-qwen3": PrimeQwen3Renderer,
1356
1369
  "qwen3-vl": Qwen3VLRenderer,
1370
+ "gemma4": Gemma4Renderer,
1357
1371
  "qwen3.5": Qwen35Renderer,
1358
1372
  "qwen3.6": Qwen36Renderer,
1359
1373
  "glm-5": GLM5Renderer,
@@ -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
 
@@ -756,6 +783,7 @@ RendererConfig = Annotated[
756
783
  Qwen35RendererConfig,
757
784
  Qwen36RendererConfig,
758
785
  Qwen3VLRendererConfig,
786
+ Gemma4RendererConfig,
759
787
  GLM5RendererConfig,
760
788
  GLM51RendererConfig,
761
789
  GLM45RendererConfig,
@@ -799,6 +827,7 @@ _CONFIG_BY_NAME: dict[str, type[BaseRendererConfig]] = {
799
827
  "qwen3.5": Qwen35RendererConfig,
800
828
  "qwen3.6": Qwen36RendererConfig,
801
829
  "qwen3-vl": Qwen3VLRendererConfig,
830
+ "gemma4": Gemma4RendererConfig,
802
831
  "glm-5": GLM5RendererConfig,
803
832
  "glm-5.1": GLM51RendererConfig,
804
833
  "glm-4.5": GLM45RendererConfig,
@@ -853,6 +882,7 @@ __all__ = [
853
882
  "GLM45RendererConfig",
854
883
  "GLM51RendererConfig",
855
884
  "GLM5RendererConfig",
885
+ "Gemma4RendererConfig",
856
886
  "GptOssRendererConfig",
857
887
  "Hy3RendererConfig",
858
888
  "KimiK25RendererConfig",