renderers 0.1.12.dev12__tar.gz → 0.1.12.dev13__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 (90) hide show
  1. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/PKG-INFO +1 -1
  2. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/renderers/_version.py +2 -2
  3. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/renderers/qwen35.py +10 -5
  4. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/renderers/qwen38.py +31 -5
  5. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/tests/test_qwen38.py +74 -0
  6. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/.github/workflows/publish-dev.yml +0 -0
  7. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/.github/workflows/publish.yml +0 -0
  8. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/.github/workflows/style.yml +0 -0
  9. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/.github/workflows/test.yml +0 -0
  10. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/.gitignore +0 -0
  11. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/.pre-commit-config.yaml +0 -0
  12. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/LICENSE +0 -0
  13. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/README.md +0 -0
  14. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/docs/renderer-config.md +0 -0
  15. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/examples/README.md +0 -0
  16. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/examples/sglang/multiturn_generate_sglang.py +0 -0
  17. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/examples/sglang/online_multiturn_sglang.py +0 -0
  18. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/examples/tinker/multiturn_generate_tinker.py +0 -0
  19. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/examples/transformers/multiturn_generate_transformers.py +0 -0
  20. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/examples/vllm/multiturn_generate_vllm.py +0 -0
  21. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/pyproject.toml +0 -0
  22. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/renderers/__init__.py +0 -0
  23. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/renderers/base.py +0 -0
  24. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/renderers/client.py +0 -0
  25. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/renderers/configs.py +0 -0
  26. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/renderers/custom.py +0 -0
  27. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/renderers/deepseek_r1.py +0 -0
  28. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/renderers/deepseek_v3.py +0 -0
  29. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/renderers/deepseek_v4.py +0 -0
  30. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/renderers/default.py +0 -0
  31. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/renderers/gemma4.py +0 -0
  32. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/renderers/glm45.py +0 -0
  33. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/renderers/glm5.py +0 -0
  34. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/renderers/gpt_oss.py +0 -0
  35. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/renderers/hy3.py +0 -0
  36. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/renderers/inkling.py +0 -0
  37. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/renderers/kimi_k2.py +0 -0
  38. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/renderers/kimi_k25.py +0 -0
  39. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/renderers/laguna_s21.py +0 -0
  40. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/renderers/laguna_xs2.py +0 -0
  41. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/renderers/llama_3.py +0 -0
  42. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/renderers/minimax_m2.py +0 -0
  43. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/renderers/nemotron3.py +0 -0
  44. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/renderers/parsers.py +0 -0
  45. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/renderers/parsing.py +0 -0
  46. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/renderers/prime_qwen3.py +0 -0
  47. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/renderers/qwen3.py +0 -0
  48. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/renderers/qwen36.py +0 -0
  49. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/renderers/qwen3_vl.py +0 -0
  50. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/renderers/reasoning.py +0 -0
  51. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/tests/conftest.py +0 -0
  52. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/tests/parity.py +0 -0
  53. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/tests/reference_rendering.py +0 -0
  54. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/tests/test_bridge.py +0 -0
  55. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/tests/test_build_helpers.py +0 -0
  56. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/tests/test_client.py +0 -0
  57. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/tests/test_custom.py +0 -0
  58. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/tests/test_deepseek_r1.py +0 -0
  59. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/tests/test_deepseek_v4.py +0 -0
  60. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/tests/test_disabled_thinking_stability.py +0 -0
  61. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/tests/test_gemma4.py +0 -0
  62. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/tests/test_glm_tool_name_validation.py +0 -0
  63. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/tests/test_hy3.py +0 -0
  64. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/tests/test_incremental.py +0 -0
  65. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/tests/test_inkling.py +0 -0
  66. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/tests/test_is_content.py +0 -0
  67. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/tests/test_kimi_k25_tool_schema.py +0 -0
  68. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/tests/test_laguna_m1.py +0 -0
  69. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/tests/test_laguna_xs21.py +0 -0
  70. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/tests/test_llama_3.py +0 -0
  71. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/tests/test_load_tokenizer.py +0 -0
  72. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/tests/test_message_indices.py +0 -0
  73. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/tests/test_message_tool_names.py +0 -0
  74. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/tests/test_multimodal.py +0 -0
  75. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/tests/test_nemotron35_vlm.py +0 -0
  76. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/tests/test_nemotron3_ultra.py +0 -0
  77. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/tests/test_offsetless_tokenizers.py +0 -0
  78. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/tests/test_parity.py +0 -0
  79. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/tests/test_parse_response.py +0 -0
  80. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/tests/test_parse_response_robustness.py +0 -0
  81. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/tests/test_parsers.py +0 -0
  82. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/tests/test_preserve_thinking.py +0 -0
  83. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/tests/test_qwen35_size_coverage.py +0 -0
  84. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/tests/test_reasoning_boundaries.py +0 -0
  85. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/tests/test_renderer_config.py +0 -0
  86. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/tests/test_roundtrip.py +0 -0
  87. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/tests/test_sampled_mask.py +0 -0
  88. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/tests/test_tokens_per_message.py +0 -0
  89. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/tests/test_tool_arg_type_preservation.py +0 -0
  90. {renderers-0.1.12.dev12 → renderers-0.1.12.dev13}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: renderers
