renderers 0.1.10.dev13__tar.gz → 0.1.11.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.10.dev13 → renderers-0.1.11.dev1}/.github/workflows/publish-dev.yml +1 -1
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/.github/workflows/publish.yml +1 -1
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/PKG-INFO +3 -3
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/README.md +1 -1
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/docs/renderer-config.md +2 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/renderers/__init__.py +4 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/renderers/_version.py +2 -2
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/renderers/base.py +12 -4
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/renderers/configs.py +49 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/renderers/qwen35.py +71 -27
- renderers-0.1.11.dev1/renderers/qwen38.py +66 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/conftest.py +2 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_bridge.py +2 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_client.py +3 -1
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_disabled_thinking_stability.py +9 -0
- renderers-0.1.11.dev1/tests/test_qwen38.py +178 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_renderer_config_parity.py +7 -4
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_roundtrip.py +2 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/.github/workflows/style.yml +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/.github/workflows/test.yml +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/.gitignore +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/.pre-commit-config.yaml +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/LICENSE +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/examples/README.md +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/examples/sglang/multiturn_generate_sglang.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/examples/sglang/online_multiturn_sglang.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/examples/tinker/multiturn_generate_tinker.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/examples/transformers/multiturn_generate_transformers.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/examples/vllm/multiturn_generate_vllm.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/pyproject.toml +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/renderers/client.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/renderers/deepseek_r1.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/renderers/deepseek_v3.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/renderers/default.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/renderers/gemma4.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/renderers/glm45.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/renderers/glm5.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/renderers/gpt_oss.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/renderers/hy3.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/renderers/inkling.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/renderers/kimi_k2.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/renderers/kimi_k25.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/renderers/laguna_s21.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/renderers/laguna_xs2.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/renderers/llama_3.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/renderers/minimax_m2.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/renderers/nemotron3.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/renderers/parsers.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/renderers/parsing.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/renderers/prime_qwen3.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/renderers/qwen3.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/renderers/qwen36.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/renderers/qwen3_vl.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_build_helpers.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_deepseek_r1.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_gemma4.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_glm_tool_name_validation.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_gpt_oss_harmony_parity.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_hy3.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_incremental.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_inkling.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_is_content.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_kimi_k25_tool_schema.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_laguna_m1.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_laguna_s21.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_laguna_xs21.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_llama_3.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_load_tokenizer.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_message_indices.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_message_tool_names.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_multimodal.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_nemotron3_parity.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_nemotron3_ultra.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_parse_response.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_parse_response_robustness.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_parsers.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_preserve_thinking.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_prime_qwen3_parity.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_qwen35_size_coverage.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_render_ids.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_renderer_config.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_sampled_mask.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_tokens_per_message.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_tool_arg_type_preservation.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: renderers
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.11.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
|
|
@@ -56,7 +56,7 @@ next_prompt_ids = r.bridge_to_next_turn(
|
|
|
56
56
|
)
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
-
Hand-coded renderers ship for `qwen3`, `qwen3-vl`, `qwen3.5`, `qwen3.6`, `gemma4`, `glm-5`, `glm-5.1`, `glm-4.5`, `minimax-m2`, `deepseek-v3`, `deepseek-r1`, `kimi-k2`, `kimi-k2.5` / `kimi-k2.6`, `laguna-xs.2`, `laguna-xs-2.1`, `laguna-s-2.1`, `laguna-m.1`, `nemotron-3`, `nemotron-3-ultra`, `nemotron-3.5`, `llama-3`, `gpt-oss`, `hy3`, `inkling` / `inkling-small`, and `prime-qwen3`. Anything else falls back to `DefaultRenderer`, a generic `apply_chat_template` wrapper. `qwen3-vl`, `qwen3.5`, `qwen3.6`, `gemma4`, `kimi-k2.5` / `kimi-k2.6`, and the Inkling checkpoints are multimodal (Inkling handles both image **and** audio).
|
|
59
|
+
Hand-coded renderers ship for `qwen3`, `qwen3-vl`, `qwen3.5`, `qwen3.6`, `qwen3.8`, `gemma4`, `glm-5`, `glm-5.1`, `glm-4.5`, `minimax-m2`, `deepseek-v3`, `deepseek-r1`, `kimi-k2`, `kimi-k2.5` / `kimi-k2.6`, `laguna-xs.2`, `laguna-xs-2.1`, `laguna-s-2.1`, `laguna-m.1`, `nemotron-3`, `nemotron-3-ultra`, `nemotron-3.5`, `llama-3`, `gpt-oss`, `hy3`, `inkling` / `inkling-small`, and `prime-qwen3`. Anything else falls back to `DefaultRenderer`, a generic `apply_chat_template` wrapper. `qwen3-vl`, `qwen3.5`, `qwen3.6`, `qwen3.8`, `gemma4`, `kimi-k2.5` / `kimi-k2.6`, and the Inkling checkpoints are multimodal (Inkling handles both image **and** audio).
|
|
60
60
|
|
|
61
61
|
## API
|
|
62
62
|
|
|
@@ -40,7 +40,7 @@ next_prompt_ids = r.bridge_to_next_turn(
|
|
|
40
40
|
)
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
Hand-coded renderers ship for `qwen3`, `qwen3-vl`, `qwen3.5`, `qwen3.6`, `gemma4`, `glm-5`, `glm-5.1`, `glm-4.5`, `minimax-m2`, `deepseek-v3`, `deepseek-r1`, `kimi-k2`, `kimi-k2.5` / `kimi-k2.6`, `laguna-xs.2`, `laguna-xs-2.1`, `laguna-s-2.1`, `laguna-m.1`, `nemotron-3`, `nemotron-3-ultra`, `nemotron-3.5`, `llama-3`, `gpt-oss`, `hy3`, `inkling` / `inkling-small`, and `prime-qwen3`. Anything else falls back to `DefaultRenderer`, a generic `apply_chat_template` wrapper. `qwen3-vl`, `qwen3.5`, `qwen3.6`, `gemma4`, `kimi-k2.5` / `kimi-k2.6`, and the Inkling checkpoints are multimodal (Inkling handles both image **and** audio).
|
|
43
|
+
Hand-coded renderers ship for `qwen3`, `qwen3-vl`, `qwen3.5`, `qwen3.6`, `qwen3.8`, `gemma4`, `glm-5`, `glm-5.1`, `glm-4.5`, `minimax-m2`, `deepseek-v3`, `deepseek-r1`, `kimi-k2`, `kimi-k2.5` / `kimi-k2.6`, `laguna-xs.2`, `laguna-xs-2.1`, `laguna-s-2.1`, `laguna-m.1`, `nemotron-3`, `nemotron-3-ultra`, `nemotron-3.5`, `llama-3`, `gpt-oss`, `hy3`, `inkling` / `inkling-small`, and `prime-qwen3`. Anything else falls back to `DefaultRenderer`, a generic `apply_chat_template` wrapper. `qwen3-vl`, `qwen3.5`, `qwen3.6`, `qwen3.8`, `gemma4`, `kimi-k2.5` / `kimi-k2.6`, and the Inkling checkpoints are multimodal (Inkling handles both image **and** audio).
|
|
44
44
|
|
|
45
45
|
## API
|
|
46
46
|
|
|
@@ -31,6 +31,7 @@ definition time. Template fields are covered by parity tests against
|
|
|
31
31
|
| PrimeIntellect Qwen3 | `PrimeQwen3RendererConfig` | - | - |
|
|
32
32
|
| Qwen3.5 | `Qwen35RendererConfig` | `enable_thinking`, `add_vision_id` | `image_cache_max` |
|
|
33
33
|
| Qwen3.6 | `Qwen36RendererConfig` | `enable_thinking`, `add_vision_id`, `preserve_thinking` | `image_cache_max` |
|
|
34
|
+
| Qwen3.8 | `Qwen38RendererConfig` | `enable_thinking`, `add_vision_id`, `preserve_thinking`, `reasoning_effort` | `image_cache_max` |
|
|
34
35
|
| Qwen3-VL | `Qwen3VLRendererConfig` | `add_vision_id` | `image_cache_max` |
|
|
35
36
|
| Gemma 4 | `Gemma4RendererConfig` | `enable_thinking`, `preserve_thinking` | `image_cache_max` |
|
|
36
37
|
| GLM-5 / 5.1 | `GLM5RendererConfig` / `GLM51RendererConfig` | `enable_thinking`, `clear_thinking` | - |
|
|
@@ -140,6 +141,7 @@ the knobs its template actually exposes:
|
|
|
140
141
|
| Qwen3 | `enable_thinking=False -> all`, else `tool_cycle` |
|
|
141
142
|
| Qwen3.5 | `enable_thinking=False -> all`, else `tool_cycle` |
|
|
142
143
|
| Qwen3.6 | `preserve_thinking=True -> all`; else `enable_thinking=False -> all`; else `tool_cycle` |
|
|
144
|
+
| Qwen3.8 | `preserve_thinking=True -> all`; else `enable_thinking=False -> all`; else `tool_cycle` |
|
|
143
145
|
| Gemma 4 | `preserve_thinking=True -> all`; else `enable_thinking=False -> all`; else `tool_cycle` |
|
|
144
146
|
| GLM-5 / 5.1 | `clear_thinking=False -> all`; else `enable_thinking=False -> all`; else `tool_cycle` |
|
|
145
147
|
| GLM-4.5 | `enable_thinking=False -> all`, else `tool_cycle` |
|
|
@@ -68,6 +68,7 @@ from renderers.configs import (
|
|
|
68
68
|
PrimeQwen3RendererConfig,
|
|
69
69
|
Qwen35RendererConfig,
|
|
70
70
|
Qwen36RendererConfig,
|
|
71
|
+
Qwen38RendererConfig,
|
|
71
72
|
Qwen3RendererConfig,
|
|
72
73
|
Qwen3VLRendererConfig,
|
|
73
74
|
RendererConfig,
|
|
@@ -109,6 +110,7 @@ _LAZY_RENDERERS: dict[str, str] = {
|
|
|
109
110
|
"PrimeQwen3Renderer": "renderers.prime_qwen3",
|
|
110
111
|
"Qwen35Renderer": "renderers.qwen35",
|
|
111
112
|
"Qwen36Renderer": "renderers.qwen36",
|
|
113
|
+
"Qwen38Renderer": "renderers.qwen38",
|
|
112
114
|
"Qwen3Renderer": "renderers.qwen3",
|
|
113
115
|
"Qwen3VLRenderer": "renderers.qwen3_vl",
|
|
114
116
|
}
|
|
@@ -192,6 +194,8 @@ __all__ = [
|
|
|
192
194
|
"Qwen35RendererConfig",
|
|
193
195
|
"Qwen36Renderer",
|
|
194
196
|
"Qwen36RendererConfig",
|
|
197
|
+
"Qwen38Renderer",
|
|
198
|
+
"Qwen38RendererConfig",
|
|
195
199
|
"Qwen3Renderer",
|
|
196
200
|
"Qwen3RendererConfig",
|
|
197
201
|
"Qwen3VLRenderer",
|
|
@@ -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.11.dev1'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 11, 'dev1')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -794,10 +794,10 @@ class MultimodalRenderer(Renderer, Protocol):
|
|
|
794
794
|
"""A :class:`Renderer` that supports multimodal inputs (images, video).
|
|
795
795
|
|
|
796
796
|
Concrete classes (``Qwen3VLRenderer``, ``Qwen35Renderer``,
|
|
797
|
-
``Qwen36Renderer``, ``KimiK25Renderer``) implement
|
|
798
|
-
structurally — no explicit inheritance required.
|
|
799
|
-
to drive vLLM's ``multi_modal_data`` features field or
|
|
800
|
-
forward across turns should dispatch on ``isinstance(r,
|
|
797
|
+
``Qwen36Renderer``, ``Qwen38Renderer``, ``KimiK25Renderer``) implement
|
|
798
|
+
this Protocol structurally — no explicit inheritance required.
|
|
799
|
+
Callers that need to drive vLLM's ``multi_modal_data`` features field or
|
|
800
|
+
carry images forward across turns should dispatch on ``isinstance(r,
|
|
801
801
|
MultimodalRenderer)`` and use the extended ``bridge_to_next_turn``
|
|
802
802
|
signature below.
|
|
803
803
|
"""
|
|
@@ -1031,6 +1031,9 @@ MODEL_RENDERER_MAP: dict[str, str] = {
|
|
|
1031
1031
|
"Qwen/Qwen3.5-397B-A17B": "qwen3.5",
|
|
1032
1032
|
# Qwen3.6.
|
|
1033
1033
|
"Qwen/Qwen3.6-35B-A3B": "qwen3.6",
|
|
1034
|
+
# Qwen3.8.
|
|
1035
|
+
"Qwen/Qwen3.8-27B": "qwen3.8",
|
|
1036
|
+
"Qwen/Qwen3.8-Flash-Next": "qwen3.8",
|
|
1034
1037
|
# Qwen3-VL.
|
|
1035
1038
|
"Qwen/Qwen3-VL-4B-Instruct": "qwen3-vl",
|
|
1036
1039
|
"Qwen/Qwen3-VL-8B-Instruct": "qwen3-vl",
|
|
@@ -1136,6 +1139,9 @@ MULTIMODAL_MODELS: dict[str, set[str]] = {
|
|
|
1136
1139
|
# Qwen3.6 extends Qwen3.5's chat template; same VL bits, only
|
|
1137
1140
|
# tool-call argument serialization differs.
|
|
1138
1141
|
"Qwen/Qwen3.6-35B-A3B": {"image"},
|
|
1142
|
+
# Qwen3.8 adds reasoning-effort control and preserves thinking by default.
|
|
1143
|
+
"Qwen/Qwen3.8-27B": {"image"},
|
|
1144
|
+
"Qwen/Qwen3.8-Flash-Next": {"image"},
|
|
1139
1145
|
# Kimi K2.5 / K2.6 are unified VLMs (HF tag ``image-text-to-text``)
|
|
1140
1146
|
# with custom processor (``KimiK25Processor`` + ``KimiK25VisionProcessor``).
|
|
1141
1147
|
# Vision wrap is different from Qwen-VL:
|
|
@@ -1367,6 +1373,7 @@ def _populate_registry():
|
|
|
1367
1373
|
from renderers.qwen3_vl import Qwen3VLRenderer
|
|
1368
1374
|
from renderers.qwen35 import Qwen35Renderer
|
|
1369
1375
|
from renderers.qwen36 import Qwen36Renderer
|
|
1376
|
+
from renderers.qwen38 import Qwen38Renderer
|
|
1370
1377
|
|
|
1371
1378
|
RENDERER_REGISTRY.update(
|
|
1372
1379
|
{
|
|
@@ -1377,6 +1384,7 @@ def _populate_registry():
|
|
|
1377
1384
|
"gemma4": Gemma4Renderer,
|
|
1378
1385
|
"qwen3.5": Qwen35Renderer,
|
|
1379
1386
|
"qwen3.6": Qwen36Renderer,
|
|
1387
|
+
"qwen3.8": Qwen38Renderer,
|
|
1380
1388
|
"glm-5": GLM5Renderer,
|
|
1381
1389
|
"glm-5.1": GLM51Renderer,
|
|
1382
1390
|
"glm-4.5": GLM45Renderer,
|
|
@@ -271,6 +271,52 @@ class Qwen36RendererConfig(BaseRendererConfig):
|
|
|
271
271
|
return self
|
|
272
272
|
|
|
273
273
|
|
|
274
|
+
class Qwen38RendererConfig(BaseRendererConfig):
|
|
275
|
+
"""Qwen3.8 renderer config.
|
|
276
|
+
|
|
277
|
+
Qwen3.8 keeps Qwen3.6's multimodal/tool template and adds native
|
|
278
|
+
reasoning-effort instructions. Historical thinking is preserved by
|
|
279
|
+
default in the upstream template.
|
|
280
|
+
"""
|
|
281
|
+
|
|
282
|
+
name: Literal["qwen3.8"] = "qwen3.8"
|
|
283
|
+
_template_fields = frozenset(
|
|
284
|
+
{
|
|
285
|
+
"enable_thinking",
|
|
286
|
+
"add_vision_id",
|
|
287
|
+
"preserve_thinking",
|
|
288
|
+
"reasoning_effort",
|
|
289
|
+
}
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
enable_thinking: bool | None = None
|
|
293
|
+
"""See :class:`Qwen35RendererConfig.enable_thinking`."""
|
|
294
|
+
|
|
295
|
+
add_vision_id: bool = False
|
|
296
|
+
"""See :class:`Qwen35RendererConfig.add_vision_id`."""
|
|
297
|
+
|
|
298
|
+
preserve_thinking: bool = True
|
|
299
|
+
"""Keep historical ``<think>`` blocks. Mirrors Qwen3.8's default."""
|
|
300
|
+
|
|
301
|
+
reasoning_effort: Literal["xhigh", "medium", "low"] = "xhigh"
|
|
302
|
+
"""Reasoning depth hint injected into the system prompt when enabled."""
|
|
303
|
+
|
|
304
|
+
image_cache_max: int = 256
|
|
305
|
+
"""See :class:`Qwen35RendererConfig.image_cache_max`."""
|
|
306
|
+
|
|
307
|
+
_internal_fields = frozenset({"image_cache_max"})
|
|
308
|
+
|
|
309
|
+
@model_validator(mode="after")
|
|
310
|
+
def _check_thinking_retention(self):
|
|
311
|
+
_reject_thinking_retention_conflict(
|
|
312
|
+
self,
|
|
313
|
+
"preserve_thinking",
|
|
314
|
+
true_implies="all",
|
|
315
|
+
false_implies="tool_cycle",
|
|
316
|
+
)
|
|
317
|
+
return self
|
|
318
|
+
|
|
319
|
+
|
|
274
320
|
class Qwen3VLRendererConfig(BaseRendererConfig):
|
|
275
321
|
"""Qwen3-VL renderer config."""
|
|
276
322
|
|
|
@@ -892,6 +938,7 @@ RendererConfig = Annotated[
|
|
|
892
938
|
PrimeQwen3RendererConfig,
|
|
893
939
|
Qwen35RendererConfig,
|
|
894
940
|
Qwen36RendererConfig,
|
|
941
|
+
Qwen38RendererConfig,
|
|
895
942
|
Qwen3VLRendererConfig,
|
|
896
943
|
Gemma4RendererConfig,
|
|
897
944
|
GLM5RendererConfig,
|
|
@@ -937,6 +984,7 @@ _CONFIG_BY_NAME: dict[str, type[BaseRendererConfig]] = {
|
|
|
937
984
|
"prime-qwen3": PrimeQwen3RendererConfig,
|
|
938
985
|
"qwen3.5": Qwen35RendererConfig,
|
|
939
986
|
"qwen3.6": Qwen36RendererConfig,
|
|
987
|
+
"qwen3.8": Qwen38RendererConfig,
|
|
940
988
|
"qwen3-vl": Qwen3VLRendererConfig,
|
|
941
989
|
"gemma4": Gemma4RendererConfig,
|
|
942
990
|
"glm-5": GLM5RendererConfig,
|
|
@@ -1013,6 +1061,7 @@ __all__ = [
|
|
|
1013
1061
|
"PrimeQwen3RendererConfig",
|
|
1014
1062
|
"Qwen35RendererConfig",
|
|
1015
1063
|
"Qwen36RendererConfig",
|
|
1064
|
+
"Qwen38RendererConfig",
|
|
1016
1065
|
"Qwen3RendererConfig",
|
|
1017
1066
|
"Qwen3VLRendererConfig",
|
|
1018
1067
|
"RendererConfig",
|
|
@@ -102,6 +102,9 @@ _ENABLE_THINKING_DEFAULTS: dict[str, bool] = {
|
|
|
102
102
|
"Qwen/Qwen3.5-397B-A17B": True,
|
|
103
103
|
# Qwen3.6 extends the Qwen3.5 template; same big-size polarity.
|
|
104
104
|
"Qwen/Qwen3.6-35B-A3B": True,
|
|
105
|
+
# Qwen3.8 keeps thinking enabled by default.
|
|
106
|
+
"Qwen/Qwen3.8-27B": True,
|
|
107
|
+
"Qwen/Qwen3.8-Flash-Next": True,
|
|
105
108
|
}
|
|
106
109
|
|
|
107
110
|
|
|
@@ -305,6 +308,41 @@ class Qwen35Renderer:
|
|
|
305
308
|
return i
|
|
306
309
|
return len(messages)
|
|
307
310
|
|
|
311
|
+
def _reasoning_instructions(self) -> str:
|
|
312
|
+
"""Optional synthetic system instruction for reasoning control.
|
|
313
|
+
|
|
314
|
+
Qwen3.5 and Qwen3.6 do not inject one. Newer family members can
|
|
315
|
+
override this hook without duplicating the full render path.
|
|
316
|
+
"""
|
|
317
|
+
return ""
|
|
318
|
+
|
|
319
|
+
def _omit_empty_system_message(self) -> bool:
|
|
320
|
+
"""Whether an empty caller-provided system message emits no tokens."""
|
|
321
|
+
return False
|
|
322
|
+
|
|
323
|
+
@staticmethod
|
|
324
|
+
def _extract_assistant_parts(msg: Message, content: str) -> tuple[str, str]:
|
|
325
|
+
"""Return ``(reasoning_content, visible_content)`` for an assistant.
|
|
326
|
+
|
|
327
|
+
Qwen3.5 and Qwen3.6 accept the legacy inline
|
|
328
|
+
``<think>...</think>content`` shape when ``reasoning_content`` is
|
|
329
|
+
absent. Qwen3.8 removes that compatibility branch and overrides this
|
|
330
|
+
hook to keep inline markers in visible content.
|
|
331
|
+
"""
|
|
332
|
+
reasoning_content = ""
|
|
333
|
+
if isinstance(msg.get("reasoning_content"), str):
|
|
334
|
+
reasoning_content = msg["reasoning_content"]
|
|
335
|
+
elif "</think>" in content:
|
|
336
|
+
before_think_end, after_think_end = content.split("</think>", 1)
|
|
337
|
+
if "<think>" in before_think_end:
|
|
338
|
+
reasoning_content = before_think_end.split("<think>")[-1].lstrip("\n")
|
|
339
|
+
else:
|
|
340
|
+
reasoning_content = before_think_end.lstrip("\n")
|
|
341
|
+
reasoning_content = reasoning_content.rstrip("\n")
|
|
342
|
+
content = after_think_end.lstrip("\n")
|
|
343
|
+
|
|
344
|
+
return reasoning_content.strip(), content
|
|
345
|
+
|
|
308
346
|
# ------------------------------------------------------------------
|
|
309
347
|
# Core render method
|
|
310
348
|
# ------------------------------------------------------------------
|
|
@@ -466,6 +504,7 @@ class Qwen35Renderer:
|
|
|
466
504
|
|
|
467
505
|
# ── 1. System message + optional tools ──────────────────────
|
|
468
506
|
first_is_system = messages[0].get("role") == "system"
|
|
507
|
+
reasoning_instructions = self._reasoning_instructions()
|
|
469
508
|
|
|
470
509
|
if tools:
|
|
471
510
|
# System message index for attribution
|
|
@@ -477,10 +516,10 @@ class Qwen35Renderer:
|
|
|
477
516
|
# JSON tool specs — is scaffold. The tools dict is
|
|
478
517
|
# recoverable from the ``tools`` argument; don't re-attribute
|
|
479
518
|
# its embedded JSON as message body.
|
|
480
|
-
segments: list[tuple[str, bool]] = [
|
|
481
|
-
|
|
482
|
-
(
|
|
483
|
-
|
|
519
|
+
segments: list[tuple[str, bool]] = [("system\n", False)]
|
|
520
|
+
if reasoning_instructions:
|
|
521
|
+
segments.append((reasoning_instructions + "\n\n", False))
|
|
522
|
+
segments.append((_TOOLS_HEADER, False))
|
|
484
523
|
for tool in tools:
|
|
485
524
|
segments.append(("\n" + json.dumps(tool, ensure_ascii=False), False))
|
|
486
525
|
segments.append((_TOOLS_FOOTER, False))
|
|
@@ -495,13 +534,33 @@ class Qwen35Renderer:
|
|
|
495
534
|
emit_text("\n", sys_idx, is_sampled=False, is_content=False)
|
|
496
535
|
elif first_is_system:
|
|
497
536
|
sys_content = self._render_content(messages[0].get("content")).strip()
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
537
|
+
if (
|
|
538
|
+
sys_content
|
|
539
|
+
or reasoning_instructions
|
|
540
|
+
or not self._omit_empty_system_message()
|
|
541
|
+
):
|
|
542
|
+
emit_special(self._im_start, 0, is_sampled=False, is_content=False)
|
|
543
|
+
sys_segments: list[tuple[str, bool]] = [("system\n", False)]
|
|
544
|
+
if reasoning_instructions:
|
|
545
|
+
separator = "\n\n" if sys_content else ""
|
|
546
|
+
sys_segments.append((reasoning_instructions + separator, False))
|
|
547
|
+
if sys_content:
|
|
548
|
+
sys_segments.append((sys_content, True))
|
|
549
|
+
emit_text_segments(sys_segments, 0, is_sampled=False)
|
|
550
|
+
emit_special(self._im_end, 0, is_sampled=False, is_content=False)
|
|
551
|
+
emit_text("\n", 0, is_sampled=False, is_content=False)
|
|
552
|
+
elif reasoning_instructions:
|
|
553
|
+
# Qwen3.8 injects reasoning guidance even when the caller did
|
|
554
|
+
# not provide a system message. It is renderer scaffold, so use
|
|
555
|
+
# the synthetic ``-1`` attribution bucket.
|
|
556
|
+
emit_special(self._im_start, -1, is_sampled=False, is_content=False)
|
|
557
|
+
emit_text_segments(
|
|
558
|
+
[("system\n", False), (reasoning_instructions, False)],
|
|
559
|
+
-1,
|
|
560
|
+
is_sampled=False,
|
|
561
|
+
)
|
|
562
|
+
emit_special(self._im_end, -1, is_sampled=False, is_content=False)
|
|
563
|
+
emit_text("\n", -1, is_sampled=False, is_content=False)
|
|
505
564
|
|
|
506
565
|
# ── 2. Compute last_query_index ─────────────────────────────
|
|
507
566
|
last_qi = self._last_query_index(messages)
|
|
@@ -932,22 +991,7 @@ class Qwen35Renderer:
|
|
|
932
991
|
emit_ids,
|
|
933
992
|
emit_text_segments,
|
|
934
993
|
) -> None:
|
|
935
|
-
|
|
936
|
-
reasoning_content = ""
|
|
937
|
-
if isinstance(msg.get("reasoning_content"), str):
|
|
938
|
-
reasoning_content = msg["reasoning_content"]
|
|
939
|
-
elif "</think>" in content:
|
|
940
|
-
# Split on </think> to separate reasoning from content
|
|
941
|
-
before_think_end, after_think_end = content.split("</think>", 1)
|
|
942
|
-
# Extract text after <think> (if present)
|
|
943
|
-
if "<think>" in before_think_end:
|
|
944
|
-
reasoning_content = before_think_end.split("<think>")[-1].lstrip("\n")
|
|
945
|
-
else:
|
|
946
|
-
reasoning_content = before_think_end.lstrip("\n")
|
|
947
|
-
reasoning_content = reasoning_content.rstrip("\n")
|
|
948
|
-
content = after_think_end.lstrip("\n")
|
|
949
|
-
|
|
950
|
-
reasoning_content = reasoning_content.strip()
|
|
994
|
+
reasoning_content, content = self._extract_assistant_parts(msg, content)
|
|
951
995
|
|
|
952
996
|
# ``<|im_start|>assistant\n`` is template-injected scaffolding —
|
|
953
997
|
# at inference the chat template emits these as the generation
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Qwen3.8 Renderer — mirrors the Qwen3.8 Jinja chat template.
|
|
2
|
+
|
|
3
|
+
Qwen3.8 retains Qwen3.6's multimodal message grammar and JSON-safe tool
|
|
4
|
+
argument serialization, with three template changes:
|
|
5
|
+
|
|
6
|
+
- ``reasoning_effort`` can be ``xhigh`` (default), ``medium``, or ``low``;
|
|
7
|
+
xhigh/low inject a matching instruction into the system prompt.
|
|
8
|
+
- ``preserve_thinking`` defaults to true, so historical reasoning blocks are
|
|
9
|
+
retained unless explicitly disabled.
|
|
10
|
+
- Legacy inline ``<think>...</think>`` text is no longer split out of
|
|
11
|
+
assistant ``content``; only ``reasoning_content`` populates the reasoning
|
|
12
|
+
block.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from renderers.base import Message
|
|
18
|
+
from renderers.configs import Qwen38RendererConfig
|
|
19
|
+
from renderers.qwen35 import Qwen35Renderer
|
|
20
|
+
from renderers.qwen36 import Qwen36Renderer
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
_REASONING_INSTRUCTIONS = {
|
|
24
|
+
"xhigh": (
|
|
25
|
+
"Reasoning effort is set to xhigh. Please think carefully through the "
|
|
26
|
+
"task, validate key assumptions, consider plausible alternatives, and "
|
|
27
|
+
"prioritize correctness, consistency, and clarity in the final answer."
|
|
28
|
+
),
|
|
29
|
+
"medium": "",
|
|
30
|
+
"low": (
|
|
31
|
+
"Reasoning effort is set to low. Keep your thinking brief and focused, "
|
|
32
|
+
"moving directly to the conclusion without unnecessary elaboration."
|
|
33
|
+
),
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Qwen38Renderer(Qwen36Renderer):
|
|
38
|
+
"""Deterministic message-to-token renderer for Qwen3.8 models."""
|
|
39
|
+
|
|
40
|
+
config: Qwen38RendererConfig
|
|
41
|
+
_config_cls = Qwen38RendererConfig
|
|
42
|
+
|
|
43
|
+
def _reasoning_instructions(self) -> str:
|
|
44
|
+
if not self.config.enable_thinking:
|
|
45
|
+
return ""
|
|
46
|
+
return _REASONING_INSTRUCTIONS[self.config.reasoning_effort]
|
|
47
|
+
|
|
48
|
+
def _omit_empty_system_message(self) -> bool:
|
|
49
|
+
return True
|
|
50
|
+
|
|
51
|
+
@staticmethod
|
|
52
|
+
def _last_query_index(messages: list[Message]) -> int:
|
|
53
|
+
last_query_index = Qwen35Renderer._last_query_index(messages)
|
|
54
|
+
if last_query_index == len(messages):
|
|
55
|
+
raise ValueError("No user query found in messages.")
|
|
56
|
+
return last_query_index
|
|
57
|
+
|
|
58
|
+
@staticmethod
|
|
59
|
+
def _extract_assistant_parts(msg: Message, content: str) -> tuple[str, str]:
|
|
60
|
+
reasoning_content = msg.get("reasoning_content")
|
|
61
|
+
if not isinstance(reasoning_content, str):
|
|
62
|
+
reasoning_content = ""
|
|
63
|
+
return reasoning_content.strip(), content
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
__all__ = ["Qwen38Renderer"]
|
|
@@ -22,6 +22,8 @@ RENDERER_MODELS = [
|
|
|
22
22
|
("PrimeIntellect/Qwen3-0.6B", "auto"),
|
|
23
23
|
("Qwen/Qwen3.5-9B", "auto"),
|
|
24
24
|
("Qwen/Qwen3.6-35B-A3B", "auto"),
|
|
25
|
+
("Qwen/Qwen3.8-27B", "auto"),
|
|
26
|
+
("Qwen/Qwen3.8-Flash-Next", "auto"),
|
|
25
27
|
("Qwen/Qwen3-VL-4B-Instruct", "auto"),
|
|
26
28
|
("google/gemma-4-31B-it", "auto"),
|
|
27
29
|
("zai-org/GLM-5", "auto"),
|
|
@@ -26,6 +26,8 @@ _BRIDGE_MODELS = [
|
|
|
26
26
|
("PrimeIntellect/Qwen3-0.6B", "auto"),
|
|
27
27
|
("Qwen/Qwen3.5-9B", "auto"),
|
|
28
28
|
("Qwen/Qwen3.6-35B-A3B", "auto"),
|
|
29
|
+
("Qwen/Qwen3.8-27B", "auto"),
|
|
30
|
+
("Qwen/Qwen3.8-Flash-Next", "auto"),
|
|
29
31
|
("google/gemma-4-31B-it", "auto"),
|
|
30
32
|
("thinkingmachines/Inkling", "auto"),
|
|
31
33
|
("thinkingmachines/Inkling-Small", "auto"),
|
|
@@ -384,8 +384,10 @@ def test_generate_threads_prompt_attribution_through_prebuilt_prompt_path():
|
|
|
384
384
|
[
|
|
385
385
|
("Qwen/Qwen3-VL-4B-Instruct", "renderers.qwen3_vl:Qwen3VLRenderer"),
|
|
386
386
|
("Qwen/Qwen3.5-2B", "renderers.qwen35:Qwen35Renderer"),
|
|
387
|
+
("Qwen/Qwen3.8-27B", "renderers.qwen38:Qwen38Renderer"),
|
|
388
|
+
("Qwen/Qwen3.8-Flash-Next", "renderers.qwen38:Qwen38Renderer"),
|
|
387
389
|
],
|
|
388
|
-
ids=["qwen3_vl", "qwen35"],
|
|
390
|
+
ids=["qwen3_vl", "qwen35", "qwen38", "qwen38_flash_next"],
|
|
389
391
|
)
|
|
390
392
|
def test_generate_serializes_multimodal_features_for_qwen_vl_family(
|
|
391
393
|
model_id, renderer_class_path
|
|
@@ -32,6 +32,7 @@ from renderers.configs import (
|
|
|
32
32
|
Qwen3RendererConfig,
|
|
33
33
|
Qwen35RendererConfig,
|
|
34
34
|
Qwen36RendererConfig,
|
|
35
|
+
Qwen38RendererConfig,
|
|
35
36
|
)
|
|
36
37
|
|
|
37
38
|
# One representative model per affected renderer family, each with
|
|
@@ -40,6 +41,14 @@ _MODELS = [
|
|
|
40
41
|
("Qwen/Qwen3-8B", Qwen3RendererConfig(enable_thinking=False)),
|
|
41
42
|
("Qwen/Qwen3.5-9B", Qwen35RendererConfig(enable_thinking=False)),
|
|
42
43
|
("Qwen/Qwen3.6-35B-A3B", Qwen36RendererConfig(enable_thinking=False)),
|
|
44
|
+
(
|
|
45
|
+
"Qwen/Qwen3.8-27B",
|
|
46
|
+
Qwen38RendererConfig(enable_thinking=False),
|
|
47
|
+
),
|
|
48
|
+
(
|
|
49
|
+
"Qwen/Qwen3.8-Flash-Next",
|
|
50
|
+
Qwen38RendererConfig(enable_thinking=False),
|
|
51
|
+
),
|
|
43
52
|
(
|
|
44
53
|
"google/gemma-4-26B-A4B-it",
|
|
45
54
|
Gemma4RendererConfig(enable_thinking=False),
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""Focused Qwen3.8 registration and upstream-template parity coverage."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from functools import lru_cache
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
from pydantic import TypeAdapter
|
|
9
|
+
|
|
10
|
+
from renderers import (
|
|
11
|
+
Qwen38Renderer,
|
|
12
|
+
Qwen38RendererConfig,
|
|
13
|
+
RendererConfig,
|
|
14
|
+
create_renderer,
|
|
15
|
+
)
|
|
16
|
+
from renderers.base import MODEL_RENDERER_MAP, MULTIMODAL_MODELS, load_tokenizer
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
TOOLS = [
|
|
20
|
+
{
|
|
21
|
+
"type": "function",
|
|
22
|
+
"function": {
|
|
23
|
+
"name": "get_weather",
|
|
24
|
+
"description": "Get weather for a city",
|
|
25
|
+
"parameters": {
|
|
26
|
+
"type": "object",
|
|
27
|
+
"properties": {"city": {"type": "string"}},
|
|
28
|
+
"required": ["city"],
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
QWEN38_MODELS = ["Qwen/Qwen3.8-27B", "Qwen/Qwen3.8-Flash-Next"]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@lru_cache(maxsize=None)
|
|
38
|
+
def _qwen38(model_name):
|
|
39
|
+
tokenizer = load_tokenizer(model_name)
|
|
40
|
+
return tokenizer, create_renderer(tokenizer)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _expected(tokenizer, messages, **kwargs):
|
|
44
|
+
result = tokenizer.apply_chat_template(
|
|
45
|
+
messages,
|
|
46
|
+
tokenize=True,
|
|
47
|
+
return_dict=False,
|
|
48
|
+
**kwargs,
|
|
49
|
+
)
|
|
50
|
+
return list(result)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@pytest.mark.parametrize("qwen38_model", QWEN38_MODELS)
|
|
54
|
+
def test_qwen38_is_registered_with_native_defaults(qwen38_model):
|
|
55
|
+
tokenizer, renderer = _qwen38(qwen38_model)
|
|
56
|
+
|
|
57
|
+
assert tokenizer.name_or_path == qwen38_model
|
|
58
|
+
assert MODEL_RENDERER_MAP[tokenizer.name_or_path] == "qwen3.8"
|
|
59
|
+
assert MULTIMODAL_MODELS[tokenizer.name_or_path] == {"image"}
|
|
60
|
+
assert isinstance(renderer, Qwen38Renderer)
|
|
61
|
+
assert renderer.config.enable_thinking is True
|
|
62
|
+
assert renderer.config.reasoning_effort == "xhigh"
|
|
63
|
+
assert renderer.config.preserve_thinking is True
|
|
64
|
+
assert renderer.effective_thinking_retention == "all"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_qwen38_config_discriminator():
|
|
68
|
+
parsed = TypeAdapter(RendererConfig).validate_python(
|
|
69
|
+
{
|
|
70
|
+
"name": "qwen3.8",
|
|
71
|
+
"reasoning_effort": "low",
|
|
72
|
+
"preserve_thinking": False,
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
assert isinstance(parsed, Qwen38RendererConfig)
|
|
77
|
+
assert parsed.reasoning_effort == "low"
|
|
78
|
+
assert parsed.preserve_thinking is False
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@pytest.mark.parametrize(
|
|
82
|
+
"config_kwargs",
|
|
83
|
+
[
|
|
84
|
+
pytest.param({}, id="defaults"),
|
|
85
|
+
pytest.param({"reasoning_effort": "xhigh"}, id="xhigh"),
|
|
86
|
+
pytest.param({"reasoning_effort": "medium"}, id="medium"),
|
|
87
|
+
pytest.param({"reasoning_effort": "low"}, id="low"),
|
|
88
|
+
pytest.param({"enable_thinking": False}, id="thinking-disabled"),
|
|
89
|
+
pytest.param({"preserve_thinking": False}, id="drop-history-thinking"),
|
|
90
|
+
],
|
|
91
|
+
)
|
|
92
|
+
@pytest.mark.parametrize("qwen38_model", QWEN38_MODELS)
|
|
93
|
+
def test_qwen38_text_and_tool_parity(config_kwargs, qwen38_model):
|
|
94
|
+
tokenizer, _ = _qwen38(qwen38_model)
|
|
95
|
+
renderer = Qwen38Renderer(tokenizer, Qwen38RendererConfig(**config_kwargs))
|
|
96
|
+
cases = [
|
|
97
|
+
(
|
|
98
|
+
[
|
|
99
|
+
{"role": "system", "content": "Be concise."},
|
|
100
|
+
{"role": "user", "content": "Hello."},
|
|
101
|
+
],
|
|
102
|
+
{"add_generation_prompt": True},
|
|
103
|
+
),
|
|
104
|
+
(
|
|
105
|
+
[
|
|
106
|
+
{"role": "system", "content": ""},
|
|
107
|
+
{"role": "user", "content": "Hello."},
|
|
108
|
+
],
|
|
109
|
+
{"add_generation_prompt": True},
|
|
110
|
+
),
|
|
111
|
+
(
|
|
112
|
+
[
|
|
113
|
+
{"role": "user", "content": "First question."},
|
|
114
|
+
{
|
|
115
|
+
"role": "assistant",
|
|
116
|
+
"reasoning_content": "First thought.",
|
|
117
|
+
"content": "First answer.",
|
|
118
|
+
},
|
|
119
|
+
{"role": "user", "content": "Second question."},
|
|
120
|
+
{
|
|
121
|
+
"role": "assistant",
|
|
122
|
+
"reasoning_content": "Second thought.",
|
|
123
|
+
"content": "Second answer.",
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
{},
|
|
127
|
+
),
|
|
128
|
+
(
|
|
129
|
+
[
|
|
130
|
+
{"role": "user", "content": "Weather in Paris?"},
|
|
131
|
+
{
|
|
132
|
+
"role": "assistant",
|
|
133
|
+
"content": "",
|
|
134
|
+
"tool_calls": [
|
|
135
|
+
{
|
|
136
|
+
"function": {
|
|
137
|
+
"name": "get_weather",
|
|
138
|
+
"arguments": {"city": "Paris", "fresh": True},
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
],
|
|
142
|
+
},
|
|
143
|
+
{"role": "tool", "content": '{"temperature": 20}'},
|
|
144
|
+
],
|
|
145
|
+
{"tools": TOOLS, "add_generation_prompt": True},
|
|
146
|
+
),
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
for messages, render_kwargs in cases:
|
|
150
|
+
expected = _expected(
|
|
151
|
+
tokenizer,
|
|
152
|
+
messages,
|
|
153
|
+
**config_kwargs,
|
|
154
|
+
**render_kwargs,
|
|
155
|
+
)
|
|
156
|
+
assert renderer.render_ids(messages, **render_kwargs) == expected
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@pytest.mark.parametrize("qwen38_model", QWEN38_MODELS)
|
|
160
|
+
def test_qwen38_keeps_inline_think_markup_in_visible_content(qwen38_model):
|
|
161
|
+
tokenizer, renderer = _qwen38(qwen38_model)
|
|
162
|
+
messages = [
|
|
163
|
+
{"role": "user", "content": "Echo this."},
|
|
164
|
+
{
|
|
165
|
+
"role": "assistant",
|
|
166
|
+
"content": "<think>literal markup</think>visible content",
|
|
167
|
+
},
|
|
168
|
+
]
|
|
169
|
+
|
|
170
|
+
assert renderer.render_ids(messages) == _expected(tokenizer, messages)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@pytest.mark.parametrize("qwen38_model", QWEN38_MODELS)
|
|
174
|
+
def test_qwen38_requires_a_real_user_query(qwen38_model):
|
|
175
|
+
_, renderer = _qwen38(qwen38_model)
|
|
176
|
+
|
|
177
|
+
with pytest.raises(ValueError, match="No user query found"):
|
|
178
|
+
renderer.render_ids([{"role": "system", "content": "System only."}])
|
|
@@ -47,6 +47,8 @@ _RENDERER_MODELS = [
|
|
|
47
47
|
("Qwen/Qwen3-8B", "auto"),
|
|
48
48
|
("Qwen/Qwen3.5-9B", "auto"),
|
|
49
49
|
("Qwen/Qwen3.6-35B-A3B", "auto"),
|
|
50
|
+
("Qwen/Qwen3.8-27B", "auto"),
|
|
51
|
+
("Qwen/Qwen3.8-Flash-Next", "auto"),
|
|
50
52
|
("google/gemma-4-31B-it", "auto"),
|
|
51
53
|
("zai-org/GLM-5", "auto"),
|
|
52
54
|
("zai-org/GLM-5.1", "auto"),
|
|
@@ -93,7 +95,7 @@ _KWARG_VALUES: dict[str, list[Any]] = {
|
|
|
93
95
|
# gpt-oss accepts low/medium/high; Hy3 accepts no_think/low/high. The
|
|
94
96
|
# union is listed here and the matrix builder drops values a given
|
|
95
97
|
# renderer's typed config rejects (see ``_value_valid_for``).
|
|
96
|
-
"reasoning_effort": ["no_think", "low", "medium", "high"],
|
|
98
|
+
"reasoning_effort": ["no_think", "low", "medium", "high", "xhigh"],
|
|
97
99
|
# Hy3 — keep <think>{reasoning}</think> on historical assistant turns
|
|
98
100
|
# (True) vs collapse past-cycle reasoning to <think></think> (False).
|
|
99
101
|
"preserved_thinking": [True, False],
|
|
@@ -131,10 +133,11 @@ _KWARG_VALUES: dict[str, list[Any]] = {
|
|
|
131
133
|
# passthrough mode. The renderer paths diverge significantly under
|
|
132
134
|
# this flag, so both values are exercised.
|
|
133
135
|
"render_assistant_messages_raw": [True, False],
|
|
134
|
-
# Qwen3.5 / Qwen3.6 / Qwen3-VL — when True, prefix each
|
|
135
|
-
# video placeholder with ``Picture N: `` / ``Video N: ``.
|
|
136
|
+
# Qwen3.5 / Qwen3.6 / Qwen3.8 / Qwen3-VL — when True, prefix each
|
|
137
|
+
# image / video placeholder with ``Picture N: `` / ``Video N: ``.
|
|
136
138
|
"add_vision_id": [True, False],
|
|
137
|
-
# Qwen3.6 — keep historical think blocks before the last real
|
|
139
|
+
# Qwen3.6 / Qwen3.8 — keep historical think blocks before the last real
|
|
140
|
+
# user query. Qwen3.8 defaults this to True.
|
|
138
141
|
"preserve_thinking": [True, False],
|
|
139
142
|
# gpt-oss — pin to a fixed date so the renderer's preamble matches
|
|
140
143
|
# the harmony oracle built with the same date. The default
|
|
@@ -32,6 +32,8 @@ _ROUNDTRIP_MODELS = [
|
|
|
32
32
|
("PrimeIntellect/Qwen3-0.6B", "auto"),
|
|
33
33
|
("Qwen/Qwen3.5-9B", "auto"),
|
|
34
34
|
("Qwen/Qwen3.6-35B-A3B", "auto"),
|
|
35
|
+
("Qwen/Qwen3.8-27B", "auto"),
|
|
36
|
+
("Qwen/Qwen3.8-Flash-Next", "auto"),
|
|
35
37
|
("Qwen/Qwen3-VL-4B-Instruct", "auto"),
|
|
36
38
|
("google/gemma-4-31B-it", "auto"),
|
|
37
39
|
("thinkingmachines/Inkling", "auto"),
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/examples/sglang/multiturn_generate_sglang.py
RENAMED
|
File without changes
|
|
File without changes
|
{renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/examples/tinker/multiturn_generate_tinker.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|