renderers 0.1.8.dev57__tar.gz → 0.1.8.dev61__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 (74) hide show
  1. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/PKG-INFO +1 -1
  2. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/renderers/_version.py +2 -2
  3. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/renderers/base.py +34 -2
  4. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/renderers/client.py +17 -6
  5. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/tests/test_build_helpers.py +28 -0
  6. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/.github/workflows/publish-dev.yml +0 -0
  7. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/.github/workflows/publish.yml +0 -0
  8. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/.github/workflows/style.yml +0 -0
  9. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/.github/workflows/test.yml +0 -0
  10. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/.gitignore +0 -0
  11. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/.pre-commit-config.yaml +0 -0
  12. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/LICENSE +0 -0
  13. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/README.md +0 -0
  14. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/docs/renderer-config.md +0 -0
  15. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/examples/README.md +0 -0
  16. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/examples/sglang/multiturn_generate_sglang.py +0 -0
  17. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/examples/sglang/online_multiturn_sglang.py +0 -0
  18. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/examples/tinker/multiturn_generate_tinker.py +0 -0
  19. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/examples/transformers/multiturn_generate_transformers.py +0 -0
  20. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/examples/vllm/multiturn_generate_vllm.py +0 -0
  21. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/pyproject.toml +0 -0
  22. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/renderers/__init__.py +0 -0
  23. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/renderers/configs.py +0 -0
  24. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/renderers/deepseek_r1.py +0 -0
  25. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/renderers/deepseek_v3.py +0 -0
  26. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/renderers/default.py +0 -0
  27. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/renderers/glm45.py +0 -0
  28. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/renderers/glm5.py +0 -0
  29. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/renderers/gpt_oss.py +0 -0
  30. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/renderers/hy3.py +0 -0
  31. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/renderers/kimi_k2.py +0 -0
  32. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/renderers/kimi_k25.py +0 -0
  33. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/renderers/laguna_xs2.py +0 -0
  34. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/renderers/llama_3.py +0 -0
  35. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/renderers/minimax_m2.py +0 -0
  36. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/renderers/nemotron3.py +0 -0
  37. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/renderers/parsers.py +0 -0
  38. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/renderers/parsing.py +0 -0
  39. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/renderers/prime_qwen3.py +0 -0
  40. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/renderers/qwen3.py +0 -0
  41. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/renderers/qwen35.py +0 -0
  42. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/renderers/qwen36.py +0 -0
  43. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/renderers/qwen3_vl.py +0 -0
  44. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/tests/conftest.py +0 -0
  45. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/tests/test_bridge.py +0 -0
  46. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/tests/test_client.py +0 -0
  47. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/tests/test_deepseek_r1.py +0 -0
  48. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/tests/test_gpt_oss_harmony_parity.py +0 -0
  49. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/tests/test_hy3.py +0 -0
  50. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/tests/test_incremental.py +0 -0
  51. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/tests/test_is_content.py +0 -0
  52. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/tests/test_kimi_k25_tool_schema.py +0 -0
  53. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/tests/test_laguna_xs21.py +0 -0
  54. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/tests/test_llama_3.py +0 -0
  55. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/tests/test_load_tokenizer.py +0 -0
  56. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/tests/test_message_indices.py +0 -0
  57. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/tests/test_message_tool_names.py +0 -0
  58. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/tests/test_multimodal.py +0 -0
  59. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/tests/test_nemotron3_parity.py +0 -0
  60. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/tests/test_nemotron3_ultra.py +0 -0
  61. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/tests/test_parse_response.py +0 -0
  62. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/tests/test_parse_response_robustness.py +0 -0
  63. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/tests/test_parsers.py +0 -0
  64. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/tests/test_preserve_thinking.py +0 -0
  65. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/tests/test_prime_qwen3_parity.py +0 -0
  66. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/tests/test_qwen35_size_coverage.py +0 -0
  67. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/tests/test_render_ids.py +0 -0
  68. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/tests/test_renderer_config.py +0 -0
  69. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/tests/test_renderer_config_parity.py +0 -0
  70. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/tests/test_roundtrip.py +0 -0
  71. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/tests/test_sampled_mask.py +0 -0
  72. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/tests/test_tokens_per_message.py +0 -0
  73. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/tests/test_tool_arg_type_preservation.py +0 -0
  74. {renderers-0.1.8.dev57 → renderers-0.1.8.dev61}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: renderers
3
- Version: 0.1.8.dev57
3
+ Version: 0.1.8.dev61
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.8.dev57'
22
- __version_tuple__ = version_tuple = (0, 1, 8, 'dev57')
21
+ __version__ = version = '0.1.8.dev61'
22
+ __version_tuple__ = version_tuple = (0, 1, 8, 'dev61')
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -1594,6 +1594,7 @@ def build_training_sample(
1594
1594
  role_to_mask: Callable[[Message], bool] | None = None,
1595
1595
  tools: list[ToolSpec] | None = None,
1596
1596
  content_sft_roles: "set[str] | frozenset[str] | None" = None,
1597
+ ensure_final_stop: bool = False,
1597
1598
  ) -> RenderedTrainingSample:
