renderers 0.1.8.dev28__tar.gz → 0.1.8.dev30__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 (63) hide show
  1. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/PKG-INFO +1 -1
  2. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/renderers/_version.py +2 -2
  3. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/renderers/nemotron3.py +2 -1
  4. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/renderers/parsing.py +17 -11
  5. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/tests/test_tool_arg_type_preservation.py +74 -1
  6. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/.github/workflows/publish-dev.yml +0 -0
  7. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/.github/workflows/publish.yml +0 -0
  8. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/.github/workflows/style.yml +0 -0
  9. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/.github/workflows/test.yml +0 -0
  10. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/.gitignore +0 -0
  11. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/.pre-commit-config.yaml +0 -0
  12. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/LICENSE +0 -0
  13. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/README.md +0 -0
  14. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/docs/renderer-config.md +0 -0
  15. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/examples/README.md +0 -0
  16. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/examples/sglang/multiturn_generate_sglang.py +0 -0
  17. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/examples/sglang/online_multiturn_sglang.py +0 -0
  18. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/examples/tinker/multiturn_generate_tinker.py +0 -0
  19. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/examples/transformers/multiturn_generate_transformers.py +0 -0
  20. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/examples/vllm/multiturn_generate_vllm.py +0 -0
  21. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/pyproject.toml +0 -0
  22. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/renderers/__init__.py +0 -0
  23. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/renderers/base.py +0 -0
  24. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/renderers/client.py +0 -0
  25. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/renderers/configs.py +0 -0
  26. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/renderers/deepseek_v3.py +0 -0
  27. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/renderers/default.py +0 -0
  28. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/renderers/glm45.py +0 -0
  29. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/renderers/glm5.py +0 -0
  30. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/renderers/gpt_oss.py +0 -0
  31. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/renderers/kimi_k2.py +0 -0
  32. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/renderers/kimi_k25.py +0 -0
  33. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/renderers/laguna_xs2.py +0 -0
  34. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/renderers/minimax_m2.py +0 -0
  35. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/renderers/parsers.py +0 -0
  36. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/renderers/qwen3.py +0 -0
  37. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/renderers/qwen35.py +0 -0
  38. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/renderers/qwen36.py +0 -0
  39. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/renderers/qwen3_vl.py +0 -0
  40. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/tests/conftest.py +0 -0
  41. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/tests/test_bridge.py +0 -0
  42. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/tests/test_build_helpers.py +0 -0
  43. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/tests/test_client.py +0 -0
  44. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/tests/test_gpt_oss_harmony_parity.py +0 -0
  45. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/tests/test_incremental.py +0 -0
  46. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/tests/test_is_content.py +0 -0
  47. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/tests/test_kimi_k25_tool_schema.py +0 -0
  48. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/tests/test_load_tokenizer.py +0 -0
  49. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/tests/test_load_tokenizer_fastokens.py +0 -0
  50. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/tests/test_message_indices.py +0 -0
  51. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/tests/test_multimodal.py +0 -0
  52. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/tests/test_parse_response.py +0 -0
  53. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/tests/test_parse_response_robustness.py +0 -0
  54. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/tests/test_parsers.py +0 -0
  55. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/tests/test_preserve_thinking.py +0 -0
  56. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/tests/test_qwen35_size_coverage.py +0 -0
  57. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/tests/test_render_ids.py +0 -0
  58. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/tests/test_renderer_config.py +0 -0
  59. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/tests/test_renderer_config_parity.py +0 -0
  60. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/tests/test_roundtrip.py +0 -0
  61. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/tests/test_sampled_mask.py +0 -0
  62. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/tests/test_tokens_per_message.py +0 -0
  63. {renderers-0.1.8.dev28 → renderers-0.1.8.dev30}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: renderers
