renderers 0.1.11.dev1__tar.gz → 0.1.11.dev2__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 (85) hide show
  1. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/PKG-INFO +29 -17
  2. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/README.md +23 -15
  3. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/docs/renderer-config.md +2 -7
  4. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/pyproject.toml +21 -5
  5. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/renderers/__init__.py +11 -15
  6. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/renderers/_version.py +2 -2
  7. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/renderers/base.py +83 -181
  8. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/renderers/client.py +10 -36
  9. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/renderers/deepseek_v3.py +2 -3
  10. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/renderers/default.py +2 -3
  11. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/renderers/gemma4.py +5 -6
  12. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/renderers/glm45.py +2 -3
  13. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/renderers/glm5.py +2 -3
  14. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/renderers/gpt_oss.py +2 -3
  15. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/renderers/hy3.py +2 -3
  16. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/renderers/inkling.py +5 -6
  17. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/renderers/kimi_k2.py +2 -3
  18. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/renderers/kimi_k25.py +9 -9
  19. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/renderers/laguna_s21.py +2 -3
  20. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/renderers/laguna_xs2.py +4 -5
  21. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/renderers/llama_3.py +2 -3
  22. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/renderers/minimax_m2.py +2 -3
  23. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/renderers/nemotron3.py +2 -3
  24. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/renderers/prime_qwen3.py +3 -4
  25. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/renderers/qwen3.py +2 -3
  26. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/renderers/qwen35.py +5 -6
  27. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/renderers/qwen3_vl.py +5 -6
  28. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/tests/test_renderer_config.py +0 -26
  29. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/uv.lock +14 -1
  30. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/.github/workflows/publish-dev.yml +0 -0
  31. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/.github/workflows/publish.yml +0 -0
  32. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/.github/workflows/style.yml +0 -0
  33. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/.github/workflows/test.yml +0 -0
  34. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/.gitignore +0 -0
  35. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/.pre-commit-config.yaml +0 -0
  36. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/LICENSE +0 -0
  37. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/examples/README.md +0 -0
  38. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/examples/sglang/multiturn_generate_sglang.py +0 -0
  39. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/examples/sglang/online_multiturn_sglang.py +0 -0
  40. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/examples/tinker/multiturn_generate_tinker.py +0 -0
  41. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/examples/transformers/multiturn_generate_transformers.py +0 -0
  42. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/examples/vllm/multiturn_generate_vllm.py +0 -0
  43. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/renderers/configs.py +0 -0
  44. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/renderers/deepseek_r1.py +0 -0
  45. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/renderers/parsers.py +0 -0
  46. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/renderers/parsing.py +0 -0
  47. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/renderers/qwen36.py +0 -0
  48. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/renderers/qwen38.py +0 -0
  49. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/tests/conftest.py +0 -0
  50. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/tests/test_bridge.py +0 -0
  51. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/tests/test_build_helpers.py +0 -0
  52. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/tests/test_client.py +0 -0
  53. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/tests/test_deepseek_r1.py +0 -0
  54. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/tests/test_disabled_thinking_stability.py +0 -0
  55. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/tests/test_gemma4.py +0 -0
  56. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/tests/test_glm_tool_name_validation.py +0 -0
  57. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/tests/test_gpt_oss_harmony_parity.py +0 -0
  58. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/tests/test_hy3.py +0 -0
  59. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/tests/test_incremental.py +0 -0
  60. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/tests/test_inkling.py +0 -0
  61. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/tests/test_is_content.py +0 -0
  62. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/tests/test_kimi_k25_tool_schema.py +0 -0
  63. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/tests/test_laguna_m1.py +0 -0
  64. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/tests/test_laguna_s21.py +0 -0
  65. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/tests/test_laguna_xs21.py +0 -0
  66. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/tests/test_llama_3.py +0 -0
  67. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/tests/test_load_tokenizer.py +0 -0
  68. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/tests/test_message_indices.py +0 -0
  69. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/tests/test_message_tool_names.py +0 -0
  70. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/tests/test_multimodal.py +0 -0
  71. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/tests/test_nemotron3_parity.py +0 -0
  72. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/tests/test_nemotron3_ultra.py +0 -0
  73. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/tests/test_parse_response.py +0 -0
  74. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/tests/test_parse_response_robustness.py +0 -0
  75. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/tests/test_parsers.py +0 -0
  76. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/tests/test_preserve_thinking.py +0 -0
  77. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/tests/test_prime_qwen3_parity.py +0 -0
  78. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/tests/test_qwen35_size_coverage.py +0 -0
  79. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/tests/test_qwen38.py +0 -0
  80. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/tests/test_render_ids.py +0 -0
  81. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/tests/test_renderer_config_parity.py +0 -0
  82. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/tests/test_roundtrip.py +0 -0
  83. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/tests/test_sampled_mask.py +0 -0
  84. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/tests/test_tokens_per_message.py +0 -0
  85. {renderers-0.1.11.dev1 → renderers-0.1.11.dev2}/tests/test_tool_arg_type_preservation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: renderers