3
- Version: 0.1.12.dev12
3
+ Version: 0.1.12.dev13
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
@@ -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.12.dev12'
22
- __version_tuple__ = version_tuple = (0, 1, 12, 'dev12')
21
+ __version__ = version = '0.1.12.dev13'
22
+ __version_tuple__ = version_tuple = (0, 1, 12, 'dev13')
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -23,6 +23,7 @@ path and remain byte-identical to ``apply_chat_template``.
23
23
  from __future__ import annotations
24
24
 
25
25
  import json
26
+ from collections.abc import Iterable
26
27
  from typing import Any
27
28
 
28
29
  from renderers.reasoning import scan_reasoning, prompt_ends_in_reasoning
@@ -252,7 +253,11 @@ class Qwen35Renderer:
252
253
  # ------------------------------------------------------------------
253
254
 
254
255
  @staticmethod
255
- def _render_content(content: Any) -> str:
256
+ def _iter_content_parts(content: list[Any]) -> Iterable[Any]:
257
+ return content
258
+
259
+ @classmethod
260
+ def _render_content(cls, content: Any) -> str:
256
261
  """Render message content to a text string (before tokenization).
257
262
 
258
263
  Handles string, list of text parts, and None. Image / video parts
@@ -268,7 +273,7 @@ class Qwen35Renderer:
268
273
  return content
269
274
  if isinstance(content, list):
270
275
  parts: list[str] = []
271
- for item in content:
276
+ for item in cls._iter_content_parts(content):
272
277
  if isinstance(item, str):
273
278
  parts.append(item)
274
279
  elif isinstance(item, dict):
@@ -489,7 +494,7 @@ class Qwen35Renderer:
489
494
  emit_text_segments(buf_segments, msg_idx, is_sampled=False)
490
495
  buf_segments.clear()
491
496
 
492
- for item in content_list:
497
+ for item in self._iter_content_parts(content_list):
493
498
  if isinstance(item, str):
494
499
  if item:
495
500
  buf_segments.append((item, True))
@@ -864,7 +869,7 @@ class Qwen35Renderer:
864
869
  emit_text_segments(buf_segments, msg_idx)
865
870
  buf_segments.clear()
866
871
 
867
- for item in content_list:
872
+ for item in self._iter_content_parts(content_list):
868
873
  if isinstance(item, str):
869
874
  if item:
870
875
  buf_segments.append((item, True))
@@ -1192,7 +1197,7 @@ class Qwen35Renderer:
1192
1197
  emit_text_segments(buf_segments, msg_idx, is_sampled=False)
1193
1198
  buf_segments.clear()
1194
1199
 
1195
- for item in raw_content:
1200
+ for item in self._iter_content_parts(raw_content):
1196
1201
  if isinstance(item, str):
1197
1202
  if item:
1198
1203
  buf_segments.append((item, True))
@@ -14,10 +14,13 @@ argument serialization, with three template changes:
14
14
 
15
15
  from __future__ import annotations
16
16
 
17
+ from collections.abc import Iterable
18
+ from typing import Any
19
+
17
20
  from renderers.base import Message
18
21
  from renderers.configs import Qwen38RendererConfig
19
- from renderers.qwen35 import Qwen35Renderer
20
22
  from renderers.qwen36 import Qwen36Renderer
23
+ from renderers.qwen3_vl import _is_image_part, _is_video_part
21
24
 
22
25
 
23
26
  _REASONING_INSTRUCTIONS = {
@@ -48,12 +51,35 @@ class Qwen38Renderer(Qwen36Renderer):
48
51
  def _omit_empty_system_message(self) -> bool:
49
52
  return True
50
53
 
54
+ @staticmethod
55
+ def _iter_content_parts(content: list[Any]) -> Iterable[Any]:
56
+ """Keep media and string-valued text in every rendering path."""
57
+ for item in content:
58
+ if isinstance(item, str):
59
+ yield item
60
+ elif isinstance(item, dict) and (
61
+ _is_image_part(item)
62
+ or _is_video_part(item)
63
+ or isinstance(item.get("text"), str)
64
+ ):
65
+ yield item
66
+
67
+ @staticmethod
68
+ def _is_user_query_message(msg: Message) -> bool:
69
+ if msg.get("role") != "user":
70
+ return False
71
+ content = Qwen38Renderer._render_content(msg.get("content")).strip()
72
+ return not (
73
+ content.startswith("<tool_response>")
74
+ and content.endswith("</tool_response>")
75
+ )
76
+
51
77
  @staticmethod
52
78
  def _last_query_index(messages: list[Message]) -> int:
53
- last_query_index = Qwen35Renderer._last_query_index(messages)
54
- if last_query_index == len(messages):
55
- raise ValueError("No user query found in messages.")
56
- return last_query_index
79
+ for i in range(len(messages) - 1, -1, -1):
80
+ if Qwen38Renderer._is_user_query_message(messages[i]):
81
+ return i
82
+ raise ValueError("No user query found in messages.")
57
83
 
58
84
  @staticmethod
59
85
  def _extract_assistant_parts(msg: Message, content: str) -> tuple[str, str]:
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from functools import lru_cache
6
+ from copy import deepcopy
6
7
 
7
8
  import pytest
8
9
  from pydantic import TypeAdapter
@@ -176,3 +177,76 @@ def test_qwen38_requires_a_real_user_query(qwen38_model):
176
177
 
177
178
  with pytest.raises(ValueError, match="No user query found"):
178
179
  renderer.render_ids([{"role": "system", "content": "System only."}])
180
+
181
+
182
+ @pytest.mark.parametrize(
183
+ "role,with_image",
184
+ [
185
+ ("system", False),
186
+ ("user", False),
187
+ ("assistant", False),
188
+ ("tool", False),
189
+ ("user", True),
190
+ ("tool", True),
191
+ ],
192
+ )
193
+ def test_qwen38_structured_content_render_and_bridge(role, with_image):
194
+ _, renderer = _qwen38(QWEN38_MODELS[0])
195
+ parts = ["Hello", {"type": "text", "text": " world"}]
196
+ if with_image:
197
+ parts.append({"type": "image", "image": "unused", "text": 123})
198
+ parts.append({"type": "text", "text": "After image"})
199
+ noisy_parts = [
200
+ {"type": "unsupported"},
201
+ *parts,
202
+ {"type": "text", "text": None},
203
+ {"type": "text", "text": 123},
204
+ None,
205
+ ]
206
+ prior = [
207
+ {"role": "user", "content": "First question"},
208
+ {"role": "assistant", "content": "First answer"},
209
+ ]
210
+ clean = {"role": role, "content": parts}
211
+ noisy = {"role": role, "content": noisy_parts}
212
+ original = deepcopy(noisy)
213
+ messages = [noisy, *prior] if role == "system" else [*prior, noisy]
214
+ expected_messages = [clean, *prior] if role == "system" else [*prior, clean]
215
+ assert renderer.render(messages, process_multimodal=False) == renderer.render(
216
+ expected_messages, process_multimodal=False
217
+ )
218
+ if role in {"user", "tool"}:
219
+ previous = renderer.render_ids(prior)
220
+ actual = renderer.bridge_to_next_turn(
221
+ previous, [], [noisy], process_multimodal=False
222
+ )
223
+ assert actual is not None
224
+ assert actual == renderer.bridge_to_next_turn(
225
+ previous, [], [clean], process_multimodal=False
226
+ )
227
+ assert noisy == original
228
+
229
+
230
+ def test_qwen38_structured_tool_response_keeps_current_reasoning():
231
+ tokenizer, _ = _qwen38(QWEN38_MODELS[0])
232
+ renderer = Qwen38Renderer(tokenizer, Qwen38RendererConfig(preserve_thinking=False))
233
+ prior = [
234
+ {"role": "user", "content": "First question"},
235
+ {"role": "assistant", "reasoning_content": "Keep me", "content": ""},
236
+ ]
237
+ content = [
238
+ {"type": "text", "text": " <tool_response>"},
239
+ {"type": "unsupported"},
240
+ {"type": "text", "text": None},
241
+ "result",
242
+ {"type": "text", "text": "</tool_response> "},
243
+ ]
244
+ messages = [*prior, {"role": "user", "content": content}]
245
+ expected = [
246
+ *prior,
247
+ {"role": "user", "content": "<tool_response>result</tool_response>"},
248
+ ]
249
+ assert renderer.render(messages) == renderer.render(expected)
250
+ assert "Keep me" in tokenizer.decode(renderer.render_ids(messages))
251
+ with pytest.raises(ValueError, match="No user query found"):
252
+ renderer.render([messages[-1]])