renderers 0.1.11.dev4__tar.gz → 0.1.12.dev1__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.
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/PKG-INFO +2 -2
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/README.md +1 -1
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/docs/renderer-config.md +2 -2
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/pyproject.toml +1 -1
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/renderers/_version.py +2 -2
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/renderers/client.py +88 -12
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/renderers/kimi_k25.py +21 -5
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/renderers/qwen35.py +44 -26
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/renderers/qwen3_vl.py +45 -26
- renderers-0.1.12.dev1/tests/conftest.py +59 -0
- renderers-0.1.12.dev1/tests/parity.py +782 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/tests/reference_rendering.py +283 -29
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/tests/test_bridge.py +2 -24
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/tests/test_client.py +46 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/tests/test_deepseek_r1.py +3 -70
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/tests/test_disabled_thinking_stability.py +17 -33
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/tests/test_gemma4.py +2 -6
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/tests/test_glm_tool_name_validation.py +2 -4
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/tests/test_incremental.py +1 -1
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/tests/test_laguna_m1.py +0 -78
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/tests/test_llama_3.py +4 -147
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/tests/test_message_indices.py +1 -1
- renderers-0.1.12.dev1/tests/test_parity.py +222 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/tests/test_parse_response.py +64 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/tests/test_qwen35_size_coverage.py +4 -91
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/tests/test_roundtrip.py +12 -50
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/tests/test_tool_arg_type_preservation.py +2 -10
- renderers-0.1.11.dev4/tests/conftest.py +0 -171
- renderers-0.1.11.dev4/tests/test_gpt_oss_harmony_parity.py +0 -271
- renderers-0.1.11.dev4/tests/test_laguna_s21.py +0 -128
- renderers-0.1.11.dev4/tests/test_nemotron3_parity.py +0 -683
- renderers-0.1.11.dev4/tests/test_prime_qwen3_parity.py +0 -254
- renderers-0.1.11.dev4/tests/test_render_ids.py +0 -402
- renderers-0.1.11.dev4/tests/test_renderer_config_parity.py +0 -602
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/.github/workflows/publish-dev.yml +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/.github/workflows/publish.yml +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/.github/workflows/style.yml +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/.github/workflows/test.yml +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/.gitignore +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/.pre-commit-config.yaml +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/LICENSE +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/examples/README.md +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/examples/sglang/multiturn_generate_sglang.py +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/examples/sglang/online_multiturn_sglang.py +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/examples/tinker/multiturn_generate_tinker.py +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/examples/transformers/multiturn_generate_transformers.py +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/examples/vllm/multiturn_generate_vllm.py +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/renderers/__init__.py +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/renderers/base.py +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/renderers/configs.py +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/renderers/deepseek_r1.py +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/renderers/deepseek_v3.py +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/renderers/deepseek_v4.py +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/renderers/default.py +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/renderers/gemma4.py +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/renderers/glm45.py +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/renderers/glm5.py +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/renderers/gpt_oss.py +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/renderers/hy3.py +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/renderers/inkling.py +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/renderers/kimi_k2.py +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/renderers/laguna_s21.py +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/renderers/laguna_xs2.py +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/renderers/llama_3.py +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/renderers/minimax_m2.py +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/renderers/nemotron3.py +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/renderers/parsers.py +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/renderers/parsing.py +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/renderers/prime_qwen3.py +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/renderers/qwen3.py +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/renderers/qwen36.py +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/renderers/qwen38.py +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/tests/test_build_helpers.py +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/tests/test_deepseek_v4.py +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/tests/test_hy3.py +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/tests/test_inkling.py +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/tests/test_is_content.py +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/tests/test_kimi_k25_tool_schema.py +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/tests/test_laguna_xs21.py +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/tests/test_load_tokenizer.py +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/tests/test_message_tool_names.py +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/tests/test_multimodal.py +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/tests/test_nemotron3_ultra.py +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/tests/test_offsetless_tokenizers.py +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/tests/test_parse_response_robustness.py +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/tests/test_parsers.py +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/tests/test_preserve_thinking.py +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/tests/test_qwen38.py +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/tests/test_renderer_config.py +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/tests/test_sampled_mask.py +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/tests/test_tokens_per_message.py +0 -0
- {renderers-0.1.11.dev4 → renderers-0.1.12.dev1}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: renderers
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.12.dev1
|
|
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
|
|
@@ -199,7 +199,7 @@ uv sync --group dev
|
|
|
199
199
|
uv run pytest
|
|
200
200
|
```
|
|
201
201
|
|
|
202
|
-
Round-trip parity (render → parse → original) and token-level parity against
|
|
202
|
+
Round-trip parity (render → parse → original) and token-level parity against each model's independent reference encoder are tested per renderer. Most references use `apply_chat_template`; DeepSeek V4 uses its shipped Python encoder, and GPT-OSS uses Harmony.
|
|
203
203
|
|
|
204
204
|
## License
|
|
205
205
|
|
|
@@ -179,7 +179,7 @@ uv sync --group dev
|
|
|
179
179
|
uv run pytest
|
|
180
180
|
```
|
|
181
181
|
|
|
182
|
-
Round-trip parity (render → parse → original) and token-level parity against
|
|
182
|
+
Round-trip parity (render → parse → original) and token-level parity against each model's independent reference encoder are tested per renderer. Most references use `apply_chat_template`; DeepSeek V4 uses its shipped Python encoder, and GPT-OSS uses Harmony.
|
|
183
183
|
|
|
184
184
|
## License
|
|
185
185
|
|
|
@@ -21,8 +21,8 @@ construction.
|
|
|
21
21
|
Use `type(config).template_field_names()` to inspect the explicit allowlist of
|
|
22
22
|
fields accepted through `chat_template_kwargs`. Every renderer-specific field
|
|
23
23
|
is classified as either a template field or a renderer-only field at class
|
|
24
|
-
definition time. Template fields are covered by
|
|
25
|
-
|
|
24
|
+
definition time. Template fields are covered by the model/scenario/kwarg
|
|
25
|
+
product in `tests/test_parity.py`.
|
|
26
26
|
|
|
27
27
|
| Renderer | Config class | Template fields | Renderer-only fields |
|
|
28
28
|
| --- | --- | --- | --- |
|
|
@@ -27,7 +27,7 @@ dependencies = [
|
|
|
27
27
|
# release through 0.5.12.post1 hard-pins ``openai-harmony==0.0.4``, so a
|
|
28
28
|
# higher floor makes ``renderers`` uninstallable alongside SGLang. The
|
|
29
29
|
# GptOssRenderer renders byte-identically on 0.0.4 (verified token-for-token
|
|
30
|
-
# against 0.0.8) and ``tests/
|
|
30
|
+
# against 0.0.8) and the Harmony cells in ``tests/test_parity.py`` pass,
|
|
31
31
|
# so the older harmony is safe.
|
|
32
32
|
"openai-harmony>=0.0.4",
|
|
33
33
|
# ``BaseRendererConfig`` inherits from ``pydantic_config.BaseConfig`` so
|
|
@@ -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.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 1,
|
|
21
|
+
__version__ = version = '0.1.12.dev1'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 12, 'dev1')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -207,6 +207,7 @@ async def generate(
|
|
|
207
207
|
priority: int | None = None,
|
|
208
208
|
extra_headers: dict[str, str] | None = None,
|
|
209
209
|
max_prompt_len: int | None = None,
|
|
210
|
+
process_multimodal: bool = True,
|
|
210
211
|
) -> dict[str, Any]:
|
|
211
212
|
"""Tokenize messages, call vLLM /inference/v1/generate, parse the response.
|
|
212
213
|
|
|
@@ -226,6 +227,9 @@ async def generate(
|
|
|
226
227
|
sidecar, then serializes it to vLLM's ``features`` schema (mm_hashes,
|
|
227
228
|
mm_placeholders, kwargs_data) before POSTing. The serializer imports
|
|
228
229
|
``vllm.*`` lazily so text-only consumers never pay for the import.
|
|
230
|
+
With ``process_multimodal=False``, rendering skips image processing and
|
|
231
|
+
the request carries ``content_parts`` instead; vLLM must return the
|
|
232
|
+
expanded prompt as ``prompt_token_ids``.
|
|
229
233
|
|
|
230
234
|
``max_prompt_len`` controls the pre-flight overflow check. When the
|
|
231
235
|
rendered prompt is strictly longer than the cap, the request is never
|
|
@@ -236,10 +240,15 @@ async def generate(
|
|
|
236
240
|
cache a ``None`` cap and the pre-flight silently disables. Engine 4xx
|
|
237
241
|
that still slip through propagate raw — converting them into a domain
|
|
238
242
|
error is the calling client's job (its error shape is engine-specific).
|
|
243
|
+
Calls with ``process_multimodal=False`` skip this pre-flight because only
|
|
244
|
+
vLLM knows the expanded prompt length.
|
|
239
245
|
|
|
240
|
-
Returns a dict with: request_id, prompt_ids,
|
|
241
|
-
|
|
242
|
-
|
|
246
|
+
Returns a dict with: request_id, prompt_ids, renderer_prompt_ids,
|
|
247
|
+
mm_placeholders, completion_ids, completion_logprobs, content,
|
|
248
|
+
reasoning_content, tool_calls, finish_reason, routed_experts,
|
|
249
|
+
multi_modal_data, prompt_attribution. ``renderer_prompt_ids`` is the
|
|
250
|
+
unexpanded logical prompt when ``process_multimodal=False`` and ``None``
|
|
251
|
+
otherwise.
|
|
243
252
|
|
|
244
253
|
``prompt_attribution`` is the renderer's :class:`RenderedTokens` for
|
|
245
254
|
the prompt — either the one this call computed via
|
|
@@ -258,6 +267,12 @@ async def generate(
|
|
|
258
267
|
f"{type(renderer).__name__} does not support tools. "
|
|
259
268
|
"Choose a model-specific renderer instead of the default fallback."
|
|
260
269
|
)
|
|
270
|
+
if not process_multimodal and not getattr(
|
|
271
|
+
renderer, "supports_process_multimodal", False
|
|
272
|
+
):
|
|
273
|
+
raise NotImplementedError(
|
|
274
|
+
f"{type(renderer).__name__} does not support process_multimodal=False"
|
|
275
|
+
)
|
|
261
276
|
|
|
262
277
|
def _prepare():
|
|
263
278
|
if prompt_ids is not None:
|
|
@@ -270,7 +285,15 @@ async def generate(
|
|
|
270
285
|
multi_modal_data,
|
|
271
286
|
prompt_attribution,
|
|
272
287
|
)
|
|
273
|
-
|
|
288
|
+
render_kwargs: dict[str, Any] = {}
|
|
289
|
+
if not process_multimodal:
|
|
290
|
+
render_kwargs["process_multimodal"] = False
|
|
291
|
+
rendered = renderer.render(
|
|
292
|
+
messages,
|
|
293
|
+
tools=tools,
|
|
294
|
+
add_generation_prompt=True,
|
|
295
|
+
**render_kwargs,
|
|
296
|
+
)
|
|
274
297
|
return (
|
|
275
298
|
rendered.token_ids,
|
|
276
299
|
renderer.get_stop_token_ids(),
|
|
@@ -280,12 +303,13 @@ async def generate(
|
|
|
280
303
|
|
|
281
304
|
prompt_ids, stop_token_ids, mm_data, prompt_attr = _prepare()
|
|
282
305
|
|
|
283
|
-
if
|
|
284
|
-
max_prompt_len
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
306
|
+
if process_multimodal:
|
|
307
|
+
if max_prompt_len is None:
|
|
308
|
+
max_prompt_len = await _resolve_max_prompt_len(client, model)
|
|
309
|
+
if max_prompt_len is not None and len(prompt_ids) > max_prompt_len:
|
|
310
|
+
raise OverlongPromptError(
|
|
311
|
+
prompt_len=len(prompt_ids), max_prompt_len=max_prompt_len
|
|
312
|
+
)
|
|
289
313
|
|
|
290
314
|
sp: dict[str, Any] = dict(sampling_params or {})
|
|
291
315
|
sp["stop_token_ids"] = stop_token_ids
|
|
@@ -297,11 +321,14 @@ async def generate(
|
|
|
297
321
|
"token_ids": prompt_ids,
|
|
298
322
|
"sampling_params": sp,
|
|
299
323
|
}
|
|
324
|
+
content_parts = _content_parts(messages) if not process_multimodal else None
|
|
300
325
|
features = (
|
|
301
326
|
_build_mm_features(renderer, mm_data)
|
|
302
|
-
if mm_data and not mm_data.is_empty()
|
|
327
|
+
if process_multimodal and mm_data and not mm_data.is_empty()
|
|
303
328
|
else None
|
|
304
329
|
)
|
|
330
|
+
if content_parts:
|
|
331
|
+
body["content_parts"] = content_parts
|
|
305
332
|
if features is not None:
|
|
306
333
|
body["features"] = features
|
|
307
334
|
if cache_salt is not None:
|
|
@@ -331,6 +358,16 @@ async def generate(
|
|
|
331
358
|
|
|
332
359
|
choice = (data.get("choices") or [{}])[0]
|
|
333
360
|
completion_ids = choice.get("token_ids") or []
|
|
361
|
+
effective_prompt_ids = data.get("prompt_token_ids")
|
|
362
|
+
if content_parts and not isinstance(effective_prompt_ids, list):
|
|
363
|
+
raise MalformedGenerateResponseError(
|
|
364
|
+
"Engine response must include prompt_token_ids when process_multimodal=False."
|
|
365
|
+
)
|
|
366
|
+
mm_placeholders = data.get("mm_placeholders")
|
|
367
|
+
if content_parts and not isinstance(mm_placeholders, dict):
|
|
368
|
+
raise MalformedGenerateResponseError(
|
|
369
|
+
"Engine response must include mm_placeholders when process_multimodal=False."
|
|
370
|
+
)
|
|
334
371
|
|
|
335
372
|
completion_logprobs = _parse_completion_logprobs(choice, completion_ids)
|
|
336
373
|
|
|
@@ -356,7 +393,9 @@ async def generate(
|
|
|
356
393
|
|
|
357
394
|
return {
|
|
358
395
|
"request_id": data.get("request_id") or "",
|
|
359
|
-
"prompt_ids": list(prompt_ids),
|
|
396
|
+
"prompt_ids": list(effective_prompt_ids or prompt_ids),
|
|
397
|
+
"renderer_prompt_ids": list(prompt_ids) if content_parts else None,
|
|
398
|
+
"mm_placeholders": mm_placeholders,
|
|
360
399
|
"completion_ids": list(completion_ids),
|
|
361
400
|
"completion_logprobs": completion_logprobs,
|
|
362
401
|
"content": parsed.content,
|
|
@@ -380,6 +419,43 @@ async def generate(
|
|
|
380
419
|
}
|
|
381
420
|
|
|
382
421
|
|
|
422
|
+
_MEDIA_URL_TYPES = {
|
|
423
|
+
"image": "image_url",
|
|
424
|
+
"image_url": "image_url",
|
|
425
|
+
"audio": "audio_url",
|
|
426
|
+
"audio_url": "audio_url",
|
|
427
|
+
"video": "video_url",
|
|
428
|
+
"video_url": "video_url",
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _content_parts(messages: list[Message]) -> list[dict[str, Any]]:
|
|
433
|
+
"""Flatten raw media in prompt order for vLLM's token generate endpoint."""
|
|
434
|
+
parts: list[dict[str, Any]] = []
|
|
435
|
+
for message in messages:
|
|
436
|
+
content = message.get("content")
|
|
437
|
+
if not isinstance(content, list):
|
|
438
|
+
continue
|
|
439
|
+
for part in content:
|
|
440
|
+
if not isinstance(part, Mapping):
|
|
441
|
+
continue
|
|
442
|
+
part_type = part.get("type")
|
|
443
|
+
if part_type is None:
|
|
444
|
+
part_type = next(
|
|
445
|
+
(key for key in _MEDIA_URL_TYPES if part.get(key)), None
|
|
446
|
+
)
|
|
447
|
+
if not isinstance(part_type, str) or part_type not in _MEDIA_URL_TYPES:
|
|
448
|
+
continue
|
|
449
|
+
source = part.get(part_type)
|
|
450
|
+
if source is None:
|
|
451
|
+
source = part.get(_MEDIA_URL_TYPES[part_type]) or part.get("url")
|
|
452
|
+
url = source.get("url") if isinstance(source, Mapping) else source
|
|
453
|
+
if not isinstance(url, str) or not url:
|
|
454
|
+
raise ValueError(f"{part_type} content part is missing a URL")
|
|
455
|
+
parts.append({"type": _MEDIA_URL_TYPES[part_type], "url": url})
|
|
456
|
+
return parts
|
|
457
|
+
|
|
458
|
+
|
|
383
459
|
def _build_mm_features(
|
|
384
460
|
renderer: Renderer,
|
|
385
461
|
mm_data: MultiModalData,
|
|
@@ -591,6 +591,8 @@ class KimiK25Renderer:
|
|
|
591
591
|
The tokenizer should be ``moonshotai/Kimi-K2-Instruct`` (same as K2).
|
|
592
592
|
"""
|
|
593
593
|
|
|
594
|
+
supports_process_multimodal = True
|
|
595
|
+
|
|
594
596
|
def __init__(
|
|
595
597
|
self,
|
|
596
598
|
tokenizer: Tokenizer,
|
|
@@ -734,6 +736,7 @@ class KimiK25Renderer:
|
|
|
734
736
|
*,
|
|
735
737
|
tools: list[ToolSpec] | None = None,
|
|
736
738
|
add_generation_prompt: bool = False,
|
|
739
|
+
process_multimodal: bool = True,
|
|
737
740
|
) -> RenderedTokens:
|
|
738
741
|
"""Render messages to tokens, matching the K2.5 chat template.
|
|
739
742
|
|
|
@@ -821,7 +824,10 @@ class KimiK25Renderer:
|
|
|
821
824
|
``<|media_content|>``, ``<|media_end|>``, the trailing
|
|
822
825
|
``\\n``) are template-injected scaffold.
|
|
823
826
|
"""
|
|
824
|
-
|
|
827
|
+
if process_multimodal:
|
|
828
|
+
_, out, _num_patches, h = self._process_image(part)
|
|
829
|
+
else:
|
|
830
|
+
out = h = None
|
|
825
831
|
emit_special(
|
|
826
832
|
self._media_begin, msg_idx, is_sampled=is_sampled, is_content=False
|
|
827
833
|
)
|
|
@@ -840,6 +846,9 @@ class KimiK25Renderer:
|
|
|
840
846
|
self._media_end, msg_idx, is_sampled=is_sampled, is_content=False
|
|
841
847
|
)
|
|
842
848
|
emit_text("\n", msg_idx, is_sampled=is_sampled, is_content=False)
|
|
849
|
+
if not process_multimodal:
|
|
850
|
+
return
|
|
851
|
+
assert out is not None and h is not None
|
|
843
852
|
mm_hashes.setdefault("image", []).append(h)
|
|
844
853
|
mm_placeholders.setdefault("image", []).append(
|
|
845
854
|
PlaceholderRange(offset=offset, length=1)
|
|
@@ -1027,6 +1036,7 @@ class KimiK25Renderer:
|
|
|
1027
1036
|
*,
|
|
1028
1037
|
tools: list[ToolSpec] | None = None,
|
|
1029
1038
|
previous_multi_modal_data: MultiModalData | None = None,
|
|
1039
|
+
process_multimodal: bool = True,
|
|
1030
1040
|
) -> "RenderedTokens | None":
|
|
1031
1041
|
if (
|
|
1032
1042
|
not previous_prompt_ids
|
|
@@ -1115,7 +1125,10 @@ class KimiK25Renderer:
|
|
|
1115
1125
|
is_sampled: bool = False,
|
|
1116
1126
|
is_content: bool = False,
|
|
1117
1127
|
) -> None:
|
|
1118
|
-
|
|
1128
|
+
if process_multimodal:
|
|
1129
|
+
_, out, _num_patches, h = self._process_image(part)
|
|
1130
|
+
else:
|
|
1131
|
+
out = h = None
|
|
1119
1132
|
emit_special(self._media_begin, msg_idx)
|
|
1120
1133
|
emit_text("image", msg_idx)
|
|
1121
1134
|
emit_special(self._media_content, msg_idx)
|
|
@@ -1125,6 +1138,9 @@ class KimiK25Renderer:
|
|
|
1125
1138
|
emit_special(self._media_pad, msg_idx, is_content=is_content)
|
|
1126
1139
|
emit_special(self._media_end, msg_idx)
|
|
1127
1140
|
emit_text("\n", msg_idx)
|
|
1141
|
+
if not process_multimodal:
|
|
1142
|
+
return
|
|
1143
|
+
assert out is not None and h is not None
|
|
1128
1144
|
new_hashes.setdefault("image", []).append(h)
|
|
1129
1145
|
new_placeholders.setdefault("image", []).append(
|
|
1130
1146
|
PlaceholderRange(offset=offset, length=1)
|
|
@@ -1188,17 +1204,17 @@ class KimiK25Renderer:
|
|
|
1188
1204
|
# below never mutates the caller's previous_multi_modal_data.
|
|
1189
1205
|
merged_hashes: dict[str, list[str]] = (
|
|
1190
1206
|
{k: list(v) for k, v in previous_multi_modal_data.mm_hashes.items()}
|
|
1191
|
-
if previous_multi_modal_data
|
|
1207
|
+
if process_multimodal and previous_multi_modal_data
|
|
1192
1208
|
else {}
|
|
1193
1209
|
)
|
|
1194
1210
|
merged_placeholders: dict[str, list[PlaceholderRange]] = (
|
|
1195
1211
|
{k: list(v) for k, v in previous_multi_modal_data.mm_placeholders.items()}
|
|
1196
|
-
if previous_multi_modal_data
|
|
1212
|
+
if process_multimodal and previous_multi_modal_data
|
|
1197
1213
|
else {}
|
|
1198
1214
|
)
|
|
1199
1215
|
merged_items: dict[str, list[dict[str, Any]]] = (
|
|
1200
1216
|
{k: list(v) for k, v in previous_multi_modal_data.mm_items.items()}
|
|
1201
|
-
if previous_multi_modal_data
|
|
1217
|
+
if process_multimodal and previous_multi_modal_data
|
|
1202
1218
|
else {}
|
|
1203
1219
|
)
|
|
1204
1220
|
for modality, vals in new_hashes.items():
|
|
@@ -123,6 +123,7 @@ def _default_enable_thinking(tokenizer) -> bool:
|
|
|
123
123
|
class Qwen35Renderer:
|
|
124
124
|
"""Deterministic message → token renderer for Qwen3.5 models."""
|
|
125
125
|
|
|
126
|
+
supports_process_multimodal = True
|
|
126
127
|
_config_cls: type = Qwen35RendererConfig
|
|
127
128
|
|
|
128
129
|
def __init__(
|
|
@@ -353,6 +354,7 @@ class Qwen35Renderer:
|
|
|
353
354
|
*,
|
|
354
355
|
tools: list[ToolSpec] | None = None,
|
|
355
356
|
add_generation_prompt: bool = False,
|
|
357
|
+
process_multimodal: bool = True,
|
|
356
358
|
) -> RenderedTokens:
|
|
357
359
|
if not messages:
|
|
358
360
|
raise ValueError("No messages provided.")
|
|
@@ -424,7 +426,11 @@ class Qwen35Renderer:
|
|
|
424
426
|
# image data, so they ARE body content (is_content=True);
|
|
425
427
|
# the surrounding ``<|vision_start|>`` / ``<|vision_end|>``
|
|
426
428
|
# specials are template scaffold.
|
|
427
|
-
|
|
429
|
+
if process_multimodal:
|
|
430
|
+
_, out, n, h = self._process_image(part)
|
|
431
|
+
else:
|
|
432
|
+
out = h = None
|
|
433
|
+
n = 1
|
|
428
434
|
vision_counts["image"] += 1
|
|
429
435
|
if self.config.add_vision_id:
|
|
430
436
|
emit_text(
|
|
@@ -442,16 +448,18 @@ class Qwen35Renderer:
|
|
|
442
448
|
self._image_pad, msg_idx, is_sampled=False, is_content=True
|
|
443
449
|
)
|
|
444
450
|
emit_special(self._vision_end, msg_idx, is_sampled=False, is_content=False)
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
451
|
+
if process_multimodal:
|
|
452
|
+
assert out is not None and h is not None
|
|
453
|
+
mm_hashes.setdefault("image", []).append(h)
|
|
454
|
+
mm_placeholders.setdefault("image", []).append(
|
|
455
|
+
PlaceholderRange(offset=offset, length=n)
|
|
456
|
+
)
|
|
457
|
+
mm_items.setdefault("image", []).append(
|
|
458
|
+
{
|
|
459
|
+
"pixel_values": out["pixel_values"],
|
|
460
|
+
"image_grid_thw": out["image_grid_thw"],
|
|
461
|
+
}
|
|
462
|
+
)
|
|
455
463
|
|
|
456
464
|
def emit_user_with_media(content_list: list[Any], msg_idx: int) -> None:
|
|
457
465
|
"""Emit a user message whose content list contains image parts.
|
|
@@ -685,6 +693,7 @@ class Qwen35Renderer:
|
|
|
685
693
|
*,
|
|
686
694
|
tools: list[ToolSpec] | None = None,
|
|
687
695
|
previous_multi_modal_data: MultiModalData | None = None,
|
|
696
|
+
process_multimodal: bool = True,
|
|
688
697
|
) -> "RenderedTokens | None":
|
|
689
698
|
if (
|
|
690
699
|
not previous_prompt_ids
|
|
@@ -721,6 +730,7 @@ class Qwen35Renderer:
|
|
|
721
730
|
# scratch correctly.
|
|
722
731
|
if (
|
|
723
732
|
self.config.add_vision_id
|
|
733
|
+
and process_multimodal
|
|
724
734
|
and previous_multi_modal_data is None
|
|
725
735
|
and self._vision_start in previous_ids
|
|
726
736
|
):
|
|
@@ -751,9 +761,11 @@ class Qwen35Renderer:
|
|
|
751
761
|
# ``add_vision_id`` parity across turns.
|
|
752
762
|
prev_image_count = 0
|
|
753
763
|
prev_video_count = 0
|
|
754
|
-
if previous_multi_modal_data is not None:
|
|
764
|
+
if process_multimodal and previous_multi_modal_data is not None:
|
|
755
765
|
prev_image_count = len(previous_multi_modal_data.mm_items.get("image", []))
|
|
756
766
|
prev_video_count = len(previous_multi_modal_data.mm_items.get("video", []))
|
|
767
|
+
elif not process_multimodal:
|
|
768
|
+
prev_image_count = previous_ids.count(self._image_pad)
|
|
757
769
|
vision_counts = {"image": prev_image_count, "video": prev_video_count}
|
|
758
770
|
|
|
759
771
|
def emit_special(
|
|
@@ -796,7 +808,11 @@ class Qwen35Renderer:
|
|
|
796
808
|
content_mask.append(is_content)
|
|
797
809
|
|
|
798
810
|
def emit_image(part: dict[str, Any], msg_idx: int = -1) -> None:
|
|
799
|
-
|
|
811
|
+
if process_multimodal:
|
|
812
|
+
_, out, n, h = self._process_image(part)
|
|
813
|
+
else:
|
|
814
|
+
out = h = None
|
|
815
|
+
n = 1
|
|
800
816
|
vision_counts["image"] += 1
|
|
801
817
|
if self.config.add_vision_id:
|
|
802
818
|
emit_text(f"Picture {vision_counts['image']}: ", msg_idx)
|
|
@@ -805,16 +821,18 @@ class Qwen35Renderer:
|
|
|
805
821
|
for _ in range(n):
|
|
806
822
|
emit_special(self._image_pad, msg_idx, is_content=True)
|
|
807
823
|
emit_special(self._vision_end, msg_idx)
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
824
|
+
if process_multimodal:
|
|
825
|
+
assert out is not None and h is not None
|
|
826
|
+
new_hashes.setdefault("image", []).append(h)
|
|
827
|
+
new_placeholders.setdefault("image", []).append(
|
|
828
|
+
PlaceholderRange(offset=offset, length=n)
|
|
829
|
+
)
|
|
830
|
+
new_items.setdefault("image", []).append(
|
|
831
|
+
{
|
|
832
|
+
"pixel_values": out["pixel_values"],
|
|
833
|
+
"image_grid_thw": out["image_grid_thw"],
|
|
834
|
+
}
|
|
835
|
+
)
|
|
818
836
|
|
|
819
837
|
def emit_user_with_media(content_list: list[Any], msg_idx: int) -> None:
|
|
820
838
|
emit_special(self._im_start, msg_idx)
|
|
@@ -905,17 +923,17 @@ class Qwen35Renderer:
|
|
|
905
923
|
# below never mutates the caller's previous_multi_modal_data.
|
|
906
924
|
merged_hashes: dict[str, list[str]] = (
|
|
907
925
|
{k: list(v) for k, v in previous_multi_modal_data.mm_hashes.items()}
|
|
908
|
-
if previous_multi_modal_data
|
|
926
|
+
if process_multimodal and previous_multi_modal_data
|
|
909
927
|
else {}
|
|
910
928
|
)
|
|
911
929
|
merged_placeholders: dict[str, list[PlaceholderRange]] = (
|
|
912
930
|
{k: list(v) for k, v in previous_multi_modal_data.mm_placeholders.items()}
|
|
913
|
-
if previous_multi_modal_data
|
|
931
|
+
if process_multimodal and previous_multi_modal_data
|
|
914
932
|
else {}
|
|
915
933
|
)
|
|
916
934
|
merged_items: dict[str, list[dict[str, Any]]] = (
|
|
917
935
|
{k: list(v) for k, v in previous_multi_modal_data.mm_items.items()}
|
|
918
|
-
if previous_multi_modal_data
|
|
936
|
+
if process_multimodal and previous_multi_modal_data
|
|
919
937
|
else {}
|
|
920
938
|
)
|
|
921
939
|
for modality, vals in new_hashes.items():
|
|
@@ -310,6 +310,8 @@ class Qwen3VLRenderer:
|
|
|
310
310
|
``thinking_retention`` still controls whether the bridge is attempted.
|
|
311
311
|
"""
|
|
312
312
|
|
|
313
|
+
supports_process_multimodal = True
|
|
314
|
+
|
|
313
315
|
def __init__(
|
|
314
316
|
self,
|
|
315
317
|
tokenizer: Tokenizer,
|
|
@@ -457,6 +459,7 @@ class Qwen3VLRenderer:
|
|
|
457
459
|
*,
|
|
458
460
|
tools: list[ToolSpec] | None = None,
|
|
459
461
|
add_generation_prompt: bool = False,
|
|
462
|
+
process_multimodal: bool = True,
|
|
460
463
|
) -> RenderedTokens:
|
|
461
464
|
if not messages:
|
|
462
465
|
raise ValueError("No messages provided.")
|
|
@@ -480,7 +483,11 @@ class Qwen3VLRenderer:
|
|
|
480
483
|
# image data, so they ARE body content (is_content=True);
|
|
481
484
|
# the surrounding ``<|vision_start|>`` / ``<|vision_end|>``
|
|
482
485
|
# markers are renderer-emitted scaffold.
|
|
483
|
-
|
|
486
|
+
if process_multimodal:
|
|
487
|
+
_, out, n, h = self._process_image(part)
|
|
488
|
+
else:
|
|
489
|
+
out = h = None
|
|
490
|
+
n = 1
|
|
484
491
|
vision_counts["image"] += 1
|
|
485
492
|
if self.config.add_vision_id:
|
|
486
493
|
em.text(
|
|
@@ -493,16 +500,18 @@ class Qwen3VLRenderer:
|
|
|
493
500
|
for _ in range(n):
|
|
494
501
|
em.special(self._image_pad, is_sampled=False, is_content=True)
|
|
495
502
|
em.special(self._vision_end, is_sampled=False, is_content=False)
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
503
|
+
if process_multimodal:
|
|
504
|
+
assert out is not None and h is not None
|
|
505
|
+
mm_hashes.setdefault("image", []).append(h)
|
|
506
|
+
mm_placeholders.setdefault("image", []).append(
|
|
507
|
+
PlaceholderRange(offset=offset, length=n)
|
|
508
|
+
)
|
|
509
|
+
mm_items.setdefault("image", []).append(
|
|
510
|
+
{
|
|
511
|
+
"pixel_values": out["pixel_values"],
|
|
512
|
+
"image_grid_thw": out["image_grid_thw"],
|
|
513
|
+
}
|
|
514
|
+
)
|
|
506
515
|
|
|
507
516
|
def render_media_content(content: Any) -> None:
|
|
508
517
|
"""Emit a user/tool content list with media handled inline.
|
|
@@ -665,6 +674,7 @@ class Qwen3VLRenderer:
|
|
|
665
674
|
*,
|
|
666
675
|
tools: list[ToolSpec] | None = None,
|
|
667
676
|
previous_multi_modal_data: MultiModalData | None = None,
|
|
677
|
+
process_multimodal: bool = True,
|
|
668
678
|
) -> "RenderedTokens | None":
|
|
669
679
|
"""Extend the prior turn with ``new_messages``.
|
|
670
680
|
|
|
@@ -709,6 +719,7 @@ class Qwen3VLRenderer:
|
|
|
709
719
|
# correctly.
|
|
710
720
|
if (
|
|
711
721
|
self.config.add_vision_id
|
|
722
|
+
and process_multimodal
|
|
712
723
|
and previous_multi_modal_data is None
|
|
713
724
|
and self._vision_start in previous_ids
|
|
714
725
|
):
|
|
@@ -746,13 +757,19 @@ class Qwen3VLRenderer:
|
|
|
746
757
|
# ``add_vision_id`` parity across turns.
|
|
747
758
|
prev_image_count = 0
|
|
748
759
|
prev_video_count = 0
|
|
749
|
-
if previous_multi_modal_data is not None:
|
|
760
|
+
if process_multimodal and previous_multi_modal_data is not None:
|
|
750
761
|
prev_image_count = len(previous_multi_modal_data.mm_items.get("image", []))
|
|
751
762
|
prev_video_count = len(previous_multi_modal_data.mm_items.get("video", []))
|
|
763
|
+
elif not process_multimodal:
|
|
764
|
+
prev_image_count = previous_ids.count(self._image_pad)
|
|
752
765
|
vision_counts = {"image": prev_image_count, "video": prev_video_count}
|
|
753
766
|
|
|
754
767
|
def emit_image(part: dict[str, Any]) -> None:
|
|
755
|
-
|
|
768
|
+
if process_multimodal:
|
|
769
|
+
_, out, n, h = self._process_image(part)
|
|
770
|
+
else:
|
|
771
|
+
out = h = None
|
|
772
|
+
n = 1
|
|
756
773
|
vision_counts["image"] += 1
|
|
757
774
|
if self.config.add_vision_id:
|
|
758
775
|
em.text(
|
|
@@ -765,16 +782,18 @@ class Qwen3VLRenderer:
|
|
|
765
782
|
for _ in range(n):
|
|
766
783
|
em.special(self._image_pad, is_sampled=False, is_content=True)
|
|
767
784
|
em.special(self._vision_end, is_sampled=False, is_content=False)
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
785
|
+
if process_multimodal:
|
|
786
|
+
assert out is not None and h is not None
|
|
787
|
+
new_hashes.setdefault("image", []).append(h)
|
|
788
|
+
new_placeholders.setdefault("image", []).append(
|
|
789
|
+
PlaceholderRange(offset=offset, length=n)
|
|
790
|
+
)
|
|
791
|
+
new_items.setdefault("image", []).append(
|
|
792
|
+
{
|
|
793
|
+
"pixel_values": out["pixel_values"],
|
|
794
|
+
"image_grid_thw": out["image_grid_thw"],
|
|
795
|
+
}
|
|
796
|
+
)
|
|
778
797
|
|
|
779
798
|
def render_media_content(content: Any) -> None:
|
|
780
799
|
if isinstance(content, str):
|
|
@@ -833,17 +852,17 @@ class Qwen3VLRenderer:
|
|
|
833
852
|
# caller's previous_multi_modal_data.
|
|
834
853
|
merged_hashes = (
|
|
835
854
|
{k: list(v) for k, v in previous_multi_modal_data.mm_hashes.items()}
|
|
836
|
-
if previous_multi_modal_data
|
|
855
|
+
if process_multimodal and previous_multi_modal_data
|
|
837
856
|
else {}
|
|
838
857
|
)
|
|
839
858
|
merged_placeholders = (
|
|
840
859
|
{k: list(v) for k, v in previous_multi_modal_data.mm_placeholders.items()}
|
|
841
|
-
if previous_multi_modal_data
|
|
860
|
+
if process_multimodal and previous_multi_modal_data
|
|
842
861
|
else {}
|
|
843
862
|
)
|
|
844
863
|
merged_items = (
|
|
845
864
|
{k: list(v) for k, v in previous_multi_modal_data.mm_items.items()}
|
|
846
|
-
if previous_multi_modal_data
|
|
865
|
+
if process_multimodal and previous_multi_modal_data
|
|
847
866
|
else {}
|
|
848
867
|
)
|
|
849
868
|
for modality, vals in new_hashes.items():
|