3
- Version: 0.1.11.dev1
3
+ Version: 0.1.11.dev2
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
@@ -11,7 +11,11 @@ Requires-Dist: openai-harmony>=0.0.4
11
11
  Requires-Dist: openai>=1.108.1
12
12
  Requires-Dist: prime-pydantic-config>=0.3.0.dev83
13
13
  Requires-Dist: tiktoken
14
- Requires-Dist: transformers>=4.50.0
14
+ Provides-Extra: multimodal
15
+ Requires-Dist: pillow>=12.2.0; extra == 'multimodal'
16
+ Requires-Dist: transformers>=4.50.0; extra == 'multimodal'
17
+ Provides-Extra: transformers
18
+ Requires-Dist: transformers>=4.50.0; extra == 'transformers'
15
19
  Description-Content-Type: text/markdown
16
20
 
17
21
  # renderers
@@ -26,13 +30,28 @@ Standalone on PyPI, and portable across training and inference stacks (transform
26
30
  uv add renderers
27
31
  ```
28
32
 
33
+ The base install supports text renderers with a bring-your-own tokenizer. Add
34
+ the Hugging Face integration for the tokenizer-loading helpers, or the complete
35
+ media stack for image/audio rendering:
36
+
37
+ ```bash
38
+ uv add 'renderers[transformers]'
39
+ uv add 'renderers[multimodal]'
40
+ ```
41
+
42
+ A BYO tokenizer must expose `encode`, `decode`, `convert_tokens_to_ids`, token
43
+ IDs such as `eos_token_id`, and `return_offsets_mapping=True` through its call
44
+ interface. `DefaultRenderer` additionally requires `apply_chat_template`.
45
+ This includes text-only Inkling training: `InklingRenderer` loads its
46
+ Transformers processor only when image or audio content is actually rendered.
47
+
29
48
  ## At a glance
30
49
 
31
50
  ```python
32
- from transformers import AutoTokenizer
33
51
  from renderers import create_renderer
52
+ from renderers.base import load_tokenizer
34
53
 
35
- tok = AutoTokenizer.from_pretrained("Qwen/Qwen3-8B")
54
+ tok = load_tokenizer("Qwen/Qwen3-8B") # renderers[transformers]
36
55
  r = create_renderer(tok) # → Qwen3Renderer (auto-resolved)
37
56
 
38
57
  prompt_ids = r.render_ids(
@@ -92,17 +111,10 @@ r = create_renderer(tok) # AutoRendererConfig is the implicit def
92
111
 
93
112
  Auto-detect matches `tokenizer.name_or_path` against `MODEL_RENDERER_MAP` by **exact match**. Prefix matching is intentionally off — same architecture can ship different chat templates (base vs instruct, fine-tune renames). Fine-tunes must pass an explicit typed config (e.g. `Qwen3RendererConfig()`). Unknown text-only names fall back to `DefaultRenderer`, unless `AutoRendererConfig(thinking_retention=...)` was set; the default renderer cannot implement that bridge policy.
94
113
 
95
- ### Pools
96
-
97
- ```python
98
- from renderers import create_renderer_pool
99
-
100
- pool = create_renderer_pool("Qwen/Qwen3-8B", size=16)
101
- with pool.checkout() as r:
102
- ids = r.render_ids(messages)
103
- ```
104
-
105
- Each slot owns its own tokenizer copy. Construction fans out across a thread pool so a 32-slot pool doesn't serially eat ~10–15s of `from_pretrained` calls at startup.
114
+ Without the `transformers` extra, exact-match registered models still
115
+ auto-resolve. For an unknown name, renderers cannot safely probe `AutoConfig`
116
+ to distinguish a text model from an unknown VLM; pass an explicit typed config
117
+ such as `DefaultRendererConfig()` for a known text-only model.
106
118
 
107
119
  ## Why use a renderer
108
120
 
@@ -125,7 +137,7 @@ Each break fragments a rollout into multiple training samples — every fragment
125
137
 
126
138
  ## Typed renderer configs
127
139
 
128
- Each renderer accepts a typed pydantic config at construction. Some fields mirror chat-template kwargs; others configure renderer-only behavior such as image caching, parsers, or Harmony preamble construction. `create_renderer` and `create_renderer_pool` take one positional `config` argument and an optional keyword-only `chat_template_kwargs` mapping:
140
+ Each renderer accepts a typed pydantic config at construction. Some fields mirror chat-template kwargs; others configure renderer-only behavior such as image caching, parsers, or Harmony preamble construction. `create_renderer` takes one positional `config` argument and an optional keyword-only `chat_template_kwargs` mapping:
129
141
 
130
142
  ```python
131
143
  from renderers import (
@@ -175,7 +187,7 @@ Fallback for unsupported text-only models. Wraps `apply_chat_template` and accep
175
187
 
176
188
  ## Roadmap
177
189
 
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`.
190
+ - **VLM expansion.** `ImagePart` support exists for Qwen3-VL, Qwen3.5-family, Gemma 4, and Kimi K2.5 / K2.6 multimodal templates. Install `renderers[multimodal]` for Pillow and the Hugging Face processors. Remaining work: audio/video support, broader VLM coverage, and more RL validation. Gemma 4 image preprocessing requires a Transformers release that provides `Gemma4Processor`.
179
191
  - **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
192
 
181
193
  ## Testing
@@ -10,13 +10,28 @@ Standalone on PyPI, and portable across training and inference stacks (transform
10
10
  uv add renderers
11
11
  ```
12
12
 
13
+ The base install supports text renderers with a bring-your-own tokenizer. Add
14
+ the Hugging Face integration for the tokenizer-loading helpers, or the complete
15
+ media stack for image/audio rendering:
16
+
17
+ ```bash
18
+ uv add 'renderers[transformers]'
19
+ uv add 'renderers[multimodal]'
20
+ ```
21
+
22
+ A BYO tokenizer must expose `encode`, `decode`, `convert_tokens_to_ids`, token
23
+ IDs such as `eos_token_id`, and `return_offsets_mapping=True` through its call
24
+ interface. `DefaultRenderer` additionally requires `apply_chat_template`.
25
+ This includes text-only Inkling training: `InklingRenderer` loads its
26
+ Transformers processor only when image or audio content is actually rendered.
27
+
13
28
  ## At a glance
14
29
 
15
30
  ```python
16
- from transformers import AutoTokenizer
17
31
  from renderers import create_renderer
32
+ from renderers.base import load_tokenizer
18
33
 
19
- tok = AutoTokenizer.from_pretrained("Qwen/Qwen3-8B")
34
+ tok = load_tokenizer("Qwen/Qwen3-8B") # renderers[transformers]
20
35
  r = create_renderer(tok) # → Qwen3Renderer (auto-resolved)
21
36
 
22
37
  prompt_ids = r.render_ids(
@@ -76,17 +91,10 @@ r = create_renderer(tok) # AutoRendererConfig is the implicit def
76
91
 
77
92
  Auto-detect matches `tokenizer.name_or_path` against `MODEL_RENDERER_MAP` by **exact match**. Prefix matching is intentionally off — same architecture can ship different chat templates (base vs instruct, fine-tune renames). Fine-tunes must pass an explicit typed config (e.g. `Qwen3RendererConfig()`). Unknown text-only names fall back to `DefaultRenderer`, unless `AutoRendererConfig(thinking_retention=...)` was set; the default renderer cannot implement that bridge policy.
78
93
 
79
- ### Pools
80
-
81
- ```python
82
- from renderers import create_renderer_pool
83
-
84
- pool = create_renderer_pool("Qwen/Qwen3-8B", size=16)
85
- with pool.checkout() as r:
86
- ids = r.render_ids(messages)
87
- ```
88
-
89
- Each slot owns its own tokenizer copy. Construction fans out across a thread pool so a 32-slot pool doesn't serially eat ~10–15s of `from_pretrained` calls at startup.
94
+ Without the `transformers` extra, exact-match registered models still
95
+ auto-resolve. For an unknown name, renderers cannot safely probe `AutoConfig`
96
+ to distinguish a text model from an unknown VLM; pass an explicit typed config
97
+ such as `DefaultRendererConfig()` for a known text-only model.
90
98
 
91
99
  ## Why use a renderer
92
100
 
@@ -109,7 +117,7 @@ Each break fragments a rollout into multiple training samples — every fragment
109
117
 
110
118
  ## Typed renderer configs
111
119
 
112
- Each renderer accepts a typed pydantic config at construction. Some fields mirror chat-template kwargs; others configure renderer-only behavior such as image caching, parsers, or Harmony preamble construction. `create_renderer` and `create_renderer_pool` take one positional `config` argument and an optional keyword-only `chat_template_kwargs` mapping:
120
+ Each renderer accepts a typed pydantic config at construction. Some fields mirror chat-template kwargs; others configure renderer-only behavior such as image caching, parsers, or Harmony preamble construction. `create_renderer` takes one positional `config` argument and an optional keyword-only `chat_template_kwargs` mapping:
113
121
 
114
122
  ```python
115
123
  from renderers import (
@@ -159,7 +167,7 @@ Fallback for unsupported text-only models. Wraps `apply_chat_template` and accep
159
167
 
160
168
  ## Roadmap
161
169
 
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`.
170
+ - **VLM expansion.** `ImagePart` support exists for Qwen3-VL, Qwen3.5-family, Gemma 4, and Kimi K2.5 / K2.6 multimodal templates. Install `renderers[multimodal]` for Pillow and the Hugging Face processors. Remaining work: audio/video support, broader VLM coverage, and more RL validation. Gemma 4 image preprocessing requires a Transformers release that provides `Gemma4Processor`.
163
171
  - **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
172
 
165
173
  ## Testing
@@ -1,8 +1,7 @@
1
1
  # Renderer config
2
2
 
3
- `renderers.RendererConfig` is the typed input to `create_renderer` and
4
- `create_renderer_pool`. It pins the renderer choice and its config at
5
- construction time.
3
+ `renderers.RendererConfig` is the typed input to `create_renderer`. It pins the
4
+ renderer choice and its config at construction time.
6
5
 
7
6
  ```python
8
7
  from renderers import create_renderer, Qwen35RendererConfig
@@ -77,10 +76,6 @@ r = create_renderer(
77
76
  tokenizer,
78
77
  chat_template_kwargs={"enable_thinking": False},
79
78
  )
80
- pool = create_renderer_pool(
81
- "Qwen/Qwen3-8B",
82
- chat_template_kwargs={"enable_thinking": False},
83
- )
84
79
  ```
85
80
 
86
81
  Renderers resolves auto configs before applying `chat_template_kwargs`, so the
@@ -19,11 +19,6 @@ dependencies = [
19
19
  "openai>=1.108.1",
20
20
  "tiktoken",
21
21
  "jinja2",
22
- # Keep this floor compatible with prime-rl's transformers pin. Inkling's
23
- # tokenizer and text-only renderer work on older releases; image/audio
24
- # processing fails lazily with an upgrade message when InklingProcessor is
25
- # unavailable (native support starts in transformers 5.14).
26
- "transformers>=4.50.0",
27
22
  # Used by GptOssRenderer to render and parse harmony tokens. Vendoring
28
23
  # OpenAI's reference implementation keeps us byte-identical with vLLM
29
24
  # (which also uses it) and saves us mirroring a 330-line Jinja template.
@@ -42,6 +37,25 @@ dependencies = [
42
37
  "prime-pydantic-config>=0.3.0.dev83",
43
38
  ]
44
39
 
40
+ [project.optional-dependencies]
41
+ # Tokenizer loading uses Hugging Face. Text-only renderers can instead be
42
+ # constructed with an offset-capable BYO tokenizer and do not import this
43
+ # dependency.
44
+ transformers = [
45
+ # Keep this floor compatible with prime-rl's transformers pin. Inkling's
46
+ # tokenizer and text-only renderer work on older releases; image/audio
47
+ # processing fails lazily with an upgrade message when InklingProcessor is
48
+ # unavailable (native support starts in transformers 5.14).
49
+ "transformers>=4.50.0",
50
+ ]
51
+
52
+ # Image/audio renderers also need Pillow to resolve media inputs. Keep this as
53
+ # a separate convenience extra so tokenizer-only users do not pull it in.
54
+ multimodal = [
55
+ "pillow>=12.2.0",
56
+ "transformers>=4.50.0",
57
+ ]
58
+
45
59
  [tool.hatch.version]
46
60
  source = "vcs"
47
61
  # Tags look like ``renderers-v0.1.8`` (prefix matches the publish.yml
@@ -90,6 +104,8 @@ dev = [
90
104
  "torch>=2.11.0",
91
105
  "torchvision>=0.26.0",
92
106
  "ty>=0.0.1a29,<0.0.22",
107
+ # Optional for consumers, but required by tokenizer/parity/VLM tests.
108
+ "transformers>=4.50.0",
93
109
  ]
94
110
 
95
111
  [tool.uv]
@@ -7,6 +7,7 @@ except ImportError:
7
7
  __version__ = "0+unknown"
8
8
 
9
9
  from renderers.base import (
10
+ ChatTemplateTokenizer,
10
11
  Content,
11
12
  ContentPart,
12
13
  ImagePart,
@@ -21,9 +22,9 @@ from renderers.base import (
21
22
  RenderedTokens,
22
23
  RenderedTrainingSample,
23
24
  Renderer,
24
- RendererPool,
25
25
  TextPart,
26
26
  ThinkingPart,
27
+ Tokenizer,
27
28
  ToolCall,
28
29
  ToolCallFunction,
29
30
  ToolCallParseStatus,
@@ -33,7 +34,6 @@ from renderers.base import (
33
34
  build_training_sample,
34
35
  build_trajectory_step,
35
36
  create_renderer,
36
- create_renderer_pool,
37
37
  extract_message_tool_names,
38
38
  is_multimodal,
39
39
  reject_assistant_in_extension,
@@ -74,17 +74,13 @@ from renderers.configs import (
74
74
  RendererConfig,
75
75
  )
76
76
 
77
- # Concrete renderer classes are lazy-loaded so that consumers needing
78
- # only the config layer (``RendererConfig`` discriminated union) don't
79
- # pay the ``transformers`` import cost. Each renderer module does
80
- # ``from transformers.tokenization_utils import PreTrainedTokenizer``
81
- # at module level, so eager imports here would drag ``transformers``
82
- # into every downstream ``import renderers``. ``__getattr__`` (PEP 562)
83
- # resolves the names on first attribute access, so ``from renderers
84
- # import DefaultRenderer`` and ``renderers.DefaultRenderer`` both work
85
- # transparently. ``create_renderer`` doesn't depend on these eager
86
- # imports — ``renderers.base._populate_registry`` lazy-imports the
87
- # concrete classes itself when a renderer is instantiated.
77
+ # Concrete renderer classes are lazy-loaded so that consumers needing only the
78
+ # config layer (``RendererConfig`` discriminated union) don't import every
79
+ # renderer module. Renderer tokenizer annotations use the local ``Tokenizer``
80
+ # protocols, so resolving a text renderer remains safe when the optional
81
+ # ``transformers`` dependency is absent. ``__getattr__`` (PEP 562) resolves the
82
+ # names on first attribute access, while ``renderers.base._populate_registry``
83
+ # handles lazy registration for ``create_renderer``.
88
84
  _LAZY_RENDERERS: dict[str, str] = {
89
85
  "DeepSeekR1Renderer": "renderers.deepseek_r1",
90
86
  "DeepSeekV3Renderer": "renderers.deepseek_v3",
@@ -134,6 +130,7 @@ def __dir__() -> list[str]:
134
130
  __all__ = [
135
131
  "AutoRendererConfig",
136
132
  "BaseRendererConfig",
133
+ "ChatTemplateTokenizer",
137
134
  "Content",
138
135
  "ContentPart",
139
136
  "DeepSeekR1Renderer",
@@ -205,9 +202,9 @@ __all__ = [
205
202
  "RenderedTrainingSample",
206
203
  "Renderer",
207
204
  "RendererConfig",
208
- "RendererPool",
209
205
  "TextPart",
210
206
  "ThinkingPart",
207
+ "Tokenizer",
211
208
  "ToolCall",
212
209
  "ToolCallFunction",
213
210
  "ToolCallParseStatus",
@@ -219,7 +216,6 @@ __all__ = [
219
216
  "build_trajectory_step",
220
217
  "config_from_name",
221
218
  "create_renderer",
222
- "create_renderer_pool",
223
219
  "extract_message_tool_names",
224
220
  "is_multimodal",
225
221
  "reject_assistant_in_extension",
@@ -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.11.dev1'
22
- __version_tuple__ = version_tuple = (0, 1, 11, 'dev1')
21
+ __version__ = version = '0.1.11.dev2'
22
+ __version_tuple__ = version_tuple = (0, 1, 11, 'dev2')
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -2,10 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import enum
4
4
  import logging
5
- import queue
6
- import threading
7
5
  from collections.abc import Mapping
8
- from contextlib import contextmanager
9
6
  from dataclasses import dataclass, field
10
7
  from typing import (
11
8
  TYPE_CHECKING,
@@ -663,6 +660,37 @@ class RenderedConversation:
663
660
  )
664
661
 
665
662
 
663
+ @runtime_checkable
664
+ class Tokenizer(Protocol):
665
+ """Structural tokenizer surface used by hand-coded renderers.
666
+
667
+ Hugging Face tokenizers satisfy this protocol, as can lightweight BYO
668
+ adapters around ``tokenizers.Tokenizer`` or another tokenizer backend.
669
+ Keeping the renderer-facing contract here makes ``transformers`` optional
670
+ for text rendering. Offset-capable ``__call__`` behavior is required by
671
+ :func:`attribute_text_segments` to preserve BPE boundary attribution.
672
+ """
673
+
674
+ name_or_path: str
675
+ unk_token_id: int | None
676
+ eos_token_id: int | None
677
+
678
+ def encode(self, text: str, *args: Any, **kwargs: Any) -> list[int]: ...
679
+
680
+ def decode(self, token_ids: Any, *args: Any, **kwargs: Any) -> str: ...
681
+
682
+ def convert_tokens_to_ids(self, tokens: Any) -> Any: ...
683
+
684
+ def __call__(self, *args: Any, **kwargs: Any) -> Any: ...
685
+
686
+
687
+ @runtime_checkable
688
+ class ChatTemplateTokenizer(Tokenizer, Protocol):
689
+ """Tokenizer surface required by :class:`DefaultRenderer`."""
690
+
691
+ def apply_chat_template(self, *args: Any, **kwargs: Any) -> Any: ...
692
+
693
+
666
694
  @runtime_checkable
667
695
  class Renderer(Protocol):
668
696
  """Owns message ↔ token conversion for a specific model family."""
@@ -679,10 +707,9 @@ class Renderer(Protocol):
679
707
  Behaviour around historical ``reasoning_content`` is owned by the
680
708
  renderer instance — the ``thinking_retention`` level is resolved at
681
709
  construction, not passed per call. To render with a different
682
- configuration, build a different renderer (or different pool). When
683
- ``thinking_retention`` is left unset, full renders follow the model's
684
- chat template and bridge policy is derived from that template's own
685
- history-retention knobs.
710
+ configuration, build a different renderer. When ``thinking_retention``
711
+ is left unset, full renders follow the model's chat template and bridge
712
+ policy is derived from that template's own history-retention knobs.
686
713
  """
687
714
  ...
688
715
 
@@ -839,9 +866,7 @@ class MultimodalRenderer(Renderer, Protocol):
839
866
  # Per-type cache for ``is_multimodal``. The ``runtime_checkable`` Protocol
840
867
  # isinstance check walks every protocol member via ``hasattr`` on each
841
868
  # call; per-type caching collapses that to a single dict lookup on the
842
- # hot path (e.g. per-bridge dispatch). Pools expose ``is_multimodal``
843
- # directly as a snapshot attribute (different pools share a class but
844
- # wrap different renderer types), so we don't need to special-case them.
869
+ # hot path (e.g. per-bridge dispatch).
845
870
  _IS_MULTIMODAL_BY_TYPE: dict[type, bool] = {}
846
871
 
847
872
 
@@ -863,127 +888,6 @@ def is_multimodal(r: object) -> bool:
863
888
  return cached
864
889
 
865
890
 
866
- class RendererPool:
867
- """Pool of Renderer instances that itself satisfies the Renderer protocol.
868
-
869
- Callers treat a pool like a single renderer — ``pool.render_ids(...)``,
870
- ``pool.bridge_to_next_turn(...)``, ``isinstance(pool, MultimodalRenderer)``
871
- all work via structural delegation. The pool internally serializes
872
- access to its inner renderers (each wraps its own tokenizer copy).
873
-
874
- Concurrency model:
875
- - ``size == 1``: a single inner renderer guarded by a ``threading.Lock``.
876
- Avoids the queue's per-call overhead on the common default config.
877
- - ``size > 1``: a ``queue.Queue`` of independent renderers, checked out
878
- one at a time. HuggingFace fast tokenizers release the GIL during
879
- Rust encoding, so threads achieve real parallelism.
880
-
881
- Construction parallelism for ``size > 1``: ``AutoTokenizer.from_pretrained``
882
- takes hundreds of ms per call (JSON parse + Rust tokenizer build + HF
883
- cache lookup), so populating a 32-slot pool serially costs ~10-15s on
884
- startup and shows up directly as a step-0 stall. We fan the factory out
885
- across a short-lived thread pool; the GIL-bound Python portion stops
886
- scaling past ~8 workers, so we clamp there.
887
- """
888
-
889
- def __init__(self, factory: Callable[[], Renderer], size: int):
890
- from concurrent.futures import ThreadPoolExecutor
891
-
892
- self._factory = factory
893
- self._size = size
894
-
895
- if size == 1:
896
- renderer = factory()
897
- self._sole: Renderer | None = renderer
898
- self._lock: threading.Lock | None = threading.Lock()
899
- self._pool: queue.Queue[Renderer] | None = None
900
- sample: Renderer = renderer
901
- else:
902
- self._sole = None
903
- self._lock = None
904
- self._pool = queue.Queue(maxsize=size)
905
- workers = min(size, 8)
906
- with ThreadPoolExecutor(max_workers=workers) as executor:
907
- for renderer in executor.map(lambda _: factory(), range(size)):
908
- self._pool.put(renderer)
909
- # Peek without removing — safe at construction time before any
910
- # checkout has been served.
911
- sample = self._pool.queue[0]
912
-
913
- # Snapshot the protocol-shaped attributes from a sample renderer.
914
- # They are constant per renderer class, so resolving them once at
915
- # construction (a) eliminates per-call ``getattr``/``isinstance``
916
- # overhead and (b) lets a future out-of-process pool variant skip
917
- # holding a live tokenizer in the parent process.
918
- self._renderer_cls: type[Renderer] = type(sample)
919
- self.supports_tools: bool = getattr(sample, "supports_tools", True)
920
- self.is_multimodal: bool = is_multimodal(sample)
921
- # ``mm_token_type_id_map`` is set ONLY on pools wrapping a
922
- # ``MultimodalRenderer``. We deliberately don't expose this as a
923
- # class-level property: ``runtime_checkable`` Protocol's
924
- # isinstance check uses ``inspect.getattr_static``, which finds
925
- # property descriptors on the class regardless of whether their
926
- # fget raises. Conditional instance attributes (present in
927
- # ``self.__dict__`` only when applicable) are the only way to
928
- # make ``isinstance(pool, MultimodalRenderer)`` reflect the
929
- # inner renderer's actual protocol conformance.
930
- if isinstance(sample, MultimodalRenderer):
931
- self.mm_token_type_id_map: dict[int, int] = sample.mm_token_type_id_map
932
-
933
- @contextmanager
934
- def checkout(self):
935
- if self._sole is not None:
936
- assert self._lock is not None
937
- with self._lock:
938
- yield self._sole
939
- return
940
- assert self._pool is not None
941
- renderer = self._pool.get()
942
- try:
943
- yield renderer
944
- finally:
945
- self._pool.put(renderer)
946
-
947
- @property
948
- def size(self) -> int:
949
- return self._size
950
-
951
- @property
952
- def renderer_cls(self) -> type[Renderer]:
953
- """Class of the renderers in this pool (uniform across all slots)."""
954
- return self._renderer_cls
955
-
956
- # ── Renderer protocol delegation ────────────────────────────────────
957
- # Pool structurally satisfies ``Renderer`` (and ``MultimodalRenderer``
958
- # when its slots wrap multimodal renderers). Callers can call methods
959
- # directly and dispatch with ``isinstance(pool, MultimodalRenderer)``
960
- # without reaching into ``checkout()``.
961
-
962
- def render(self, *args: Any, **kwargs: Any) -> "RenderedTokens":
963
- with self.checkout() as r:
964
- return r.render(*args, **kwargs)
965
-
966
- def render_ids(self, *args: Any, **kwargs: Any) -> list[int]:
967
- with self.checkout() as r:
968
- return r.render_ids(*args, **kwargs)
969
-
970
- def parse_response(self, *args: Any, **kwargs: Any) -> "ParsedResponse":
971
- with self.checkout() as r:
972
- return r.parse_response(*args, **kwargs)
973
-
974
- def get_stop_token_ids(self) -> list[int]:
975
- with self.checkout() as r:
976
- return r.get_stop_token_ids()
977
-
978
- def bridge_to_next_turn(self, *args: Any, **kwargs: Any) -> "RenderedTokens | None":
979
- with self.checkout() as r:
980
- return r.bridge_to_next_turn(*args, **kwargs)
981
-
982
- # ``mm_token_type_id_map`` (the MultimodalRenderer protocol attribute)
983
- # is set in ``__init__`` only for pools wrapping multimodal renderers;
984
- # see the comment there for why this isn't a class-level property.
985
-
986
-
987
891
  RENDERER_REGISTRY: dict[str, type] = {}
988
892
 
989
893
  # Exact canonical HF model names → renderer. We do NOT use prefix
@@ -1156,6 +1060,25 @@ MULTIMODAL_MODELS: dict[str, set[str]] = {
1156
1060
  }
1157
1061
 
1158
1062
 
1063
+ _TRANSFORMERS_INSTALL_HINT = (
1064
+ "Install the optional dependency with "
1065
+ "`pip install 'renderers[transformers]'` (or "
1066
+ "`uv add 'renderers[transformers]'`). Text-only renderers work without "
1067
+ "it when constructed with an offset-capable tokenizer object."
1068
+ )
1069
+
1070
+
1071
+ def _require_transformers(feature: str) -> Any:
1072
+ """Return ``transformers`` or raise an actionable optional-extra error."""
1073
+ try:
1074
+ import transformers
1075
+ except ImportError as exc:
1076
+ raise ImportError(
1077
+ f"{feature} requires Transformers. {_TRANSFORMERS_INSTALL_HINT}"
1078
+ ) from exc
1079
+ return transformers
1080
+
1081
+
1159
1082
  def _model_has_vision_config(model_name: str) -> bool:
1160
1083
  """Return True if the HF config for ``model_name`` declares vision inputs.
1161
1084
 
@@ -1166,13 +1089,26 @@ def _model_has_vision_config(model_name: str) -> bool:
1166
1089
  match what the trainer reconstructs — a class of bug the renderer
1167
1090
  abstraction exists to prevent.
1168
1091
 
1169
- Returns False on any AutoConfig failure (offline, gated, missing) so
1170
- a flaky HF probe never blocks a legitimate text-only fine-tune.
1092
+ Returns False on remote/config failures so a flaky HF probe never blocks a
1093
+ legitimate text-only fine-tune. When Transformers itself is unavailable,
1094
+ however, auto-resolution cannot safely distinguish an unknown text model
1095
+ from an unknown VLM; callers must install the extra or choose an explicit
1096
+ renderer config.
1171
1097
  """
1172
1098
  try:
1173
- from transformers import AutoConfig
1174
-
1175
- cfg = AutoConfig.from_pretrained(model_name, trust_remote_code=False)
1099
+ transformers = _require_transformers("Auto-resolving an unregistered model")
1100
+ except ImportError as exc:
1101
+ raise ImportError(
1102
+ f"Cannot auto-resolve unregistered model {model_name!r} without "
1103
+ "checking whether it is multimodal. Install "
1104
+ "`renderers[transformers]`, or pass an explicit typed renderer "
1105
+ "config such as `DefaultRendererConfig()` for a known text-only "
1106
+ "model."
1107
+ ) from exc
1108
+ try:
1109
+ cfg = transformers.AutoConfig.from_pretrained(
1110
+ model_name, trust_remote_code=False
1111
+ )
1176
1112
  except Exception:
1177
1113
  return False
1178
1114
  # Most VLM configs nest a vision tower as ``vision_config`` (Qwen-VL,
@@ -1196,8 +1132,8 @@ def _model_has_vision_config(model_name: str) -> bool:
1196
1132
  # Pinning the revision keeps the trust narrow: even with
1197
1133
  # ``trust_remote_code=True``, transformers downloads / executes the
1198
1134
  # tokenizer Python from this exact commit only. A future malicious push
1199
- # to the Moonshot HF repo doesn't auto-propagate to anyone using
1200
- # ``create_renderer_pool``. Bump these SHAs deliberately, with review.
1135
+ # to the Moonshot HF repo doesn't auto-propagate to callers of
1136
+ # ``load_tokenizer``. Bump these SHAs deliberately, with review.
1201
1137
  TRUSTED_REVISIONS: dict[str, str] = {
1202
1138
  "moonshotai/Kimi-K2-Instruct": "fd1984e2b7a3350dbf7305fe73a4ede25c14de50",
1203
1139
  "moonshotai/Kimi-K2.5": "4d01dfe0332d63057c186e0b262165819efb6611",
@@ -1330,7 +1266,9 @@ def load_tokenizer(model_name_or_path: str):
1330
1266
  those exact IDs we load tokenizer files from the audited unrestricted
1331
1267
  ``unsloth`` mirrors instead, then restore ``tokenizer.name_or_path`` to
1332
1268
  the requested Meta ID so auto-resolution still selects ``Llama3Renderer``.
1269
+ Requires the ``renderers[transformers]`` extra.
1333
1270
  """
1271
+ _require_transformers("Loading a tokenizer")
1334
1272
  load_name_or_path = _tokenizer_source_for(model_name_or_path)
1335
1273
  kwargs = _tokenizer_load_kwargs(load_name_or_path)
1336
1274
  tok = _load_tokenizer_via_auto(load_name_or_path, **kwargs)
@@ -1408,44 +1346,6 @@ def _populate_registry():
1408
1346
  )
1409
1347
 
1410
1348
 
1411
- def create_renderer_pool(
1412
- tokenizer_name_or_path: str,
1413
- config: RendererConfig | None = None,
1414
- *,
1415
- size: int = 16,
1416
- chat_template_kwargs: Mapping[str, Any] | None = None,
1417
- ) -> RendererPool:
1418
- """Create a RendererPool with *size* independent tokenizer copies.
1419
-
1420
- Each slot loads its own tokenizer so threads never share mutable
1421
- state. HuggingFace fast tokenizers release the GIL during Rust
1422
- encoding, so threads achieve real parallelism.
1423
-
1424
- ``config`` is the typed renderer config (one of the variants of
1425
- :data:`renderers.RendererConfig`). Defaults to
1426
- :class:`AutoRendererConfig`, which resolves to a concrete renderer
1427
- via ``MODEL_RENDERER_MAP`` at construction time using the loaded
1428
- tokenizer's name. ``chat_template_kwargs`` are merged into the
1429
- resolved concrete config and validated before renderer construction.
1430
- Every slot in the pool shares the same config; to run a different
1431
- config, build a different pool.
1432
-
1433
- Tokenizers load via ``load_tokenizer`` — see its docstring for the
1434
- ``trust_remote_code`` policy (default off; Moonshot Kimi-K2 family
1435
- opts in with a pinned ``revision``).
1436
- """
1437
-
1438
- def factory() -> Renderer:
1439
- tokenizer = load_tokenizer(tokenizer_name_or_path)
1440
- return create_renderer(
1441
- tokenizer,
1442
- config,
1443
- chat_template_kwargs=chat_template_kwargs,
1444
- )
1445
-
1446
- return RendererPool(factory, size=size)
1447
-
1448
-
1449
1349
  def create_renderer(
1450
1350
  tokenizer,
1451
1351
  config: RendererConfig | None = None,
@@ -1455,7 +1355,8 @@ def create_renderer(
1455
1355
  """Create a Renderer from a typed config.
1456
1356
 
1457
1357
  Args:
1458
- tokenizer: HuggingFace tokenizer instance.
1358
+ tokenizer: An object satisfying :class:`Tokenizer`; the generic
1359
+ fallback additionally requires :class:`ChatTemplateTokenizer`.
1459
1360
  config: Typed renderer config — one of the variants of
1460
1361
  :data:`renderers.RendererConfig`. ``None`` defaults to
1461
1362
  :class:`AutoRendererConfig`, which resolves to a concrete
@@ -1471,10 +1372,11 @@ def create_renderer(
1471
1372
  config from ``tokenizer.name_or_path`` and then validates these
1472
1373
  kwargs against that config.
1473
1374
 
1474
- Selecting the auto-renderer for a model without a registered
1475
- renderer falls back to :class:`DefaultRenderer` for text-only models
1476
- and raises for VLMs (where ``apply_chat_template`` would silently
1477
- drop images).
1375
+ Selecting the auto-renderer for a model without a registered renderer
1376
+ probes Hugging Face ``AutoConfig`` before falling back to
1377
+ :class:`DefaultRenderer`, so unknown VLMs fail instead of silently dropping
1378
+ media. Without the ``transformers`` extra, pass an explicit renderer config
1379
+ for unregistered model names.
1478
1380
  """
1479
1381
  _populate_registry()
1480
1382