renderers 0.1.13.dev8__tar.gz → 0.1.13.dev14__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.13.dev8 → renderers-0.1.13.dev14}/PKG-INFO +1 -1
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/renderers/_version.py +2 -2
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/renderers/client.py +287 -20
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/renderers/muse_glimmer.py +44 -18
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/tests/test_client.py +270 -7
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/tests/test_muse_glimmer.py +30 -2
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/tests/test_tool_arg_type_preservation.py +14 -5
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/.github/workflows/publish-dev.yml +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/.github/workflows/publish.yml +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/.github/workflows/style.yml +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/.github/workflows/test.yml +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/.gitignore +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/.pre-commit-config.yaml +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/LICENSE +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/README.md +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/docs/renderer-config.md +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/examples/README.md +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/examples/sglang/multiturn_generate_sglang.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/examples/sglang/online_multiturn_sglang.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/examples/tinker/multiturn_generate_tinker.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/examples/transformers/multiturn_generate_transformers.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/examples/vllm/multiturn_generate_vllm.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/pyproject.toml +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/renderers/__init__.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/renderers/base.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/renderers/configs.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/renderers/custom.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/renderers/deepseek_r1.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/renderers/deepseek_v3.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/renderers/deepseek_v4.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/renderers/default.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/renderers/gemma4.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/renderers/glm45.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/renderers/glm5.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/renderers/gpt_oss.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/renderers/hy3.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/renderers/inkling.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/renderers/kimi_k2.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/renderers/kimi_k25.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/renderers/laguna_s21.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/renderers/laguna_xs2.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/renderers/llama_3.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/renderers/minimax_m2.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/renderers/nemotron3.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/renderers/parsers.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/renderers/parsing.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/renderers/prime_qwen3.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/renderers/qwen3.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/renderers/qwen35.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/renderers/qwen36.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/renderers/qwen38.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/renderers/qwen3_vl.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/renderers/reasoning.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/renderers/tagged.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/tests/audit_prefix_stability.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/tests/conftest.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/tests/fixtures/prefix_stability.json +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/tests/parity.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/tests/reference_continuations.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/tests/reference_rendering.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/tests/replay.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/tests/test_bridge.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/tests/test_build_helpers.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/tests/test_custom.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/tests/test_deepseek_r1.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/tests/test_deepseek_v4.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/tests/test_disabled_thinking_stability.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/tests/test_gemma4.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/tests/test_glm53_flash.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/tests/test_glm_tool_name_validation.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/tests/test_hy3.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/tests/test_incremental.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/tests/test_inkling.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/tests/test_is_content.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/tests/test_kimi_k25_tool_schema.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/tests/test_laguna_m1.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/tests/test_laguna_xs21.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/tests/test_llama_3.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/tests/test_load_tokenizer.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/tests/test_message_indices.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/tests/test_message_tool_names.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/tests/test_multimodal.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/tests/test_nemotron35_vlm.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/tests/test_nemotron3_ultra.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/tests/test_offsetless_tokenizers.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/tests/test_parity.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/tests/test_parse_response.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/tests/test_parse_response_robustness.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/tests/test_parsers.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/tests/test_preserve_thinking.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/tests/test_qwen35_size_coverage.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/tests/test_qwen38.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/tests/test_reasoning_boundaries.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/tests/test_renderer_config.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/tests/test_roundtrip.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/tests/test_sampled_mask.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/tests/test_tagged_rollout.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/tests/test_tokens_per_message.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev14}/uv.lock +0 -0
|
@@ -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.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 1, 13, '
|
|
21
|
+
__version__ = version = '0.1.13.dev14'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 13, 'dev14')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -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
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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 =
|
|
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
|
-
|
|
135
|
-
|
|
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
|
|
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
|
|
230
|
-
|
|
231
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
@@ -51,7 +51,6 @@ from renderers.base import (
|
|
|
51
51
|
should_rerender_for_thinking_retention,
|
|
52
52
|
)
|
|
53
53
|
from renderers.configs import MuseGlimmerRendererConfig
|
|
54
|
-
from renderers.parsing import _build_param_type_index, _coerce_arg_value
|
|
55
54
|
|
|
56
55
|
_IMAGE_TYPES = ("image", "image_url")
|
|
57
56
|
_UNSUPPORTED_MEDIA_TYPES = ("video", "video_url", "audio", "audio_url", "input_audio")
|
|
@@ -104,6 +103,15 @@ _PARAM_RE = re.compile(
|
|
|
104
103
|
r'<atem:parameter\b[^>]*?\bname="(?P<key>[^"]+)"[^>]*?>(?P<value>.*?)</atem:parameter>',
|
|
105
104
|
re.DOTALL,
|
|
106
105
|
)
|
|
106
|
+
_STRIP_REASONING_RE = re.compile(
|
|
107
|
+
r"(?:<\|start\|>assistant\s*)?to=self<\|message\|>.*?<\|eom\|>", re.DOTALL
|
|
108
|
+
)
|
|
109
|
+
_STRIP_OPEN_REASONING_RE = re.compile(
|
|
110
|
+
r"(?:<\|start\|>assistant\s*)?to=self<\|message\|>"
|
|
111
|
+
r"(?:(?!<\|eom\|>)(?!to=[^\s<]+<\|message\|>).)*"
|
|
112
|
+
r"(?=to=[^\s<]+<\|message\|>|$)",
|
|
113
|
+
re.DOTALL,
|
|
114
|
+
)
|
|
107
115
|
|
|
108
116
|
|
|
109
117
|
def _json(value: Any) -> str:
|
|
@@ -723,23 +731,42 @@ class MuseGlimmerRenderer:
|
|
|
723
731
|
reasoning = (
|
|
724
732
|
"\n".join(p for p in reasoning_parts if p) if reasoning_parts else None
|
|
725
733
|
)
|
|
726
|
-
|
|
734
|
+
registered = {_function(tool)["name"] for tool in tools or []}
|
|
727
735
|
calls = [
|
|
728
736
|
c
|
|
729
737
|
for recipient, body, closed in tool_bodies
|
|
730
|
-
for c in self._parse_calls(
|
|
738
|
+
for c in self._parse_calls(body, registered)
|
|
731
739
|
]
|
|
740
|
+
content = "".join(content_parts)
|
|
741
|
+
if (
|
|
742
|
+
messages
|
|
743
|
+
and messages[-1][0] == "user"
|
|
744
|
+
and not messages[-1][2]
|
|
745
|
+
and not any(call.name for call in calls)
|
|
746
|
+
and content.endswith("\n")
|
|
747
|
+
and "<atem:invoke" not in content
|
|
748
|
+
and "<atem:function_calls>" not in content
|
|
749
|
+
):
|
|
750
|
+
# vLLM's non-streaming content regex ends at `$`, before one final LF.
|
|
751
|
+
content = content[:-1]
|
|
752
|
+
if tool_bodies and not any(call.name for call in calls):
|
|
753
|
+
# Stock vLLM preserves malformed tool markup as content when no
|
|
754
|
+
# complete invocation can be dispatched; it does not invent a call.
|
|
755
|
+
remainder = _STRIP_REASONING_RE.sub("", text)
|
|
756
|
+
remainder = _STRIP_OPEN_REASONING_RE.sub("", remainder)
|
|
757
|
+
if "<atem:invoke" in remainder or "<atem:function_calls>" in remainder:
|
|
758
|
+
content = remainder
|
|
759
|
+
elif not content:
|
|
760
|
+
content = "" if "to=self<|message|>" in text else text
|
|
732
761
|
return ParsedResponse(
|
|
733
|
-
content=
|
|
762
|
+
content=content,
|
|
734
763
|
reasoning_content=reasoning,
|
|
735
764
|
tool_calls=calls,
|
|
736
765
|
reasoning_complete=reasoning_complete,
|
|
737
766
|
)
|
|
738
767
|
|
|
739
768
|
@staticmethod
|
|
740
|
-
def _parse_calls(
|
|
741
|
-
recipient: str, body: str, param_types: dict[str, dict[str, dict[str, Any]]]
|
|
742
|
-
) -> list[ParsedToolCall]:
|
|
769
|
+
def _parse_calls(body: str, registered: set[str]) -> list[ParsedToolCall]:
|
|
743
770
|
"""Every ``<atem:invoke>`` in one tool-channel message."""
|
|
744
771
|
calls: list[ParsedToolCall] = []
|
|
745
772
|
last_end = 0
|
|
@@ -754,23 +781,24 @@ class MuseGlimmerRenderer:
|
|
|
754
781
|
)
|
|
755
782
|
)
|
|
756
783
|
continue
|
|
757
|
-
|
|
784
|
+
if name not in registered:
|
|
785
|
+
head, sep, tail = name.partition(".")
|
|
786
|
+
if sep and head == tail and head in registered:
|
|
787
|
+
name = head
|
|
758
788
|
arguments: dict[str, Any] = {}
|
|
759
|
-
fallback = False
|
|
760
789
|
for p in _PARAM_RE.finditer(m.group("body")):
|
|
761
|
-
value
|
|
762
|
-
|
|
763
|
-
|
|
790
|
+
value = p.group("value")
|
|
791
|
+
try:
|
|
792
|
+
value = json.loads(value)
|
|
793
|
+
except (json.JSONDecodeError, ValueError):
|
|
794
|
+
pass
|
|
764
795
|
arguments[p.group("key")] = value
|
|
765
|
-
fallback = fallback or used
|
|
766
796
|
calls.append(
|
|
767
797
|
ParsedToolCall(
|
|
768
798
|
raw=m.group(0),
|
|
769
799
|
name=name,
|
|
770
800
|
arguments=arguments,
|
|
771
|
-
status=ToolCallParseStatus.
|
|
772
|
-
if fallback
|
|
773
|
-
else ToolCallParseStatus.OK,
|
|
801
|
+
status=ToolCallParseStatus.OK,
|
|
774
802
|
)
|
|
775
803
|
)
|
|
776
804
|
rest = body[last_end:]
|
|
@@ -779,7 +807,6 @@ class MuseGlimmerRenderer:
|
|
|
779
807
|
calls.append(
|
|
780
808
|
ParsedToolCall(
|
|
781
809
|
raw=rest.strip(),
|
|
782
|
-
name=recipient,
|
|
783
810
|
status=ToolCallParseStatus.UNCLOSED_BLOCK,
|
|
784
811
|
)
|
|
785
812
|
)
|
|
@@ -787,7 +814,6 @@ class MuseGlimmerRenderer:
|
|
|
787
814
|
calls.append(
|
|
788
815
|
ParsedToolCall(
|
|
789
816
|
raw=body,
|
|
790
|
-
name=recipient,
|
|
791
817
|
status=ToolCallParseStatus.MALFORMED_STRUCTURE,
|
|
792
818
|
)
|
|
793
819
|
)
|