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.
Files changed (85) hide show
  1. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/.github/workflows/publish-dev.yml +1 -1
  2. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/.github/workflows/publish.yml +1 -1
  3. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/PKG-INFO +3 -3
  4. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/README.md +1 -1
  5. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/docs/renderer-config.md +2 -0
  6. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/renderers/__init__.py +4 -0
  7. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/renderers/_version.py +2 -2
  8. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/renderers/base.py +12 -4
  9. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/renderers/configs.py +49 -0
  10. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/renderers/qwen35.py +71 -27
  11. renderers-0.1.11.dev1/renderers/qwen38.py +66 -0
  12. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/conftest.py +2 -0
  13. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_bridge.py +2 -0
  14. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_client.py +3 -1
  15. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_disabled_thinking_stability.py +9 -0
  16. renderers-0.1.11.dev1/tests/test_qwen38.py +178 -0
  17. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_renderer_config_parity.py +7 -4
  18. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_roundtrip.py +2 -0
  19. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/.github/workflows/style.yml +0 -0
  20. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/.github/workflows/test.yml +0 -0
  21. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/.gitignore +0 -0
  22. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/.pre-commit-config.yaml +0 -0
  23. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/LICENSE +0 -0
  24. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/examples/README.md +0 -0
  25. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/examples/sglang/multiturn_generate_sglang.py +0 -0
  26. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/examples/sglang/online_multiturn_sglang.py +0 -0
  27. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/examples/tinker/multiturn_generate_tinker.py +0 -0
  28. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/examples/transformers/multiturn_generate_transformers.py +0 -0
  29. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/examples/vllm/multiturn_generate_vllm.py +0 -0
  30. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/pyproject.toml +0 -0
  31. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/renderers/client.py +0 -0
  32. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/renderers/deepseek_r1.py +0 -0
  33. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/renderers/deepseek_v3.py +0 -0
  34. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/renderers/default.py +0 -0
  35. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/renderers/gemma4.py +0 -0
  36. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/renderers/glm45.py +0 -0
  37. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/renderers/glm5.py +0 -0
  38. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/renderers/gpt_oss.py +0 -0
  39. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/renderers/hy3.py +0 -0
  40. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/renderers/inkling.py +0 -0
  41. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/renderers/kimi_k2.py +0 -0
  42. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/renderers/kimi_k25.py +0 -0
  43. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/renderers/laguna_s21.py +0 -0
  44. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/renderers/laguna_xs2.py +0 -0
  45. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/renderers/llama_3.py +0 -0
  46. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/renderers/minimax_m2.py +0 -0
  47. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/renderers/nemotron3.py +0 -0
  48. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/renderers/parsers.py +0 -0
  49. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/renderers/parsing.py +0 -0
  50. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/renderers/prime_qwen3.py +0 -0
  51. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/renderers/qwen3.py +0 -0
  52. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/renderers/qwen36.py +0 -0
  53. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/renderers/qwen3_vl.py +0 -0
  54. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_build_helpers.py +0 -0
  55. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_deepseek_r1.py +0 -0
  56. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_gemma4.py +0 -0
  57. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_glm_tool_name_validation.py +0 -0
  58. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_gpt_oss_harmony_parity.py +0 -0
  59. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_hy3.py +0 -0
  60. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_incremental.py +0 -0
  61. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_inkling.py +0 -0
  62. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_is_content.py +0 -0
  63. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_kimi_k25_tool_schema.py +0 -0
  64. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_laguna_m1.py +0 -0
  65. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_laguna_s21.py +0 -0
  66. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_laguna_xs21.py +0 -0
  67. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_llama_3.py +0 -0
  68. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_load_tokenizer.py +0 -0
  69. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_message_indices.py +0 -0
  70. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_message_tool_names.py +0 -0
  71. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_multimodal.py +0 -0
  72. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_nemotron3_parity.py +0 -0
  73. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_nemotron3_ultra.py +0 -0
  74. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_parse_response.py +0 -0
  75. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_parse_response_robustness.py +0 -0
  76. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_parsers.py +0 -0
  77. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_preserve_thinking.py +0 -0
  78. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_prime_qwen3_parity.py +0 -0
  79. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_qwen35_size_coverage.py +0 -0
  80. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_render_ids.py +0 -0
  81. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_renderer_config.py +0 -0
  82. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_sampled_mask.py +0 -0
  83. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_tokens_per_message.py +0 -0
  84. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/tests/test_tool_arg_type_preservation.py +0 -0
  85. {renderers-0.1.10.dev13 → renderers-0.1.11.dev1}/uv.lock +0 -0
@@ -62,6 +62,6 @@ jobs:
62
62
  path: dist/
63
63
 
64
64
  - name: Publish to PyPI
65
- uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
65
+ uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
66
66
  with:
67
67
  skip-existing: true
@@ -87,4 +87,4 @@ jobs:
87
87
  path: dist/
88
88
 
89
89
  - name: Publish to PyPI
90
- uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
90
+ uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: renderers
3
- Version: 0.1.10.dev13
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.10.dev13'
22
- __version_tuple__ = version_tuple = (0, 1, 10, 'dev13')
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 this Protocol
798
- structurally — no explicit inheritance required. Callers that need
799
- to drive vLLM's ``multi_modal_data`` features field or carry images
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
- ("system\n", False),
482
- (_TOOLS_HEADER, False),
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
- emit_special(self._im_start, 0, is_sampled=False, is_content=False)
499
- sys_segments: list[tuple[str, bool]] = [("system\n", False)]
500
- if sys_content:
501
- sys_segments.append((sys_content, True))
502
- emit_text_segments(sys_segments, 0, is_sampled=False)
503
- emit_special(self._im_end, 0, is_sampled=False, is_content=False)
504
- emit_text("\n", 0, is_sampled=False, is_content=False)
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
- # Extract reasoning_content
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 image /
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 user query.
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"),