renderers 0.1.13.dev7__tar.gz → 0.1.13.dev13__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 (99) hide show
  1. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/PKG-INFO +2 -2
  2. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/README.md +1 -1
  3. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/docs/renderer-config.md +2 -0
  4. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/renderers/__init__.py +4 -0
  5. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/renderers/_version.py +2 -2
  6. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/renderers/base.py +4 -0
  7. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/renderers/client.py +287 -20
  8. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/renderers/configs.py +31 -0
  9. renderers-0.1.13.dev13/renderers/muse_glimmer.py +887 -0
  10. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/tests/fixtures/prefix_stability.json +10 -0
  11. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/tests/parity.py +6 -0
  12. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/tests/test_build_helpers.py +8 -0
  13. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/tests/test_client.py +270 -7
  14. renderers-0.1.13.dev13/tests/test_muse_glimmer.py +740 -0
  15. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/tests/test_reasoning_boundaries.py +14 -0
  16. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/.github/workflows/publish-dev.yml +0 -0
  17. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/.github/workflows/publish.yml +0 -0
  18. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/.github/workflows/style.yml +0 -0
  19. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/.github/workflows/test.yml +0 -0
  20. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/.gitignore +0 -0
  21. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/.pre-commit-config.yaml +0 -0
  22. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/LICENSE +0 -0
  23. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/examples/README.md +0 -0
  24. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/examples/sglang/multiturn_generate_sglang.py +0 -0
  25. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/examples/sglang/online_multiturn_sglang.py +0 -0
  26. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/examples/tinker/multiturn_generate_tinker.py +0 -0
  27. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/examples/transformers/multiturn_generate_transformers.py +0 -0
  28. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/examples/vllm/multiturn_generate_vllm.py +0 -0
  29. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/pyproject.toml +0 -0
  30. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/renderers/custom.py +0 -0
  31. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/renderers/deepseek_r1.py +0 -0
  32. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/renderers/deepseek_v3.py +0 -0
  33. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/renderers/deepseek_v4.py +0 -0
  34. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/renderers/default.py +0 -0
  35. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/renderers/gemma4.py +0 -0
  36. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/renderers/glm45.py +0 -0
  37. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/renderers/glm5.py +0 -0
  38. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/renderers/gpt_oss.py +0 -0
  39. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/renderers/hy3.py +0 -0
  40. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/renderers/inkling.py +0 -0
  41. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/renderers/kimi_k2.py +0 -0
  42. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/renderers/kimi_k25.py +0 -0
  43. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/renderers/laguna_s21.py +0 -0
  44. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/renderers/laguna_xs2.py +0 -0
  45. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/renderers/llama_3.py +0 -0
  46. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/renderers/minimax_m2.py +0 -0
  47. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/renderers/nemotron3.py +0 -0
  48. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/renderers/parsers.py +0 -0
  49. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/renderers/parsing.py +0 -0
  50. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/renderers/prime_qwen3.py +0 -0
  51. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/renderers/qwen3.py +0 -0
  52. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/renderers/qwen35.py +0 -0
  53. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/renderers/qwen36.py +0 -0
  54. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/renderers/qwen38.py +0 -0
  55. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/renderers/qwen3_vl.py +0 -0
  56. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/renderers/reasoning.py +0 -0
  57. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/renderers/tagged.py +0 -0
  58. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/tests/audit_prefix_stability.py +0 -0
  59. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/tests/conftest.py +0 -0
  60. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/tests/reference_continuations.py +0 -0
  61. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/tests/reference_rendering.py +0 -0
  62. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/tests/replay.py +0 -0
  63. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/tests/test_bridge.py +0 -0
  64. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/tests/test_custom.py +0 -0
  65. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/tests/test_deepseek_r1.py +0 -0
  66. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/tests/test_deepseek_v4.py +0 -0
  67. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/tests/test_disabled_thinking_stability.py +0 -0
  68. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/tests/test_gemma4.py +0 -0
  69. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/tests/test_glm53_flash.py +0 -0
  70. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/tests/test_glm_tool_name_validation.py +0 -0
  71. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/tests/test_hy3.py +0 -0
  72. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/tests/test_incremental.py +0 -0
  73. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/tests/test_inkling.py +0 -0
  74. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/tests/test_is_content.py +0 -0
  75. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/tests/test_kimi_k25_tool_schema.py +0 -0
  76. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/tests/test_laguna_m1.py +0 -0
  77. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/tests/test_laguna_xs21.py +0 -0
  78. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/tests/test_llama_3.py +0 -0
  79. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/tests/test_load_tokenizer.py +0 -0
  80. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/tests/test_message_indices.py +0 -0
  81. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/tests/test_message_tool_names.py +0 -0
  82. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/tests/test_multimodal.py +0 -0
  83. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/tests/test_nemotron35_vlm.py +0 -0
  84. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/tests/test_nemotron3_ultra.py +0 -0
  85. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/tests/test_offsetless_tokenizers.py +0 -0
  86. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/tests/test_parity.py +0 -0
  87. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/tests/test_parse_response.py +0 -0
  88. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/tests/test_parse_response_robustness.py +0 -0
  89. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/tests/test_parsers.py +0 -0
  90. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/tests/test_preserve_thinking.py +0 -0
  91. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/tests/test_qwen35_size_coverage.py +0 -0
  92. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/tests/test_qwen38.py +0 -0
  93. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/tests/test_renderer_config.py +0 -0
  94. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/tests/test_roundtrip.py +0 -0
  95. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/tests/test_sampled_mask.py +0 -0
  96. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/tests/test_tagged_rollout.py +0 -0
  97. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/tests/test_tokens_per_message.py +0 -0
  98. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/tests/test_tool_arg_type_preservation.py +0 -0
  99. {renderers-0.1.13.dev7 → renderers-0.1.13.dev13}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: renderers
