renderers 0.1.8.dev27__tar.gz → 0.1.8.dev29__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.dev27 → renderers-0.1.8.dev29}/PKG-INFO +1 -1
  2. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/renderers/__init__.py +45 -14
  3. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/renderers/_version.py +2 -2
  4. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/renderers/parsing.py +17 -11
  5. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/tests/test_tool_arg_type_preservation.py +72 -0
  6. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/.github/workflows/publish-dev.yml +0 -0
  7. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/.github/workflows/publish.yml +0 -0
  8. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/.github/workflows/style.yml +0 -0
  9. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/.github/workflows/test.yml +0 -0
  10. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/.gitignore +0 -0
  11. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/.pre-commit-config.yaml +0 -0
  12. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/LICENSE +0 -0
  13. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/README.md +0 -0
  14. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/docs/renderer-config.md +0 -0
  15. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/examples/README.md +0 -0
  16. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/examples/sglang/multiturn_generate_sglang.py +0 -0
  17. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/examples/sglang/online_multiturn_sglang.py +0 -0
  18. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/examples/tinker/multiturn_generate_tinker.py +0 -0
  19. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/examples/transformers/multiturn_generate_transformers.py +0 -0
  20. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/examples/vllm/multiturn_generate_vllm.py +0 -0
  21. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/pyproject.toml +0 -0
  22. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/renderers/base.py +0 -0
  23. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/renderers/client.py +0 -0
  24. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/renderers/configs.py +0 -0
  25. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/renderers/deepseek_v3.py +0 -0
  26. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/renderers/default.py +0 -0
  27. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/renderers/glm45.py +0 -0
  28. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/renderers/glm5.py +0 -0
  29. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/renderers/gpt_oss.py +0 -0
  30. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/renderers/kimi_k2.py +0 -0
  31. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/renderers/kimi_k25.py +0 -0
  32. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/renderers/laguna_xs2.py +0 -0
  33. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/renderers/minimax_m2.py +0 -0
  34. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/renderers/nemotron3.py +0 -0
  35. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/renderers/parsers.py +0 -0
  36. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/renderers/qwen3.py +0 -0
  37. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/renderers/qwen35.py +0 -0
  38. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/renderers/qwen36.py +0 -0
  39. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/renderers/qwen3_vl.py +0 -0
  40. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/tests/conftest.py +0 -0
  41. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/tests/test_bridge.py +0 -0
  42. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/tests/test_build_helpers.py +0 -0
  43. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/tests/test_client.py +0 -0
  44. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/tests/test_gpt_oss_harmony_parity.py +0 -0
  45. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/tests/test_incremental.py +0 -0
  46. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/tests/test_is_content.py +0 -0
  47. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/tests/test_kimi_k25_tool_schema.py +0 -0
  48. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/tests/test_load_tokenizer.py +0 -0
  49. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/tests/test_load_tokenizer_fastokens.py +0 -0
  50. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/tests/test_message_indices.py +0 -0
  51. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/tests/test_multimodal.py +0 -0
  52. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/tests/test_parse_response.py +0 -0
  53. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/tests/test_parse_response_robustness.py +0 -0
  54. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/tests/test_parsers.py +0 -0
  55. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/tests/test_preserve_thinking.py +0 -0
  56. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/tests/test_qwen35_size_coverage.py +0 -0
  57. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/tests/test_render_ids.py +0 -0
  58. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/tests/test_renderer_config.py +0 -0
  59. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/tests/test_renderer_config_parity.py +0 -0
  60. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/tests/test_roundtrip.py +0 -0
  61. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/tests/test_sampled_mask.py +0 -0
  62. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/tests/test_tokens_per_message.py +0 -0
  63. {renderers-0.1.8.dev27 → renderers-0.1.8.dev29}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: renderers
3
- Version: 0.1.8.dev27
3
+ Version: 0.1.8.dev29
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
@@ -59,20 +59,51 @@ from renderers.configs import (
59
59
  Qwen3VLRendererConfig,
60
60
  RendererConfig,
61
61
  )
