renderers 0.1.10.dev13__tar.gz → 0.1.10.dev15__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.10.dev15}/.github/workflows/publish-dev.yml +1 -1
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/.github/workflows/publish.yml +1 -1
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/PKG-INFO +3 -3
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/README.md +1 -1
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/docs/renderer-config.md +2 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/renderers/__init__.py +4 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/renderers/_version.py +2 -2
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/renderers/base.py +10 -4
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/renderers/configs.py +49 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/renderers/qwen35.py +70 -27
- renderers-0.1.10.dev15/renderers/qwen38.py +66 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/tests/conftest.py +1 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/tests/test_bridge.py +1 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/tests/test_client.py +2 -1
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/tests/test_disabled_thinking_stability.py +5 -0
- renderers-0.1.10.dev15/tests/test_qwen38.py +172 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/tests/test_renderer_config_parity.py +6 -4
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/tests/test_roundtrip.py +1 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/.github/workflows/style.yml +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/.github/workflows/test.yml +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/.gitignore +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/.pre-commit-config.yaml +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/LICENSE +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/examples/README.md +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/examples/sglang/multiturn_generate_sglang.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/examples/sglang/online_multiturn_sglang.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/examples/tinker/multiturn_generate_tinker.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/examples/transformers/multiturn_generate_transformers.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/examples/vllm/multiturn_generate_vllm.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/pyproject.toml +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/renderers/client.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/renderers/deepseek_r1.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/renderers/deepseek_v3.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/renderers/default.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/renderers/gemma4.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/renderers/glm45.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/renderers/glm5.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/renderers/gpt_oss.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/renderers/hy3.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/renderers/inkling.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/renderers/kimi_k2.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/renderers/kimi_k25.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/renderers/laguna_s21.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/renderers/laguna_xs2.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/renderers/llama_3.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/renderers/minimax_m2.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/renderers/nemotron3.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/renderers/parsers.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/renderers/parsing.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/renderers/prime_qwen3.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/renderers/qwen3.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/renderers/qwen36.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/renderers/qwen3_vl.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/tests/test_build_helpers.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/tests/test_deepseek_r1.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/tests/test_gemma4.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/tests/test_glm_tool_name_validation.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/tests/test_gpt_oss_harmony_parity.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/tests/test_hy3.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/tests/test_incremental.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/tests/test_inkling.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/tests/test_is_content.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/tests/test_kimi_k25_tool_schema.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/tests/test_laguna_m1.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/tests/test_laguna_s21.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/tests/test_laguna_xs21.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/tests/test_llama_3.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/tests/test_load_tokenizer.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/tests/test_message_indices.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/tests/test_message_tool_names.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/tests/test_multimodal.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/tests/test_nemotron3_parity.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/tests/test_nemotron3_ultra.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/tests/test_parse_response.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/tests/test_parse_response_robustness.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/tests/test_parsers.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/tests/test_preserve_thinking.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/tests/test_prime_qwen3_parity.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/tests/test_qwen35_size_coverage.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/tests/test_render_ids.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/tests/test_renderer_config.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/tests/test_sampled_mask.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/tests/test_tokens_per_message.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/tests/test_tool_arg_type_preservation.py +0 -0
- {renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/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.10.
|
|
3
|
+
Version: 0.1.10.dev15
|
|
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.10.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 1, 10, '
|
|
21
|
+
__version__ = version = '0.1.10.dev15'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 10, 'dev15')
|
|
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,8 @@ 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",
|
|
1034
1036
|
# Qwen3-VL.
|
|
1035
1037
|
"Qwen/Qwen3-VL-4B-Instruct": "qwen3-vl",
|
|
1036
1038
|
"Qwen/Qwen3-VL-8B-Instruct": "qwen3-vl",
|
|
@@ -1136,6 +1138,8 @@ MULTIMODAL_MODELS: dict[str, set[str]] = {
|
|
|
1136
1138
|
# Qwen3.6 extends Qwen3.5's chat template; same VL bits, only
|
|
1137
1139
|
# tool-call argument serialization differs.
|
|
1138
1140
|
"Qwen/Qwen3.6-35B-A3B": {"image"},
|
|
1141
|
+
# Qwen3.8 adds reasoning-effort control and preserves thinking by default.
|
|
1142
|
+
"Qwen/Qwen3.8-27B": {"image"},
|
|
1139
1143
|
# Kimi K2.5 / K2.6 are unified VLMs (HF tag ``image-text-to-text``)
|
|
1140
1144
|
# with custom processor (``KimiK25Processor`` + ``KimiK25VisionProcessor``).
|
|
1141
1145
|
# Vision wrap is different from Qwen-VL:
|
|
@@ -1367,6 +1371,7 @@ def _populate_registry():
|
|
|
1367
1371
|
from renderers.qwen3_vl import Qwen3VLRenderer
|
|
1368
1372
|
from renderers.qwen35 import Qwen35Renderer
|
|
1369
1373
|
from renderers.qwen36 import Qwen36Renderer
|
|
1374
|
+
from renderers.qwen38 import Qwen38Renderer
|
|
1370
1375
|
|
|
1371
1376
|
RENDERER_REGISTRY.update(
|
|
1372
1377
|
{
|
|
@@ -1377,6 +1382,7 @@ def _populate_registry():
|
|
|
1377
1382
|
"gemma4": Gemma4Renderer,
|
|
1378
1383
|
"qwen3.5": Qwen35Renderer,
|
|
1379
1384
|
"qwen3.6": Qwen36Renderer,
|
|
1385
|
+
"qwen3.8": Qwen38Renderer,
|
|
1380
1386
|
"glm-5": GLM5Renderer,
|
|
1381
1387
|
"glm-5.1": GLM51Renderer,
|
|
1382
1388
|
"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,8 @@ _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,
|
|
105
107
|
}
|
|
106
108
|
|
|
107
109
|
|
|
@@ -305,6 +307,41 @@ class Qwen35Renderer:
|
|
|
305
307
|
return i
|
|
306
308
|
return len(messages)
|
|
307
309
|
|
|
310
|
+
def _reasoning_instructions(self) -> str:
|
|
311
|
+
"""Optional synthetic system instruction for reasoning control.
|
|
312
|
+
|
|
313
|
+
Qwen3.5 and Qwen3.6 do not inject one. Newer family members can
|
|
314
|
+
override this hook without duplicating the full render path.
|
|
315
|
+
"""
|
|
316
|
+
return ""
|
|
317
|
+
|
|
318
|
+
def _omit_empty_system_message(self) -> bool:
|
|
319
|
+
"""Whether an empty caller-provided system message emits no tokens."""
|
|
320
|
+
return False
|
|
321
|
+
|
|
322
|
+
@staticmethod
|
|
323
|
+
def _extract_assistant_parts(msg: Message, content: str) -> tuple[str, str]:
|
|
324
|
+
"""Return ``(reasoning_content, visible_content)`` for an assistant.
|
|
325
|
+
|
|
326
|
+
Qwen3.5 and Qwen3.6 accept the legacy inline
|
|
327
|
+
``<think>...</think>content`` shape when ``reasoning_content`` is
|
|
328
|
+
absent. Qwen3.8 removes that compatibility branch and overrides this
|
|
329
|
+
hook to keep inline markers in visible content.
|
|
330
|
+
"""
|
|
331
|
+
reasoning_content = ""
|
|
332
|
+
if isinstance(msg.get("reasoning_content"), str):
|
|
333
|
+
reasoning_content = msg["reasoning_content"]
|
|
334
|
+
elif "</think>" in content:
|
|
335
|
+
before_think_end, after_think_end = content.split("</think>", 1)
|
|
336
|
+
if "<think>" in before_think_end:
|
|
337
|
+
reasoning_content = before_think_end.split("<think>")[-1].lstrip("\n")
|
|
338
|
+
else:
|
|
339
|
+
reasoning_content = before_think_end.lstrip("\n")
|
|
340
|
+
reasoning_content = reasoning_content.rstrip("\n")
|
|
341
|
+
content = after_think_end.lstrip("\n")
|
|
342
|
+
|
|
343
|
+
return reasoning_content.strip(), content
|
|
344
|
+
|
|
308
345
|
# ------------------------------------------------------------------
|
|
309
346
|
# Core render method
|
|
310
347
|
# ------------------------------------------------------------------
|
|
@@ -466,6 +503,7 @@ class Qwen35Renderer:
|
|
|
466
503
|
|
|
467
504
|
# ── 1. System message + optional tools ──────────────────────
|
|
468
505
|
first_is_system = messages[0].get("role") == "system"
|
|
506
|
+
reasoning_instructions = self._reasoning_instructions()
|
|
469
507
|
|
|
470
508
|
if tools:
|
|
471
509
|
# System message index for attribution
|
|
@@ -477,10 +515,10 @@ class Qwen35Renderer:
|
|
|
477
515
|
# JSON tool specs — is scaffold. The tools dict is
|
|
478
516
|
# recoverable from the ``tools`` argument; don't re-attribute
|
|
479
517
|
# its embedded JSON as message body.
|
|
480
|
-
segments: list[tuple[str, bool]] = [
|
|
481
|
-
|
|
482
|
-
(
|
|
483
|
-
|
|
518
|
+
segments: list[tuple[str, bool]] = [("system\n", False)]
|
|
519
|
+
if reasoning_instructions:
|
|
520
|
+
segments.append((reasoning_instructions + "\n\n", False))
|
|
521
|
+
segments.append((_TOOLS_HEADER, False))
|
|
484
522
|
for tool in tools:
|
|
485
523
|
segments.append(("\n" + json.dumps(tool, ensure_ascii=False), False))
|
|
486
524
|
segments.append((_TOOLS_FOOTER, False))
|
|
@@ -495,13 +533,33 @@ class Qwen35Renderer:
|
|
|
495
533
|
emit_text("\n", sys_idx, is_sampled=False, is_content=False)
|
|
496
534
|
elif first_is_system:
|
|
497
535
|
sys_content = self._render_content(messages[0].get("content")).strip()
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
536
|
+
if (
|
|
537
|
+
sys_content
|
|
538
|
+
or reasoning_instructions
|
|
539
|
+
or not self._omit_empty_system_message()
|
|
540
|
+
):
|
|
541
|
+
emit_special(self._im_start, 0, is_sampled=False, is_content=False)
|
|
542
|
+
sys_segments: list[tuple[str, bool]] = [("system\n", False)]
|
|
543
|
+
if reasoning_instructions:
|
|
544
|
+
separator = "\n\n" if sys_content else ""
|
|
545
|
+
sys_segments.append((reasoning_instructions + separator, False))
|
|
546
|
+
if sys_content:
|
|
547
|
+
sys_segments.append((sys_content, True))
|
|
548
|
+
emit_text_segments(sys_segments, 0, is_sampled=False)
|
|
549
|
+
emit_special(self._im_end, 0, is_sampled=False, is_content=False)
|
|
550
|
+
emit_text("\n", 0, is_sampled=False, is_content=False)
|
|
551
|
+
elif reasoning_instructions:
|
|
552
|
+
# Qwen3.8 injects reasoning guidance even when the caller did
|
|
553
|
+
# not provide a system message. It is renderer scaffold, so use
|
|
554
|
+
# the synthetic ``-1`` attribution bucket.
|
|
555
|
+
emit_special(self._im_start, -1, is_sampled=False, is_content=False)
|
|
556
|
+
emit_text_segments(
|
|
557
|
+
[("system\n", False), (reasoning_instructions, False)],
|
|
558
|
+
-1,
|
|
559
|
+
is_sampled=False,
|
|
560
|
+
)
|
|
561
|
+
emit_special(self._im_end, -1, is_sampled=False, is_content=False)
|
|
562
|
+
emit_text("\n", -1, is_sampled=False, is_content=False)
|
|
505
563
|
|
|
506
564
|
# ── 2. Compute last_query_index ─────────────────────────────
|
|
507
565
|
last_qi = self._last_query_index(messages)
|
|
@@ -932,22 +990,7 @@ class Qwen35Renderer:
|
|
|
932
990
|
emit_ids,
|
|
933
991
|
emit_text_segments,
|
|
934
992
|
) -> 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()
|
|
993
|
+
reasoning_content, content = self._extract_assistant_parts(msg, content)
|
|
951
994
|
|
|
952
995
|
# ``<|im_start|>assistant\n`` is template-injected scaffolding —
|
|
953
996
|
# 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,7 @@ 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"),
|
|
25
26
|
("Qwen/Qwen3-VL-4B-Instruct", "auto"),
|
|
26
27
|
("google/gemma-4-31B-it", "auto"),
|
|
27
28
|
("zai-org/GLM-5", "auto"),
|
|
@@ -26,6 +26,7 @@ _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"),
|
|
29
30
|
("google/gemma-4-31B-it", "auto"),
|
|
30
31
|
("thinkingmachines/Inkling", "auto"),
|
|
31
32
|
("thinkingmachines/Inkling-Small", "auto"),
|
|
@@ -384,8 +384,9 @@ 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"),
|
|
387
388
|
],
|
|
388
|
-
ids=["qwen3_vl", "qwen35"],
|
|
389
|
+
ids=["qwen3_vl", "qwen35", "qwen38"],
|
|
389
390
|
)
|
|
390
391
|
def test_generate_serializes_multimodal_features_for_qwen_vl_family(
|
|
391
392
|
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,10 @@ _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
|
+
),
|
|
43
48
|
(
|
|
44
49
|
"google/gemma-4-26B-A4B-it",
|
|
45
50
|
Gemma4RendererConfig(enable_thinking=False),
|
|
@@ -0,0 +1,172 @@
|
|
|
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
|
+
|
|
35
|
+
@lru_cache(maxsize=1)
|
|
36
|
+
def _qwen38():
|
|
37
|
+
tokenizer = load_tokenizer("Qwen/Qwen3.8-27B")
|
|
38
|
+
return tokenizer, create_renderer(tokenizer)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _expected(tokenizer, messages, **kwargs):
|
|
42
|
+
result = tokenizer.apply_chat_template(
|
|
43
|
+
messages,
|
|
44
|
+
tokenize=True,
|
|
45
|
+
return_dict=False,
|
|
46
|
+
**kwargs,
|
|
47
|
+
)
|
|
48
|
+
return list(result)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_qwen38_is_registered_with_native_defaults():
|
|
52
|
+
tokenizer, renderer = _qwen38()
|
|
53
|
+
|
|
54
|
+
assert tokenizer.name_or_path == "Qwen/Qwen3.8-27B"
|
|
55
|
+
assert MODEL_RENDERER_MAP[tokenizer.name_or_path] == "qwen3.8"
|
|
56
|
+
assert MULTIMODAL_MODELS[tokenizer.name_or_path] == {"image"}
|
|
57
|
+
assert isinstance(renderer, Qwen38Renderer)
|
|
58
|
+
assert renderer.config.enable_thinking is True
|
|
59
|
+
assert renderer.config.reasoning_effort == "xhigh"
|
|
60
|
+
assert renderer.config.preserve_thinking is True
|
|
61
|
+
assert renderer.effective_thinking_retention == "all"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_qwen38_config_discriminator():
|
|
65
|
+
parsed = TypeAdapter(RendererConfig).validate_python(
|
|
66
|
+
{
|
|
67
|
+
"name": "qwen3.8",
|
|
68
|
+
"reasoning_effort": "low",
|
|
69
|
+
"preserve_thinking": False,
|
|
70
|
+
}
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
assert isinstance(parsed, Qwen38RendererConfig)
|
|
74
|
+
assert parsed.reasoning_effort == "low"
|
|
75
|
+
assert parsed.preserve_thinking is False
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@pytest.mark.parametrize(
|
|
79
|
+
"config_kwargs",
|
|
80
|
+
[
|
|
81
|
+
pytest.param({}, id="defaults"),
|
|
82
|
+
pytest.param({"reasoning_effort": "xhigh"}, id="xhigh"),
|
|
83
|
+
pytest.param({"reasoning_effort": "medium"}, id="medium"),
|
|
84
|
+
pytest.param({"reasoning_effort": "low"}, id="low"),
|
|
85
|
+
pytest.param({"enable_thinking": False}, id="thinking-disabled"),
|
|
86
|
+
pytest.param({"preserve_thinking": False}, id="drop-history-thinking"),
|
|
87
|
+
],
|
|
88
|
+
)
|
|
89
|
+
def test_qwen38_text_and_tool_parity(config_kwargs):
|
|
90
|
+
tokenizer, _ = _qwen38()
|
|
91
|
+
renderer = Qwen38Renderer(tokenizer, Qwen38RendererConfig(**config_kwargs))
|
|
92
|
+
cases = [
|
|
93
|
+
(
|
|
94
|
+
[
|
|
95
|
+
{"role": "system", "content": "Be concise."},
|
|
96
|
+
{"role": "user", "content": "Hello."},
|
|
97
|
+
],
|
|
98
|
+
{"add_generation_prompt": True},
|
|
99
|
+
),
|
|
100
|
+
(
|
|
101
|
+
[
|
|
102
|
+
{"role": "system", "content": ""},
|
|
103
|
+
{"role": "user", "content": "Hello."},
|
|
104
|
+
],
|
|
105
|
+
{"add_generation_prompt": True},
|
|
106
|
+
),
|
|
107
|
+
(
|
|
108
|
+
[
|
|
109
|
+
{"role": "user", "content": "First question."},
|
|
110
|
+
{
|
|
111
|
+
"role": "assistant",
|
|
112
|
+
"reasoning_content": "First thought.",
|
|
113
|
+
"content": "First answer.",
|
|
114
|
+
},
|
|
115
|
+
{"role": "user", "content": "Second question."},
|
|
116
|
+
{
|
|
117
|
+
"role": "assistant",
|
|
118
|
+
"reasoning_content": "Second thought.",
|
|
119
|
+
"content": "Second answer.",
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
{},
|
|
123
|
+
),
|
|
124
|
+
(
|
|
125
|
+
[
|
|
126
|
+
{"role": "user", "content": "Weather in Paris?"},
|
|
127
|
+
{
|
|
128
|
+
"role": "assistant",
|
|
129
|
+
"content": "",
|
|
130
|
+
"tool_calls": [
|
|
131
|
+
{
|
|
132
|
+
"function": {
|
|
133
|
+
"name": "get_weather",
|
|
134
|
+
"arguments": {"city": "Paris", "fresh": True},
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
],
|
|
138
|
+
},
|
|
139
|
+
{"role": "tool", "content": '{"temperature": 20}'},
|
|
140
|
+
],
|
|
141
|
+
{"tools": TOOLS, "add_generation_prompt": True},
|
|
142
|
+
),
|
|
143
|
+
]
|
|
144
|
+
|
|
145
|
+
for messages, render_kwargs in cases:
|
|
146
|
+
expected = _expected(
|
|
147
|
+
tokenizer,
|
|
148
|
+
messages,
|
|
149
|
+
**config_kwargs,
|
|
150
|
+
**render_kwargs,
|
|
151
|
+
)
|
|
152
|
+
assert renderer.render_ids(messages, **render_kwargs) == expected
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def test_qwen38_keeps_inline_think_markup_in_visible_content():
|
|
156
|
+
tokenizer, renderer = _qwen38()
|
|
157
|
+
messages = [
|
|
158
|
+
{"role": "user", "content": "Echo this."},
|
|
159
|
+
{
|
|
160
|
+
"role": "assistant",
|
|
161
|
+
"content": "<think>literal markup</think>visible content",
|
|
162
|
+
},
|
|
163
|
+
]
|
|
164
|
+
|
|
165
|
+
assert renderer.render_ids(messages) == _expected(tokenizer, messages)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def test_qwen38_requires_a_real_user_query():
|
|
169
|
+
_, renderer = _qwen38()
|
|
170
|
+
|
|
171
|
+
with pytest.raises(ValueError, match="No user query found"):
|
|
172
|
+
renderer.render_ids([{"role": "system", "content": "System only."}])
|
|
@@ -47,6 +47,7 @@ _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"),
|
|
50
51
|
("google/gemma-4-31B-it", "auto"),
|
|
51
52
|
("zai-org/GLM-5", "auto"),
|
|
52
53
|
("zai-org/GLM-5.1", "auto"),
|
|
@@ -93,7 +94,7 @@ _KWARG_VALUES: dict[str, list[Any]] = {
|
|
|
93
94
|
# gpt-oss accepts low/medium/high; Hy3 accepts no_think/low/high. The
|
|
94
95
|
# union is listed here and the matrix builder drops values a given
|
|
95
96
|
# renderer's typed config rejects (see ``_value_valid_for``).
|
|
96
|
-
"reasoning_effort": ["no_think", "low", "medium", "high"],
|
|
97
|
+
"reasoning_effort": ["no_think", "low", "medium", "high", "xhigh"],
|
|
97
98
|
# Hy3 — keep <think>{reasoning}</think> on historical assistant turns
|
|
98
99
|
# (True) vs collapse past-cycle reasoning to <think></think> (False).
|
|
99
100
|
"preserved_thinking": [True, False],
|
|
@@ -131,10 +132,11 @@ _KWARG_VALUES: dict[str, list[Any]] = {
|
|
|
131
132
|
# passthrough mode. The renderer paths diverge significantly under
|
|
132
133
|
# this flag, so both values are exercised.
|
|
133
134
|
"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: ``.
|
|
135
|
+
# Qwen3.5 / Qwen3.6 / Qwen3.8 / Qwen3-VL — when True, prefix each
|
|
136
|
+
# image / video placeholder with ``Picture N: `` / ``Video N: ``.
|
|
136
137
|
"add_vision_id": [True, False],
|
|
137
|
-
# Qwen3.6 — keep historical think blocks before the last real
|
|
138
|
+
# Qwen3.6 / Qwen3.8 — keep historical think blocks before the last real
|
|
139
|
+
# user query. Qwen3.8 defaults this to True.
|
|
138
140
|
"preserve_thinking": [True, False],
|
|
139
141
|
# gpt-oss — pin to a fixed date so the renderer's preamble matches
|
|
140
142
|
# the harmony oracle built with the same date. The default
|
|
@@ -32,6 +32,7 @@ _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"),
|
|
35
36
|
("Qwen/Qwen3-VL-4B-Instruct", "auto"),
|
|
36
37
|
("google/gemma-4-31B-it", "auto"),
|
|
37
38
|
("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.10.dev15}/examples/sglang/multiturn_generate_sglang.py
RENAMED
|
File without changes
|
{renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/examples/sglang/online_multiturn_sglang.py
RENAMED
|
File without changes
|
{renderers-0.1.10.dev13 → renderers-0.1.10.dev15}/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
|