3
- Version: 0.1.13.dev7
3
+ Version: 0.1.13.dev13
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
@@ -114,7 +114,7 @@ next_prompt_ids = r.bridge_to_next_turn(
114
114
  )
115
115
  ```
116
116
 
117
- Hand-coded renderers ship for `qwen3`, `qwen3-vl`, `qwen3.5`, `qwen3.6`, `qwen3.8`, `gemma4`, `glm-5`, `glm-5.1`, `glm-5.3`, `glm-5.3-flash`, `glm-4.5`, `minimax-m2`, `deepseek-v3`, `deepseek-r1`, `deepseek-v4` (V4 Flash 0731), `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`, `glm-5.3-flash`, `kimi-k2.5` / `kimi-k2.6`, and the Inkling checkpoints are multimodal (Inkling handles both image **and** audio; `glm-5.3-flash` renders images only with `process_multimodal=False`).
117
+ Hand-coded renderers ship for `qwen3`, `qwen3-vl`, `qwen3.5`, `qwen3.6`, `qwen3.8`, `gemma4`, `glm-5`, `glm-5.1`, `glm-5.3`, `glm-5.3-flash`, `glm-4.5`, `minimax-m2`, `deepseek-v3`, `deepseek-r1`, `deepseek-v4` (V4 Flash 0731), `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`, `muse-glimmer`, `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`, `glm-5.3-flash`, `muse-glimmer`, `kimi-k2.5` / `kimi-k2.6`, and the Inkling checkpoints are multimodal (Inkling handles both image **and** audio; `glm-5.3-flash` and `muse-glimmer` render images only with `process_multimodal=False`).
118
118
 
119
119
  ## API
120
120
 
@@ -94,7 +94,7 @@ next_prompt_ids = r.bridge_to_next_turn(
94
94
  )
95
95
  ```
96
96
 
97
- Hand-coded renderers ship for `qwen3`, `qwen3-vl`, `qwen3.5`, `qwen3.6`, `qwen3.8`, `gemma4`, `glm-5`, `glm-5.1`, `glm-5.3`, `glm-5.3-flash`, `glm-4.5`, `minimax-m2`, `deepseek-v3`, `deepseek-r1`, `deepseek-v4` (V4 Flash 0731), `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`, `glm-5.3-flash`, `kimi-k2.5` / `kimi-k2.6`, and the Inkling checkpoints are multimodal (Inkling handles both image **and** audio; `glm-5.3-flash` renders images only with `process_multimodal=False`).
97
+ Hand-coded renderers ship for `qwen3`, `qwen3-vl`, `qwen3.5`, `qwen3.6`, `qwen3.8`, `gemma4`, `glm-5`, `glm-5.1`, `glm-5.3`, `glm-5.3-flash`, `glm-4.5`, `minimax-m2`, `deepseek-v3`, `deepseek-r1`, `deepseek-v4` (V4 Flash 0731), `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`, `muse-glimmer`, `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`, `glm-5.3-flash`, `muse-glimmer`, `kimi-k2.5` / `kimi-k2.6`, and the Inkling checkpoints are multimodal (Inkling handles both image **and** audio; `glm-5.3-flash` and `muse-glimmer` render images only with `process_multimodal=False`).
98
98
 
99
99
  ## API
100
100
 
@@ -37,6 +37,7 @@ product in `tests/test_parity.py`.
37
37
  | GLM-5.3 / 5.3 Flash | `GLM53RendererConfig` / `GLM53FlashRendererConfig` | `clear_thinking`, `reasoning_effort` | - |