1598
1599
  """Build a :class:`RenderedTrainingSample` for supervised training.
1599
1600
 
@@ -1649,6 +1650,17 @@ def build_training_sample(
1649
1650
  or hand-coded renderers that haven't been wired up yet) ignore
1650
1651
  ``content_sft_roles`` silently — falling back to the original
1651
1652
  ``role_to_mask`` + ``sampled_mask`` behaviour.
1653
+
1654
+ ``ensure_final_stop`` appends the renderer's canonical stop token
1655
+ when the sample ends with an assistant message that the template
1656
+ leaves unterminated. Some templates close an assistant turn only
1657
+ via the *next* message's role marker (e.g. GLM's ``<|user|>`` /
1658
+ ``<|observation|>``), so a final assistant message renders with no
1659
+ stop token at all. No-op when the template already closes the turn
1660
+ in-message (ChatML ``<|im_end|>``, Llama ``<|eot_id|>``); where it
1661
+ fires, the output intentionally diverges from ``apply_chat_template``.
1662
+ Ignored for renderers without ``sampled_mask`` (``DefaultRenderer``) —
1663
+ the close of an opaque template can't be located reliably.
1652
1664
  """
1653
1665
  rendered = renderer.render(messages, tools=tools)
1654
1666
  has_sampled_info = len(rendered.sampled_mask) == len(rendered.token_ids)