3
- Version: 0.1.8.dev28
3
+ Version: 0.1.8.dev30
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.dev28'
22
- __version_tuple__ = version_tuple = (0, 1, 8, 'dev28')
21
+ __version__ = version = '0.1.8.dev30'
22
+ __version_tuple__ = version_tuple = (0, 1, 8, 'dev30')
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -430,7 +430,7 @@ class Nemotron3Renderer:
430
430
  self,
431
431
  token_ids: list[int],
432
432
  *,
433
- tools: list[ToolSpec] | None = None, # noqa: ARG002 — args land in a JSON object, schema not needed
433
+ tools: list[ToolSpec] | None = None,
434
434
  ) -> ParsedResponse:
435
435
  stop_ids = {self._im_end}
436
436
  if self._endoftext is not None:
@@ -443,6 +443,7 @@ class Nemotron3Renderer:
443
443
  think_end_id=self._think_end,
444
444
  tool_call_id=self._tool_call,
445
445
  tool_call_end_id=self._tool_call_end,
446
+ tools=tools,
446
447
  )
447
448
 
448
449
  def get_stop_token_ids(self) -> list[int]:
@@ -65,30 +65,36 @@ def _coerce_arg_value(
65
65
  """Coerce a raw ``<arg_value>`` body to its declared type.
66
66
 
67
67
  Returns ``(value, used_json_fallback)``. The boolean is ``True`` only
68
- when ``json.loads`` was attempted and raised, so the caller knows
69
- whether to flag ``INVALID_JSON``. Returning a string verbatim
70
- because the schema declared ``type: "string"`` is NOT a fallback.
68
+ when ``json.loads`` was attempted, raised, AND the schema doesn't
69
+ permit a string. Returning a string verbatim because the schema
70
+ permits strings is NOT a fallback.
71
71
 
72
72
  Rule (matches vLLM / SGLang reference parsers):
73
73
 
74
74
  - If the param's declared ``type`` is ``"string"`` (or single-element
75
75
  ``["string"]``), return ``text`` verbatim — never ``json.loads``.
76
- - Anything else (no schema, non-string scalar, object, array, or
77
- union types that include non-string): try ``json.loads`` and fall
78
- back to raw ``text`` on parse failure.
79
-
80
- Union types that include ``"string"`` alongside other types still
81
- attempt ``json.loads`` first so an explicit integer / bool can
82
- parse; the string branch only wins as the fallback.
76
+ - Otherwise try ``json.loads``. If that fails, return raw ``text``.
77
+ The ``used_json_fallback`` flag is ``True`` only when the schema
78
+ does NOT permit a string i.e. the fallback is truly suspect.
79
+
80
+ Union types (``anyOf``/``oneOf``) that include ``"string"`` alongside
81
+ other types still attempt ``json.loads`` first so an explicit
82
+ integer / bool can parse; the string branch wins as fallback, and
83
+ landing there is expected — not a malformed-JSON signal.
83
84
  """
85
+ string_is_allowed = False
84
86
  if param_schema is not None:
85
87
  declared = param_schema.get("type")
86
88
  if declared == "string" or declared == ["string"]:
87
89
  return text, False
90
+ for branch in param_schema.get("anyOf") or param_schema.get("oneOf") or []:
91
+ if isinstance(branch, dict) and branch.get("type") == "string":
92
+ string_is_allowed = True
93
+ break
88
94
  try:
89
95
  return json.loads(text), False
90
96
  except (json.JSONDecodeError, ValueError):
91
- return text, True
97
+ return text, not string_is_allowed
92
98
 
93
99
 
94
100
  def _find(ids: list[int], target: int, start: int = 0) -> int:
@@ -26,7 +26,7 @@ import pytest
26
26
 
27
27
 
28
28
  # (HuggingFace model name, renderer name). Two JSON-shaped controls
29
- # (string types already preserved by the wire format) + four XML-style
29
+ # (string types already preserved by the wire format) + five XML-style
30
30
  # parsers that rely on the schema to preserve them.
31
31
  _MODELS = [
32
32
  ("Qwen/Qwen3-8B", "auto"), # hermes JSON — control
@@ -35,6 +35,7 @@ _MODELS = [
35
35
  ("zai-org/GLM-5", "auto"), # XML
36
36
  ("MiniMaxAI/MiniMax-M2.5", "auto"), # XML
37
37
  ("poolside/Laguna-XS.2", "auto"), # XML
38
+ ("nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16", "auto"), # XML
38
39
  ]
39
40
 
40
41
 
@@ -137,3 +138,75 @@ def test_string_arg_preserves_type(model, renderer_name, renderer, args):
137
138
  assert got == args, (
138
139
  f"{model}: tool-arg type drift — sent {args!r}, parser returned {got!r}"
139
140
  )
141
+
142
+
143
+ # Schemas where ``string`` is one branch of a union (``anyOf`` / ``oneOf``).
144
+ # These are common in practice — e.g. ``form_input.value: str | bool`` in
145
+ # Pydantic serialises to ``{"anyOf": [{"type": "string"}, {"type": "boolean"}]}``.
146
+ # Without the union-aware check, the XML parser's ``json.loads`` falls back
147
+ # to raw text for bare strings, but flags the call ``INVALID_JSON`` because
148
+ # no top-level ``type`` key declared a string — silently dropping otherwise
149
+ # valid tool calls in the renderer client.
150
+ UNION_WITH_STRING_SCHEMAS = [
151
+ pytest.param(
152
+ {"anyOf": [{"type": "string"}, {"type": "boolean"}]},
153
+ id="anyOf-string-boolean",
154
+ ),
155
+ pytest.param(
156
+ {"anyOf": [{"type": "string"}, {"type": "null"}]},
157
+ id="anyOf-string-null",
158
+ ),
159
+ pytest.param(
160
+ {"oneOf": [{"type": "string"}, {"type": "integer"}]},
161
+ id="oneOf-string-integer",
162
+ ),
163
+ ]
164
+
165
+
166
+ @pytest.mark.parametrize("param_schema", UNION_WITH_STRING_SCHEMAS)
167
+ def test_union_with_string_emits_ok_status(
168
+ model, renderer_name, renderer, param_schema
169
+ ):
170
+ """Union schemas containing ``string`` must yield ``status=OK`` when
171
+ the model emits a bare string. Pre-fix, ``_coerce_arg_value`` flagged
172
+ this as ``INVALID_JSON`` because the top-level ``type`` key was
173
+ absent (the string branch was under ``anyOf`` / ``oneOf``)."""
174
+ from renderers.base import ToolCallParseStatus
175
+
176
+ tools = [
177
+ {
178
+ "type": "function",
179
+ "function": {
180
+ "name": "f",
181
+ "description": "Test tool with one union-typed parameter.",
182
+ "parameters": {
183
+ "type": "object",
184
+ "properties": {"x": param_schema},
185
+ "required": ["x"],
186
+ },
187
+ },
188
+ }
189
+ ]
190
+ args = {"x": "abc"}
191
+ msg = {
192
+ "role": "assistant",
193
+ "content": "",
194
+ "tool_calls": [
195
+ {
196
+ "id": "functions.f:0",
197
+ "function": {"name": "f", "arguments": args},
198
+ }
199
+ ],
200
+ }
201
+ completion_ids = _extract_assistant_tokens(renderer, PROMPT, msg, tools=tools)
202
+ parsed = renderer.parse_response(completion_ids, tools=tools)
203
+
204
+ assert parsed.tool_calls, f"{model}: parser returned no tool_calls"
205
+ tc = parsed.tool_calls[0]
206
+ assert tc.status == ToolCallParseStatus.OK, (
207
+ f"{model}: union-with-string schema flagged {tc.status} on bare string"
208
+ )
209
+ got = _normalize_args(tc.arguments)
210
+ assert got == args, (
211
+ f"{model}: tool-arg drift — sent {args!r}, parser returned {got!r}"
212
+ )
File without changes
File without changes