38
38
  | GLM-4.5 | `GLM45RendererConfig` | `enable_thinking` | - |
39
39
  | gpt-oss | `GptOssRendererConfig` | `reasoning_effort`, `conversation_start_date` | `use_system_prompt`, `knowledge_cutoff`, `model_identity`, `auto_drop_analysis` |
40
+ | Muse Glimmer | `MuseGlimmerRendererConfig` | `reasoning_strength`, `knowledge_cutoff`, `current_date` | - |
40
41
  | Hy3 | `Hy3RendererConfig` | `reasoning_effort`, `preserved_thinking`, `is_training`, `raw_last_assistant`, `fallback_strategy` | - |
41
42
  | Kimi K2 | `KimiK2RendererConfig` | - | `enable_thinking` |
42
43
  | Kimi K2.5 / 2.6 | `KimiK25RendererConfig` | `thinking` | `image_cache_max` |
@@ -144,6 +145,7 @@ the knobs its template actually exposes:
144
145
  | GLM-5.3 / 5.3 Flash | `clear_thinking=False -> all`, else `tool_cycle` |
145
146
  | GLM-4.5 | `enable_thinking=False -> all`, else `tool_cycle` |
146
147
  | gpt-oss | `auto_drop_analysis=False -> all`, else `tool_cycle` |