@@ -1689,6 +1701,25 @@ def build_training_sample(
1689
1701
  else:
1690
1702
  loss_mask.append(role_to_mask(msg))
1691
1703
 
1704
+ token_ids = list(rendered.token_ids)
1705
+ # Requires sampled_mask (opaque templates hide the assistant close)
1706
+ # and a final assistant message the role filter trains.
1707
+ if (
1708
+ ensure_final_stop
1709
+ and has_sampled_info
1710
+ and messages[-1].get("role") == "assistant"
1711
+ and (role_to_mask is None or role_to_mask(messages[-1]))
1712
+ ):
1713
+ stop_ids = set(renderer.get_stop_token_ids())
1714
+ last_trainable = next(
1715
+ (k for k in range(len(loss_mask) - 1, -1, -1) if loss_mask[k]), None
1716
+ )
1717
+ if last_trainable is None or token_ids[last_trainable] not in stop_ids:
1718
+ token_ids.append(renderer.get_stop_token_ids()[0])
1719
+ # loss_mask=True marks the token as trainable — the appended
1720
+ # stop is a training target, like any sampled token.
1721
+ loss_mask.append(True)
1722
+
1692
1723
  # Surface the multimodal payload for VLM renderers. ``None`` for text
1693
1724
  # renderers and for text-only samples (empty media) so downstream
1694
1725
  # ``multi_modal_data is not None`` is a reliable "has media" check.
@@ -1696,12 +1727,12 @@ def build_training_sample(
1696
1727
  if mm is not None and mm.is_empty():
1697
1728
  mm = None
1698
1729
  mm_token_type_ids = (
1699
- _build_mm_token_type_ids(mm.mm_placeholders, len(rendered.token_ids))
1730
+ _build_mm_token_type_ids(mm.mm_placeholders, len(token_ids))
1700
1731
  if mm is not None and mm.mm_placeholders
1701
1732
  else None
1702
1733
  )
1703
1734
  return RenderedTrainingSample(
1704
- token_ids=rendered.token_ids,
1735
+ token_ids=token_ids,
1705
1736
  loss_mask=loss_mask,
1706
1737
  multi_modal_data=mm,
1707
1738
  mm_token_type_ids=mm_token_type_ids,
@@ -1975,6 +2006,7 @@ def build_trajectory_step(
1975
2006
  "completion_mask": [True] * len(completion_ids),
1976
2007
  "completion_logprobs": [0.0] * len(completion_ids),
1977
2008
  "routed_experts": None,
2009
+ "kept_tokens": None,
1978
2010
  }
1979
2011
  if (
1980
2012
  full_rendered.multi_modal_data is not None
@@ -32,6 +32,7 @@ from renderers.base import (
32
32
 
33
33
  _request_logger = logging.getLogger("renderers.client")
34
34
  ROUTED_EXPERTS_DATA_PREFIX = b'"routed_experts":{"data":"'
35
+ KEPT_TOKENS_IDS_PREFIX = b'"kept_tokens":{"ids":"'
35
36
 
36
37
 
37
38
  class OverlongPromptError(Exception):
@@ -121,23 +122,31 @@ async def _maybe_offload(renderer: Renderer | RendererPool, fn):
121
122
  return fn()
122
123
 
123
124
 
124
- def strip_routed_experts_data(raw: bytes) -> tuple[bytes, memoryview | None]:
125
- data_start = raw.find(ROUTED_EXPERTS_DATA_PREFIX)
125
+ def _strip_base64_field(raw: bytes, prefix: bytes) -> tuple[bytes, memoryview | None]:
126
+ """Splice a large base64 string field out of raw JSON bytes.
127
+
128
+ Avoids json-decoding megabytes of base64; the returned memoryview
129
+ references ``raw`` and is re-inserted into the parsed payload.
130
+ """
131
+ data_start = raw.find(prefix)
126
132
  if data_start < 0:
127
133
  return raw, None
128
134
 
129
- data_start += len(ROUTED_EXPERTS_DATA_PREFIX)
135
+ data_start += len(prefix)
130
136
  data_end = raw.index(b'"', data_start)
131
- routed_data = memoryview(raw)[data_start:data_end]
137
+ data = memoryview(raw)[data_start:data_end]
132
138
  stripped = raw[:data_start] + raw[data_end:]
133
- return stripped, routed_data
139
+ return stripped, data
134
140
 
135
141
 
136
142
  def parse_generate_response(raw: bytes) -> dict[str, Any]:
137
- stripped, routed_data = strip_routed_experts_data(raw)
143
+ stripped, routed_data = _strip_base64_field(raw, ROUTED_EXPERTS_DATA_PREFIX)
144
+ stripped, kept_ids_data = _strip_base64_field(stripped, KEPT_TOKENS_IDS_PREFIX)
138
145
  payload: dict[str, Any] = json.loads(stripped)
139
146
  if routed_data is not None:
140
147
  payload["choices"][0]["routed_experts"]["data"] = routed_data
148
+ if kept_ids_data is not None:
149
+ payload["choices"][0]["kept_tokens"]["ids"] = kept_ids_data
141
150
  return payload
142
151
 
143
152
 
@@ -293,6 +302,7 @@ async def generate(
293
302
  completion_logprobs = [float(c.get("logprob") or 0.0) for c in content_lp or []]
294
303
 
295
304
  routed_experts = choice.get("routed_experts")
305
+ kept_tokens = choice.get("kept_tokens")
296
306
 
297
307
  # /inference/v1/generate returns finish_reason in {"stop","length",...} —
298
308
  # never "tool_calls" (a chat-completions concept). Promote stop→tool_calls
@@ -319,6 +329,7 @@ async def generate(
319
329
  "tool_calls": parsed.tool_calls,
320
330
  "finish_reason": finish_reason,
321
331
  "routed_experts": routed_experts,
332
+ "kept_tokens": kept_tokens,
322
333
  # The mm sidecar consumed on the request side, surfaced back so
323
334
  # callers can persist it on the trajectory step for downstream
324
335
  # multi-turn bridging and training-sample construction.
@@ -64,6 +64,34 @@ def test_build_training_sample_has_trainable_tokens(model_name, tokenizer, rende
64
64
  assert len(mask) == len(ids)
65
65
 
66
66
 
67
+ def test_build_training_sample_ensures_final_stop(model_name, tokenizer, renderer):
68
+ """The final assistant turn ends at a trainable renderer stop token.
69
+
70
+ Templates whose assistant close is part of the message (ChatML, Llama)
71
+ already satisfy this, so the sample stays byte-identical; templates
72
+ that terminate turns with the next message's role marker (GLM) get the
73
+ canonical stop appended as the training target.
74
+ """
75
+ if not renderer.render([{"role": "user", "content": "x"}]).sampled_mask:
76
+ return # DefaultRenderer: no sampled_mask, role-only masking
77
+ msgs = [
78
+ {"role": "user", "content": "Hi"},
79
+ {"role": "assistant", "content": "Hello!"},
80
+ ]
81
+ sample = build_training_sample(renderer, msgs, ensure_final_stop=True)
82
+ stop_ids = set(renderer.get_stop_token_ids())
83
+ last_trainable = max(k for k, m in enumerate(sample.loss_mask) if m)
84
+ assert sample.token_ids[last_trainable] in stop_ids
85
+
86
+ baseline = build_training_sample(renderer, msgs)
87
+ if (
88
+ baseline.token_ids[max(k for k, m in enumerate(baseline.loss_mask) if m)]
89
+ in stop_ids
90
+ ):
91
+ # In-message close: ensure_final_stop must not change the bytes.
92
+ assert sample.token_ids == baseline.token_ids
93
+
94
+
67
95
  def test_build_trajectory_step_reconstructs_full(model_name, tokenizer, renderer):
68
96
  """prompt_ids + completion_ids must equal the full rendered sequence."""
69
97
  prompt = [
File without changes
File without changes