62
- from renderers.deepseek_v3 import DeepSeekV3Renderer
63
- from renderers.default import DefaultRenderer
64
- from renderers.glm5 import GLM5Renderer, GLM51Renderer
65
- from renderers.glm45 import GLM45Renderer
66
- from renderers.gpt_oss import GptOssRenderer
67
- from renderers.kimi_k2 import KimiK2Renderer
68
- from renderers.kimi_k25 import KimiK25Renderer
69
- from renderers.laguna_xs2 import LagunaXS2Renderer
70
- from renderers.minimax_m2 import MiniMaxM2Renderer
71
- from renderers.nemotron3 import Nemotron3Renderer
72
- from renderers.qwen3 import Qwen3Renderer
73
- from renderers.qwen3_vl import Qwen3VLRenderer
74
- from renderers.qwen35 import Qwen35Renderer
75
- from renderers.qwen36 import Qwen36Renderer
62
+
63
+ # Concrete renderer classes are lazy-loaded so that consumers needing
64
+ # only the config layer (``RendererConfig`` discriminated union) don't
65
+ # pay the ``transformers`` import cost. Each renderer module does
66
+ # ``from transformers.tokenization_utils import PreTrainedTokenizer``
67
+ # at module level, so eager imports here would drag ``transformers``
68
+ # into every downstream ``import renderers``. ``__getattr__`` (PEP 562)
69
+ # resolves the names on first attribute access, so ``from renderers
70
+ # import DefaultRenderer`` and ``renderers.DefaultRenderer`` both work
71
+ # transparently. ``create_renderer`` doesn't depend on these eager
72
+ # imports — ``renderers.base._populate_registry`` lazy-imports the
73
+ # concrete classes itself when a renderer is instantiated.
74
+ _LAZY_RENDERERS: dict[str, str] = {
75
+ "DeepSeekV3Renderer": "renderers.deepseek_v3",
76
+ "DefaultRenderer": "renderers.default",
77
+ "GLM45Renderer": "renderers.glm45",
78
+ "GLM51Renderer": "renderers.glm5",
79
+ "GLM5Renderer": "renderers.glm5",
80
+ "GptOssRenderer": "renderers.gpt_oss",
81
+ "KimiK25Renderer": "renderers.kimi_k25",
82
+ "KimiK2Renderer": "renderers.kimi_k2",
83
+ "LagunaXS2Renderer": "renderers.laguna_xs2",
84
+ "MiniMaxM2Renderer": "renderers.minimax_m2",
85
+ "Nemotron3Renderer": "renderers.nemotron3",
86
+ "Qwen35Renderer": "renderers.qwen35",
87
+ "Qwen36Renderer": "renderers.qwen36",
88
+ "Qwen3Renderer": "renderers.qwen3",
89
+ "Qwen3VLRenderer": "renderers.qwen3_vl",
90
+ }
91
+
92
+
93
+ def __getattr__(name: str):
94
+ if name in _LAZY_RENDERERS:
95
+ import importlib
96
+
97
+ module = importlib.import_module(_LAZY_RENDERERS[name])
98
+ value = getattr(module, name)
99
+ globals()[name] = value # cache for subsequent lookups
100
+ return value
101
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
102
+
103
+
104
+ def __dir__() -> list[str]:
105
+ return sorted(set(globals().keys()) | set(_LAZY_RENDERERS))
106
+
76
107
 
77
108
  __all__ = [
78
109
  "AutoRendererConfig",
@@ -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.dev27'
22
- __version_tuple__ = version_tuple = (0, 1, 8, 'dev27')
21
+ __version__ = version = '0.1.8.dev29'
22
+ __version_tuple__ = version_tuple = (0, 1, 8, 'dev29')
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -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:
@@ -137,3 +137,75 @@ def test_string_arg_preserves_type(model, renderer_name, renderer, args):
137
137
  assert got == args, (
138
138
  f"{model}: tool-arg type drift — sent {args!r}, parser returned {got!r}"
139
139
  )
140
+
141
+
142
+ # Schemas where ``string`` is one branch of a union (``anyOf`` / ``oneOf``).
143
+ # These are common in practice — e.g. ``form_input.value: str | bool`` in
144
+ # Pydantic serialises to ``{"anyOf": [{"type": "string"}, {"type": "boolean"}]}``.
145
+ # Without the union-aware check, the XML parser's ``json.loads`` falls back
146
+ # to raw text for bare strings, but flags the call ``INVALID_JSON`` because
147
+ # no top-level ``type`` key declared a string — silently dropping otherwise
148
+ # valid tool calls in the renderer client.
149
+ UNION_WITH_STRING_SCHEMAS = [
150
+ pytest.param(
151
+ {"anyOf": [{"type": "string"}, {"type": "boolean"}]},
152
+ id="anyOf-string-boolean",
153
+ ),
154
+ pytest.param(
155
+ {"anyOf": [{"type": "string"}, {"type": "null"}]},
156
+ id="anyOf-string-null",
157
+ ),
158
+ pytest.param(
159
+ {"oneOf": [{"type": "string"}, {"type": "integer"}]},
160
+ id="oneOf-string-integer",
161
+ ),
162
+ ]
163
+
164
+
165
+ @pytest.mark.parametrize("param_schema", UNION_WITH_STRING_SCHEMAS)
166
+ def test_union_with_string_emits_ok_status(
167
+ model, renderer_name, renderer, param_schema
168
+ ):
169
+ """Union schemas containing ``string`` must yield ``status=OK`` when
170
+ the model emits a bare string. Pre-fix, ``_coerce_arg_value`` flagged
171
+ this as ``INVALID_JSON`` because the top-level ``type`` key was
172
+ absent (the string branch was under ``anyOf`` / ``oneOf``)."""
173
+ from renderers.base import ToolCallParseStatus
174
+
175
+ tools = [
176
+ {
177
+ "type": "function",
178
+ "function": {
179
+ "name": "f",
180
+ "description": "Test tool with one union-typed parameter.",
181
+ "parameters": {
182
+ "type": "object",
183
+ "properties": {"x": param_schema},
184
+ "required": ["x"],
185
+ },
186
+ },
187
+ }
188
+ ]
189
+ args = {"x": "abc"}
190
+ msg = {
191
+ "role": "assistant",
192
+ "content": "",
193
+ "tool_calls": [
194
+ {
195
+ "id": "functions.f:0",
196
+ "function": {"name": "f", "arguments": args},
197
+ }
198
+ ],
199
+ }
200
+ completion_ids = _extract_assistant_tokens(renderer, PROMPT, msg, tools=tools)
201
+ parsed = renderer.parse_response(completion_ids, tools=tools)
202
+
203
+ assert parsed.tool_calls, f"{model}: parser returned no tool_calls"
204
+ tc = parsed.tool_calls[0]
205
+ assert tc.status == ToolCallParseStatus.OK, (
206
+ f"{model}: union-with-string schema flagged {tc.status} on bare string"
207
+ )
208
+ got = _normalize_args(tc.arguments)
209
+ assert got == args, (
210
+ f"{model}: tool-arg drift — sent {args!r}, parser returned {got!r}"
211
+ )
File without changes
File without changes