148
+ | Muse Glimmer | `all` (the template keeps every assistant's reasoning) |
147
149
  | Hy3 | `preserved_thinking=True -> all`, else `tool_cycle` |
148
150
  | Kimi K2.5 / 2.6 | `thinking=False -> all`, else `tool_cycle` |
149
151
  | Nemotron-3 / 3.5 | `truncate_history_thinking=False -> all`; else `enable_thinking=False -> all`; else `tool_cycle` |
@@ -68,6 +68,7 @@ from renderers.configs import (
68
68
  LagunaXS21RendererConfig,
69
69
  Llama3RendererConfig,
70
70
  MiniMaxM2RendererConfig,
71
+ MuseGlimmerRendererConfig,
71
72
  Nemotron3RendererConfig,
72
73
  Nemotron3UltraRendererConfig,
73
74
  Nemotron35RendererConfig,
@@ -111,6 +112,7 @@ _LAZY_RENDERERS: dict[str, str] = {
111
112
  "LagunaXS2Renderer": "renderers.laguna_xs2",
112
113
  "Llama3Renderer": "renderers.llama_3",
113
114
  "MiniMaxM2Renderer": "renderers.minimax_m2",
115
+ "MuseGlimmerRenderer": "renderers.muse_glimmer",
114
116
  "Nemotron35Renderer": "renderers.nemotron3",
115
117
  "Nemotron3Renderer": "renderers.nemotron3",
116
118
  "Nemotron3UltraRenderer": "renderers.nemotron3",
@@ -193,6 +195,8 @@ __all__ = [
193
195
  "MiniMaxM2RendererConfig",
194
196
  "MultiModalData",
195
197
  "MultimodalRenderer",
198
+ "MuseGlimmerRenderer",
199
+ "MuseGlimmerRendererConfig",
196
200
  "Nemotron35Renderer",
197
201
  "Nemotron35RendererConfig",
198
202
  "Nemotron3Renderer",
@@ -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.13.dev7'
22
- __version_tuple__ = version_tuple = (0, 1, 13, 'dev7')
21
+ __version__ = version = '0.1.13.dev13'
22
+ __version_tuple__ = version_tuple = (0, 1, 13, 'dev13')
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -1063,6 +1063,8 @@ MODEL_RENDERER_MAP: dict[str, str] = {
1063
1063
  # GPT-OSS.
1064
1064
  "openai/gpt-oss-20b": "gpt-oss",
1065
1065
  "openai/gpt-oss-120b": "gpt-oss",
1066
+ # Muse Glimmer (ATEM channel format; images as one <|patch|> each).
1067
+ "meta-models/Muse-Glimmer-30B": "muse-glimmer",
1066
1068
  # Thinking Machines Inkling checkpoints share byte-identical tokenizer,
1067
1069
  # chat-template, and processor assets (vision + audio; transformers >= 5.14).
1068
1070
  "thinkingmachines/Inkling": "inkling",
@@ -1375,6 +1377,7 @@ def _populate_registry():
1375
1377
  )
1376
1378
  from renderers.llama_3 import Llama3Renderer
1377
1379
  from renderers.minimax_m2 import MiniMaxM2Renderer
1380
+ from renderers.muse_glimmer import MuseGlimmerRenderer
1378
1381
  from renderers.nemotron3 import (
1379
1382
  Nemotron3Renderer,
1380
1383
  Nemotron3UltraRenderer,
@@ -1419,6 +1422,7 @@ def _populate_registry():
1419
1422
  "nemotron-3-ultra": Nemotron3UltraRenderer,
1420
1423
  "nemotron-3.5": Nemotron35Renderer,
1421
1424
  "gpt-oss": GptOssRenderer,
1425
+ "muse-glimmer": MuseGlimmerRenderer,
1422
1426
  }
1423
1427
  )
1424
1428
 
@@ -8,10 +8,12 @@ from __future__ import annotations
8
8
 
9
9
  import asyncio
10
10
  import base64
11
+ import binascii
11
12
  import json
12
13
  import logging
13
14
  import math
14
15
  from collections.abc import Mapping
16
+ from itertools import islice, pairwise
15
17
  from typing import Any, cast
16
18
 
17
19
  import httpx
@@ -93,7 +95,7 @@ async def _resolve_max_prompt_len(client: AsyncOpenAI, model: str) -> int | None
93
95
  return _max_prompt_len_cache[key]
94
96
  try:
95
97
  payload = await client.get("/models", cast_to=cast(Any, dict[str, Any]))
96
- except Exception as exc:
98
+ except Exception as exc: # noqa: BLE001
97
99
  _request_logger.debug("max_prompt_len lookup failed: %s", exc)
98
100
  _max_prompt_len_cache[key] = None
99
101
  return None
@@ -111,28 +113,45 @@ async def _resolve_max_prompt_len(client: AsyncOpenAI, model: str) -> int | None
111
113
  return value
112
114
 
113
115
 
114
- def _strip_base64_field(raw: bytes, prefix: bytes) -> tuple[bytes, memoryview | None]:
116
+ def _strip_base64_fields(raw: bytes, prefix: bytes) -> tuple[bytes, list[memoryview]]:
115
117
  """Splice a large base64 string field out of raw JSON bytes.
116
118
 
117
119
  Avoids json-decoding megabytes of base64; the returned memoryview
118
120
  references ``raw`` and is re-inserted into the parsed payload.
119
121
  """
120
- data_start = raw.find(prefix)
121
- if data_start < 0:
122
- return raw, None
123
-
124
- data_start += len(prefix)
125
- data_end = raw.index(b'"', data_start)
126
- data = memoryview(raw)[data_start:data_end]
127
- stripped = raw[:data_start] + raw[data_end:]
128
- return stripped, data
122
+ chunks = []
123
+ fields = []
124
+ cursor = 0
125
+ while (start := raw.find(prefix, cursor)) >= 0:
126
+ start += len(prefix)
127
+ end = raw.index(b'"', start)
128
+ chunks.append(raw[cursor:start])
129
+ fields.append(memoryview(raw)[start:end])
130
+ cursor = end
131
+ if not fields:
132
+ return raw, fields
133
+ chunks.append(raw[cursor:])
134
+ return b"".join(chunks), fields
129
135
 
130
136
 
131
137
  def parse_generate_response(raw: bytes) -> dict[str, Any]:
132
- stripped, routed_data = _strip_base64_field(raw, ROUTED_EXPERTS_DATA_PREFIX)
138
+ stripped, routed_data = _strip_base64_fields(raw, ROUTED_EXPERTS_DATA_PREFIX)
139
+ stripped, compact_data = _strip_base64_fields(
140
+ stripped, b'"compact_logprobs":{"data":"'
141
+ )
133
142
  payload: dict[str, Any] = json.loads(stripped)
134
- if routed_data is not None:
135
- payload["choices"][0]["routed_experts"]["data"] = routed_data
143
+ for field, buffers in (
144
+ ("routed_experts", routed_data),
145
+ ("compact_logprobs", compact_data),
146
+ ):
147
+ if buffers:
148
+ pending = iter(buffers)
149
+ for choice in payload["choices"]:
150
+ if (
151
+ isinstance(choice.get(field), dict)
152
+ and choice[field].get("data") == ""
153
+ ):
154
+ choice[field]["data"] = next(pending)
136
155
  return payload
137
156
 
138
157
 
@@ -150,6 +169,80 @@ def _parse_packed_completion_logprobs(
150
169
  return values.astype(np.float64).tolist()
151
170
 
152
171
 
172
+ def _parse_compact_logprobs(
173
+ choice: Mapping[str, Any], completion_ids: list[int], k: int | None
174
+ ) -> tuple[list[float], list[list[int]] | None, list[list[float]] | None]:
175
+ packed = choice.get("compact_logprobs")
176
+ if not isinstance(packed, Mapping) or packed.get("format") != "u32-f32-le-v1":
177
+ raise MalformedGenerateResponseError("Unsupported compact logprob format")
178
+ offsets = packed.get("offsets")
179
+ if (
180
+ not isinstance(offsets, list)
181
+ or len(offsets) != len(completion_ids) + 1
182
+ or any(type(x) is not int or x < 0 for x in offsets)
183
+ or offsets[0] != 0
184
+ or any(end <= start for start, end in pairwise(offsets))
185
+ ):
186
+ raise MalformedGenerateResponseError("Invalid compact logprob offsets")
187
+ num_top = packed.get("num_top_logprobs")
188
+ if type(num_top) is not int or num_top < -1:
189
+ raise MalformedGenerateResponseError("Invalid compact logprob candidate count")
190
+ try:
191
+ raw = base64.b64decode(packed["data"], validate=True)
192
+ except (KeyError, TypeError, ValueError, binascii.Error) as exc:
193
+ raise MalformedGenerateResponseError("Invalid compact logprob base64") from exc
194
+ count = offsets[-1]
195
+ if len(raw) != count * 8:
196
+ raise MalformedGenerateResponseError("Invalid compact logprob buffer length")
197
+ ids = np.frombuffer(raw, dtype="<u4", count=count)
198
+ values = np.frombuffer(raw, dtype="<f4", count=count, offset=count * 4)
199
+ if not np.isfinite(values).all() or (values > 1e-6).any():
200
+ raise MalformedGenerateResponseError("Invalid compact logprob values")
201
+ sampled = []
202
+ top_ids: list[list[int]] = []
203
+ top_logprobs: list[list[float]] = []
204
+ supports = _parse_sampling_mask(choice, completion_ids) if k is not None else None
205
+ for index, (token_id, start, end) in enumerate(
206
+ zip(completion_ids, offsets, offsets[1:])
207
+ ):
208
+ # FlatLogprobs can repeat the sampled ID. Match vLLM's dict order/value semantics.
209
+ items = list(zip(ids[start:end].tolist(), values[start:end].tolist()))
210
+ row = dict(items)
211
+ if any(row[i] != p for i, p in items):
212
+ raise MalformedGenerateResponseError("Repeated compact logprobs disagree")
213
+ if token_id not in row or row[token_id] == VLLM_LOGPROB_SENTINEL:
214
+ raise MalformedGenerateResponseError(
215
+ "Missing compact sampled-token evidence"
216
+ )
217
+ sampled.append(row[token_id])
218
+ if k is not None:
219
+ head = dict(islice(row.items(), max(num_top, 1)))
220
+ selected_ids, selected_logprobs = _select_sampler_head(
221
+ head,
222
+ token_id,
223
+ row[token_id],
224
+ k,
225
+ supports[index] if supports is not None else None,
226
+ )
227
+ top_ids.append(selected_ids)
228
+ top_logprobs.append(selected_logprobs)
229
+ return (
230
+ sampled,
231
+ top_ids if k is not None else None,
232
+ top_logprobs if k is not None else None,
233
+ )
234
+
235
+
236
+ def _logprob_token_id(entry: Mapping[str, Any]) -> int | None:
237
+ if "token_id" in entry:
238
+ token_id = entry["token_id"]
239
+ return token_id if type(token_id) is int and token_id >= 0 else None
240
+ token = entry.get("token")
241
+ if isinstance(token, str) and token.startswith("token_id:") and token[9:].isdigit():
242
+ return int(token[9:])
243
+ return None
244
+
245
+
153
246
  def _parse_completion_logprobs(
154
247
  choice: Mapping[str, Any], completion_ids: list[int]
155
248
  ) -> list[float]:
@@ -180,7 +273,7 @@ def _parse_completion_logprobs(
180
273
  f"Engine response choice.logprobs.content[{index}] must be an object."
181
274
  )
182
275
  expected_token = f"token_id:{completion_ids[index]}"
183
- if entry.get("token") != expected_token:
276
+ if _logprob_token_id(entry) != completion_ids[index]:
184
277
  raise MalformedGenerateResponseError(
185
278
  f"Engine response choice.logprobs.content[{index}].token must be {expected_token!r}."
186
279
  )
@@ -207,6 +300,145 @@ def _parse_completion_logprobs(
207
300
  return completion_logprobs
208
301
 
209
302
 
303
+ def _parse_sampling_mask(
304
+ choice: Mapping[str, Any], completion_ids: list[int]
305
+ ) -> list[list[int]] | None:
306
+ mask = choice.get("sampling_mask")
307
+ if mask is None:
308
+ return None
309
+ if isinstance(mask, Mapping):
310
+ arrays = {}
311
+ for name in ("ids", "counts"):
312
+ packed = mask.get(name)
313
+ if not isinstance(packed, Mapping) or packed.get("dtype") != "int32":
314
+ raise MalformedGenerateResponseError("Invalid packed sampling mask")
315
+ try:
316
+ values = np.frombuffer(
317
+ base64.b64decode(packed["data"], validate=True), dtype=np.int32
318
+ )
319
+ except (KeyError, TypeError, ValueError, binascii.Error) as exc:
320
+ raise MalformedGenerateResponseError(
321
+ "Invalid packed sampling mask"
322
+ ) from exc
323
+ if packed.get("shape") != [len(values)] or (values < 0).any():
324
+ raise MalformedGenerateResponseError("Invalid packed sampling mask")
325
+ arrays[name] = values
326
+ counts, ids = arrays["counts"], arrays["ids"]
327
+ if len(counts) != len(completion_ids) or int(counts.sum()) != len(ids):
328
+ raise MalformedGenerateResponseError("Sampling mask token count mismatch")
329
+ mask = [row.tolist() for row in np.split(ids, np.cumsum(counts)[:-1])]
330
+ if not len(counts):
331
+ mask = []
332
+ if not isinstance(mask, list) or len(mask) != len(completion_ids):
333
+ raise MalformedGenerateResponseError("Sampling mask token count mismatch")
334
+ for sampled, support in zip(completion_ids, mask, strict=True):
335
+ if (
336
+ not isinstance(support, list)
337
+ or not support
338
+ or any(type(i) is not int or i < 0 for i in support)
339
+ or len(set(support)) != len(support)
340
+ or sampled not in support
341
+ ):
342
+ raise MalformedGenerateResponseError("Invalid recorded sampling support")
343
+ return mask
344
+
345
+
346
+ def _select_sampler_head(
347
+ head: dict[int, float],
348
+ sampled_id: int,
349
+ sampled_logprob: float,
350
+ k: int,
351
+ support: list[int] | None = None,
352
+ ) -> tuple[list[int], list[float]]:
353
+ """Keep an action-independent head, or the complete recorded replay support."""
354
+ if sampled_id in head and not math.isclose(
355
+ head[sampled_id], sampled_logprob, rel_tol=1e-6, abs_tol=1e-5
356
+ ):
357
+ raise MalformedGenerateResponseError(
358
+ "Sampler head and sampled-token logprob disagree"
359
+ )
360
+ # vLLM serializes filtered-out (-inf) candidates as -9999.
361
+ ranked = sorted(
362
+ ((i, p) for i, p in head.items() if p != VLLM_LOGPROB_SENTINEL),
363
+ key=lambda x: (-x[1], x[0]),
364
+ )
365
+ if support is not None:
366
+ support_set = set(support)
367
+ if not support_set.issubset(i for i, _ in ranked):
368
+ raise MalformedGenerateResponseError(
369
+ "Missing sampling-support probabilities"
370
+ )
371
+ ranked = [(i, p) for i, p in ranked if i in support_set]
372
+ if not math.isclose(
373
+ math.fsum(math.exp(p) for _, p in ranked), 1.0, rel_tol=0, abs_tol=1e-4
374
+ ):
375
+ raise MalformedGenerateResponseError(
376
+ "Sampler probabilities must normalize over the recorded support"
377
+ )
378
+ elif len(ranked) > k and ranked[k - 1][1] == ranked[k][1]:
379
+ boundary = ranked[k][1]
380
+ ranked = [(i, p) for i, p in ranked if p > boundary]
381
+ else:
382
+ ranked = ranked[:k]
383
+ if not ranked or math.fsum(math.exp(p) for _, p in ranked) > 1.0001:
384
+ raise MalformedGenerateResponseError("Invalid sampler head probability mass")
385
+ if support is None and len(head) < k + 2:
386
+ raise MalformedGenerateResponseError(
387
+ "Engine returned fewer top logprobs than requested"
388
+ )
389
+ return [i for i, _ in ranked], [p for _, p in ranked]
390
+
391
+
392
+ def _parse_completion_top_logprobs(
393
+ choice: Mapping[str, Any], completion_ids: list[int], k: int
394
+ ) -> tuple[list[list[int]], list[list[float]]]:
395
+ """Select an action-independent head from k+2 candidates."""
396
+ logprobs = choice.get("logprobs")
397
+ content = logprobs.get("content") if isinstance(logprobs, Mapping) else None
398
+ if not isinstance(content, list) or len(content) != len(completion_ids):
399
+ raise MalformedGenerateResponseError("Missing requested top logprobs")
400
+ top_ids: list[list[int]] = []
401
+ top_logprobs: list[list[float]] = []
402
+ supports = _parse_sampling_mask(choice, completion_ids)
403
+ for index, (sampled_id, entry) in enumerate(
404
+ zip(completion_ids, content, strict=True)
405
+ ):
406
+ candidates = entry.get("top_logprobs")
407
+ if not isinstance(candidates, list) or not candidates:
408
+ raise MalformedGenerateResponseError("Missing requested top logprobs")
409
+ head: dict[int, float] = {}
410
+ for candidate in candidates:
411
+ if not isinstance(candidate, Mapping):
412
+ raise MalformedGenerateResponseError("Top logprob must be an object")
413
+ token_id = _logprob_token_id(candidate)
414
+ if token_id is None:
415
+ raise MalformedGenerateResponseError(
416
+ "Top logprob must identify a nonnegative token id"
417
+ )
418
+ logprob = candidate.get("logprob")
419
+ if (
420
+ isinstance(logprob, bool)
421
+ or not isinstance(logprob, (int, float))
422
+ or not math.isfinite(logprob)
423
+ ):
424
+ raise MalformedGenerateResponseError("Top logprob must be finite")
425
+ if logprob > 1e-6:
426
+ raise MalformedGenerateResponseError("Top logprob cannot be positive")
427
+ if token_id in head:
428
+ raise MalformedGenerateResponseError("Duplicate top logprob token id")
429
+ head[token_id] = float(logprob)
430
+ selected_ids, selected_logprobs = _select_sampler_head(
431
+ head,
432
+ sampled_id,
433
+ entry["logprob"],
434
+ k,
435
+ supports[index] if supports is not None else None,
436
+ )
437
+ top_ids.append(selected_ids)
438
+ top_logprobs.append(selected_logprobs)
439
+ return top_ids, top_logprobs
440
+
441
+
210
442
  async def generate(
211
443
  *,
212
444
  client: AsyncOpenAI,
@@ -226,9 +458,16 @@ async def generate(
226
458
  ) -> dict[str, Any]:
227
459
  """Tokenize messages, call vLLM /inference/v1/generate, parse the response.
228
460
 
229
- ``sampling_params`` is forwarded to vLLM verbatim. Two fields are always
230
- set by us and override caller values: ``stop_token_ids`` (from the
231
- renderer) and ``logprobs=1`` (we always emit completion_logprobs). Pass
461
+ ``sampling_params`` is forwarded to vLLM, with ``stop_token_ids`` taken
462
+ from the renderer and ``logprobs`` set to at least one for sampled-token
463
+ evidence. A positive integer requests a top-k head; we ask the engine
464
+ for two extra candidates to select that head independently of the sampled
465
+ action, excluding ties at its boundary.
466
+ Head requests opt into compact numeric HTTP logprobs; set
467
+ ``extra_args.prl_compact_logprobs=False`` to use native JSON. Servers without
468
+ the extension can still return native JSON. Sampled-only requests keep the
469
+ native path by default.
470
+ Pass
232
471
  ``prompt_ids`` to skip rendering and use a prebuilt token sequence —
233
472
  pair it with ``multi_modal_data`` when the prebuilt prompt has image /
234
473
  video placeholders that need engine-side mm payload, and with
@@ -267,6 +506,10 @@ async def generate(
267
506
  is vLLM's (>= 0.32) list of ``{version, start, end}`` spans over
268
507
  completion-token indexes, or ``None`` when the engine labels none.
269
508
 
509
+ When ``sampling_params["logprobs"]`` is a positive int, also
510
+ ``completion_top_ids`` / ``completion_top_logprobs``
511
+ (original sampler probabilities, retaining full-vocabulary normalization).
512
+
270
513
  ``prompt_attribution`` is the renderer's :class:`RenderedTokens` for
271
514
  the prompt — either the one this call computed via
272
515
  ``renderer.render(...)`` or the one the caller threaded in alongside
@@ -330,7 +573,18 @@ async def generate(
330
573
 
331
574
  sp: dict[str, Any] = dict(sampling_params or {})
332
575
  sp["stop_token_ids"] = stop_token_ids
333
- sp["logprobs"] = 1
576
+ requested_logprobs = sp.get("logprobs")
577
+ head_size = (
578
+ requested_logprobs
579
+ if type(requested_logprobs) is int and requested_logprobs > 0
580
+ else None
581
+ )
582
+ sp["logprobs"] = head_size + 2 if head_size is not None else 1
583
+ if head_size is not None:
584
+ sp["extra_args"] = {
585
+ "prl_compact_logprobs": True,
586
+ **(sp.get("extra_args") or {}),
587
+ }
334
588
  sp.setdefault("skip_special_tokens", False)
335
589
 
336
590
  body: dict[str, Any] = {
@@ -389,7 +643,18 @@ async def generate(
389
643
  "Engine response must include mm_placeholders when process_multimodal=False."
390
644
  )
391
645
 
392
- completion_logprobs = _parse_completion_logprobs(choice, completion_ids)
646
+ completion_top_ids = None
647
+ completion_top_logprobs = None
648
+ if choice.get("compact_logprobs") is not None:
649
+ completion_logprobs, completion_top_ids, completion_top_logprobs = (
650
+ _parse_compact_logprobs(choice, completion_ids, head_size)
651
+ )
652
+ else:
653
+ completion_logprobs = _parse_completion_logprobs(choice, completion_ids)
654
+ if head_size is not None:
655
+ completion_top_ids, completion_top_logprobs = (
656
+ _parse_completion_top_logprobs(choice, completion_ids, head_size)
657
+ )
393
658
 
394
659
  parsed = renderer.parse_response(
395
660
  completion_ids,
@@ -426,6 +691,8 @@ async def generate(
426
691
  "mm_placeholders": mm_placeholders,
427
692
  "completion_ids": list(completion_ids),
428
693
  "completion_logprobs": completion_logprobs,
694
+ "completion_top_ids": completion_top_ids,
695
+ "completion_top_logprobs": completion_top_logprobs,
429
696
  "content": parsed.content,
430
697
  "reasoning_content": parsed.reasoning_content,
431
698
  "tool_calls": parsed.tool_calls,
@@ -647,6 +647,34 @@ class GptOssRendererConfig(BaseRendererConfig):
647
647
  return self
648
648
 
649
649
 
650
+ class MuseGlimmerRendererConfig(BaseRendererConfig):
651
+ """Muse Glimmer (ATEM) renderer config.
652
+
653
+ The chat template never drops historical reasoning, so bridges keep all
654
+ reasoning by default (``thinking_retention`` resolves to ``"all"``).
655
+ """
656
+
657
+ name: Literal["muse-glimmer"] = "muse-glimmer"
658
+ _template_fields = frozenset(
659
+ {"reasoning_strength", "knowledge_cutoff", "current_date"}
660
+ )
661
+
662
+ reasoning_strength: Literal["low", "medium", "high", "xhigh"] = "high"
663
+ """``Reasoning strength: <value>.`` line in the system preamble. Mirrors
664
+ the template's ``reasoning_strength`` kwarg (template default ``high``).
665
+ A system message that already states a reasoning strength (or effort)
666
+ keeps its own line instead."""
667
+
668
+ knowledge_cutoff: str = "2026-01-04"
669
+ """Knowledge-cutoff date in the default preamble (used only when the
670
+ conversation has no system message). Mirrors ``knowledge_cutoff``."""
671
+
672
+ current_date: str | None = None
673
+ """``Current date`` in the default preamble, as ``YYYY-MM-DD``. ``None``
674
+ uses today's date (the template's ``strftime_now`` fallback), fixed when
675
+ the renderer is constructed. Pin it for reproducible training renders."""
676
+
677
+
650
678
  class KimiK2RendererConfig(BaseRendererConfig):
651
679
  """Kimi K2 renderer config.
652
680
 
@@ -1051,6 +1079,7 @@ RendererConfig = Annotated[
1051
1079
  GLM53FlashRendererConfig,
1052
1080
  GLM45RendererConfig,
1053
1081
  GptOssRendererConfig,
1082
+ MuseGlimmerRendererConfig,
1054
1083
  Hy3RendererConfig,
1055
1084
  InklingRendererConfig,
1056
1085
  KimiK2RendererConfig,
@@ -1101,6 +1130,7 @@ _CONFIG_BY_NAME: dict[str, type[BaseRendererConfig]] = {
1101
1130
  "glm-5.3-flash": GLM53FlashRendererConfig,
1102
1131
  "glm-4.5": GLM45RendererConfig,
1103
1132
  "gpt-oss": GptOssRendererConfig,
1133
+ "muse-glimmer": MuseGlimmerRendererConfig,
1104
1134
  "hy3": Hy3RendererConfig,
1105
1135
  "inkling": InklingRendererConfig,
1106
1136
  "kimi-k2": KimiK2RendererConfig,
@@ -1168,6 +1198,7 @@ __all__ = [
1168
1198
  "LagunaXS21RendererConfig",
1169
1199
  "Llama3RendererConfig",
1170
1200
  "MiniMaxM2RendererConfig",
1201
+ "MuseGlimmerRendererConfig",
1171
1202
  "Nemotron35RendererConfig",
1172
1203
  "Nemotron3RendererConfig",
1173
1204
  "Nemotron3UltraRendererConfig",