renderers 0.1.13.dev8__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.
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/PKG-INFO +1 -1
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/renderers/_version.py +2 -2
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/renderers/client.py +287 -20
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/tests/test_client.py +270 -7
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/.github/workflows/publish-dev.yml +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/.github/workflows/publish.yml +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/.github/workflows/style.yml +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/.github/workflows/test.yml +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/.gitignore +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/.pre-commit-config.yaml +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/LICENSE +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/README.md +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/docs/renderer-config.md +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/examples/README.md +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/examples/sglang/multiturn_generate_sglang.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/examples/sglang/online_multiturn_sglang.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/examples/tinker/multiturn_generate_tinker.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/examples/transformers/multiturn_generate_transformers.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/examples/vllm/multiturn_generate_vllm.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/pyproject.toml +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/renderers/__init__.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/renderers/base.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/renderers/configs.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/renderers/custom.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/renderers/deepseek_r1.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/renderers/deepseek_v3.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/renderers/deepseek_v4.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/renderers/default.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/renderers/gemma4.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/renderers/glm45.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/renderers/glm5.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/renderers/gpt_oss.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/renderers/hy3.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/renderers/inkling.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/renderers/kimi_k2.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/renderers/kimi_k25.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/renderers/laguna_s21.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/renderers/laguna_xs2.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/renderers/llama_3.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/renderers/minimax_m2.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/renderers/muse_glimmer.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/renderers/nemotron3.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/renderers/parsers.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/renderers/parsing.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/renderers/prime_qwen3.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/renderers/qwen3.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/renderers/qwen35.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/renderers/qwen36.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/renderers/qwen38.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/renderers/qwen3_vl.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/renderers/reasoning.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/renderers/tagged.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/tests/audit_prefix_stability.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/tests/conftest.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/tests/fixtures/prefix_stability.json +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/tests/parity.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/tests/reference_continuations.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/tests/reference_rendering.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/tests/replay.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/tests/test_bridge.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/tests/test_build_helpers.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/tests/test_custom.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/tests/test_deepseek_r1.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/tests/test_deepseek_v4.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/tests/test_disabled_thinking_stability.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/tests/test_gemma4.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/tests/test_glm53_flash.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/tests/test_glm_tool_name_validation.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/tests/test_hy3.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/tests/test_incremental.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/tests/test_inkling.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/tests/test_is_content.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/tests/test_kimi_k25_tool_schema.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/tests/test_laguna_m1.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/tests/test_laguna_xs21.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/tests/test_llama_3.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/tests/test_load_tokenizer.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/tests/test_message_indices.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/tests/test_message_tool_names.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/tests/test_multimodal.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/tests/test_muse_glimmer.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/tests/test_nemotron35_vlm.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/tests/test_nemotron3_ultra.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/tests/test_offsetless_tokenizers.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/tests/test_parity.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/tests/test_parse_response.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/tests/test_parse_response_robustness.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/tests/test_parsers.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/tests/test_preserve_thinking.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/tests/test_qwen35_size_coverage.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/tests/test_qwen38.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/tests/test_reasoning_boundaries.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/tests/test_renderer_config.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/tests/test_roundtrip.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/tests/test_sampled_mask.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/tests/test_tagged_rollout.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/tests/test_tokens_per_message.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/tests/test_tool_arg_type_preservation.py +0 -0
- {renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/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.dev13'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 13, 'dev13')
|
|
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,
|
|
@@ -5,6 +5,7 @@ import json
|
|
|
5
5
|
import httpx
|
|
6
6
|
import numpy as np
|
|
7
7
|
import pytest
|
|
8
|
+
|
|
8
9
|
from renderers import MalformedGenerateResponseError
|
|
9
10
|
from renderers.base import (
|
|
10
11
|
ParsedResponse,
|
|
@@ -12,7 +13,210 @@ from renderers.base import (
|
|
|
12
13
|
RenderedTokens,
|
|
13
14
|
ToolCallParseStatus,
|
|
14
15
|
)
|
|
15
|
-
from renderers.client import
|
|
16
|
+
from renderers.client import (
|
|
17
|
+
_parse_compact_logprobs,
|
|
18
|
+
_parse_completion_top_logprobs,
|
|
19
|
+
generate,
|
|
20
|
+
parse_generate_response,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _compact_choice(choice):
|
|
25
|
+
entries = choice["logprobs"]["content"]
|
|
26
|
+
ids, values, offsets = [], [], [0]
|
|
27
|
+
for entry in entries:
|
|
28
|
+
# Flat vLLM storage retains the sampled token even when it repeats in top-k.
|
|
29
|
+
for candidate in [entry, *entry.get("top_logprobs", [])]:
|
|
30
|
+
ids.append(
|
|
31
|
+
candidate["token_id"]
|
|
32
|
+
if "token_id" in candidate
|
|
33
|
+
else int(candidate["token"].removeprefix("token_id:"))
|
|
34
|
+
)
|
|
35
|
+
values.append(candidate["logprob"])
|
|
36
|
+
offsets.append(len(ids))
|
|
37
|
+
raw = (
|
|
38
|
+
np.asarray(ids, dtype="<u4").tobytes()
|
|
39
|
+
+ np.asarray(values, dtype="<f4").tobytes()
|
|
40
|
+
)
|
|
41
|
+
return {
|
|
42
|
+
**choice,
|
|
43
|
+
"logprobs": None,
|
|
44
|
+
"compact_logprobs": {
|
|
45
|
+
"data": base64.b64encode(raw).decode("ascii"),
|
|
46
|
+
"format": "u32-f32-le-v1",
|
|
47
|
+
"offsets": offsets,
|
|
48
|
+
"num_top_logprobs": max(
|
|
49
|
+
(len(e.get("top_logprobs", [])) for e in entries), default=0
|
|
50
|
+
),
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _parse_test_head(choice, sampled, k, compact):
|
|
56
|
+
if compact:
|
|
57
|
+
_, ids, logprobs = _parse_compact_logprobs(_compact_choice(choice), sampled, k)
|
|
58
|
+
return ids, logprobs
|
|
59
|
+
return _parse_completion_top_logprobs(choice, sampled, k)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@pytest.mark.parametrize(
|
|
63
|
+
"update",
|
|
64
|
+
[
|
|
65
|
+
{"format": "unknown"},
|
|
66
|
+
{"offsets": None},
|
|
67
|
+
{"offsets": []},
|
|
68
|
+
{"offsets": [1, 2]},
|
|
69
|
+
{"offsets": [0, 0]},
|
|
70
|
+
{"offsets": [0, -1]},
|
|
71
|
+
{"offsets": [False, 1]},
|
|
72
|
+
{"offsets": [0, 1.5]},
|
|
73
|
+
{"offsets": [0, 100]},
|
|
74
|
+
{"num_top_logprobs": True},
|
|
75
|
+
{"num_top_logprobs": -2},
|
|
76
|
+
{"data": "?"},
|
|
77
|
+
{"data": None},
|
|
78
|
+
{"data": ""},
|
|
79
|
+
],
|
|
80
|
+
)
|
|
81
|
+
def test_compact_logprobs_reject_malformed_payload(update):
|
|
82
|
+
choice = _compact_choice(
|
|
83
|
+
{"logprobs": {"content": [{"token": "token_id:7", "logprob": -0.1}]}}
|
|
84
|
+
)
|
|
85
|
+
choice["compact_logprobs"].update(update)
|
|
86
|
+
with pytest.raises(MalformedGenerateResponseError):
|
|
87
|
+
_parse_compact_logprobs(choice, [7], None)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@pytest.mark.parametrize(
|
|
91
|
+
"logprob", [float("nan"), float("inf"), float("-inf"), 0.1, -9999.0]
|
|
92
|
+
)
|
|
93
|
+
def test_compact_logprobs_require_valid_sampled_evidence(logprob):
|
|
94
|
+
choice = _compact_choice(
|
|
95
|
+
{"logprobs": {"content": [{"token": "token_id:7", "logprob": logprob}]}}
|
|
96
|
+
)
|
|
97
|
+
with pytest.raises(MalformedGenerateResponseError):
|
|
98
|
+
_parse_compact_logprobs(choice, [7], None)
|
|
99
|
+
choice = _compact_choice(
|
|
100
|
+
{"logprobs": {"content": [{"token": "token_id:7", "logprob": -0.1}]}}
|
|
101
|
+
)
|
|
102
|
+
with pytest.raises(MalformedGenerateResponseError, match="Missing compact sampled"):
|
|
103
|
+
_parse_compact_logprobs(choice, [8], None)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@pytest.mark.parametrize("separators", [(",", ":"), (", ", ": ")])
|
|
107
|
+
def test_parse_generate_response_preserves_multiple_compact_choices(separators):
|
|
108
|
+
choices = [
|
|
109
|
+
_compact_choice(
|
|
110
|
+
{
|
|
111
|
+
"logprobs": {"content": [{"token": f"token_id:{i}", "logprob": -0.5}]},
|
|
112
|
+
"routed_experts": {
|
|
113
|
+
"data": base64.b64encode(bytes([i])).decode("ascii"),
|
|
114
|
+
"shape": [1, 1, 1],
|
|
115
|
+
},
|
|
116
|
+
}
|
|
117
|
+
)
|
|
118
|
+
for i in (7, 8)
|
|
119
|
+
]
|
|
120
|
+
choices.append(_compact_choice({"logprobs": {"content": []}}))
|
|
121
|
+
payload = parse_generate_response(
|
|
122
|
+
json.dumps({"choices": choices}, separators=separators).encode(),
|
|
123
|
+
)
|
|
124
|
+
for i, choice in zip((7, 8), payload["choices"]):
|
|
125
|
+
assert _parse_compact_logprobs(choice, [i], None) == ([-0.5], None, None)
|
|
126
|
+
assert base64.b64decode(choice["routed_experts"]["data"]) == bytes([i])
|
|
127
|
+
assert _parse_compact_logprobs(payload["choices"][2], [], 2) == ([], [], [])
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@pytest.mark.parametrize("sampled", [1, 3, 5])
|
|
131
|
+
@pytest.mark.parametrize("compact", [False, True])
|
|
132
|
+
@pytest.mark.parametrize(
|
|
133
|
+
"probabilities, expected_ids",
|
|
134
|
+
[
|
|
135
|
+
({1: 0.6, 2: 0.25, 3: 0.1, 4: 0.04, 5: 0.01}, [1, 2]),
|
|
136
|
+
({1: 0.4, 2: 0.2, 3: 0.2, 4: 0.1, 5: 0.1}, [1]),
|
|
137
|
+
],
|
|
138
|
+
)
|
|
139
|
+
def test_top_logprobs_head_is_independent_of_sampled_action(
|
|
140
|
+
sampled, probabilities, expected_ids, compact
|
|
141
|
+
):
|
|
142
|
+
import math
|
|
143
|
+
|
|
144
|
+
order = [sampled, *[i for i in probabilities if i != sampled]][:4]
|
|
145
|
+
entry = {
|
|
146
|
+
"token": f"token_id:{sampled}",
|
|
147
|
+
"logprob": math.log(probabilities[sampled]),
|
|
148
|
+
"top_logprobs": [
|
|
149
|
+
{"token": f"token_id:{i}", "logprob": math.log(probabilities[i])}
|
|
150
|
+
for i in order
|
|
151
|
+
],
|
|
152
|
+
}
|
|
153
|
+
ids, logprobs = _parse_test_head(
|
|
154
|
+
{"logprobs": {"content": [entry]}}, [sampled], 2, compact
|
|
155
|
+
)
|
|
156
|
+
assert ids == [expected_ids]
|
|
157
|
+
np.testing.assert_allclose(
|
|
158
|
+
np.exp(logprobs), [[probabilities[i] for i in expected_ids]]
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
entry["top_logprobs"] = entry["top_logprobs"][:2]
|
|
162
|
+
with pytest.raises(MalformedGenerateResponseError, match="fewer top logprobs"):
|
|
163
|
+
_parse_test_head({"logprobs": {"content": [entry]}}, [sampled], 2, compact)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@pytest.mark.parametrize("compact", [False, True])
|
|
167
|
+
@pytest.mark.parametrize("packed", [False, True])
|
|
168
|
+
@pytest.mark.parametrize("error", [None, "missing", "mass", "mask"])
|
|
169
|
+
def test_top_logprobs_preserve_complete_replay_support(compact, packed, error):
|
|
170
|
+
import math
|
|
171
|
+
|
|
172
|
+
probabilities = {1: 0.6, 2: 0.2, 3: 0.2}
|
|
173
|
+
if error == "mass":
|
|
174
|
+
probabilities[1] = 0.5
|
|
175
|
+
entry = {
|
|
176
|
+
"token": "token_id:3",
|
|
177
|
+
"logprob": math.log(probabilities[3]),
|
|
178
|
+
"top_logprobs": [
|
|
179
|
+
{"token": f"token_id:{i}", "logprob": math.log(p)}
|
|
180
|
+
for i, p in probabilities.items()
|
|
181
|
+
if error != "missing" or i != 2
|
|
182
|
+
],
|
|
183
|
+
}
|
|
184
|
+
support = [1, 2] if error == "mask" else [3, 1, 2]
|
|
185
|
+
mask = [support]
|
|
186
|
+
if packed:
|
|
187
|
+
mask = {
|
|
188
|
+
name: {
|
|
189
|
+
"data": base64.b64encode(np.asarray(values, dtype=np.int32)).decode(),
|
|
190
|
+
"shape": [len(values)],
|
|
191
|
+
"dtype": "int32",
|
|
192
|
+
}
|
|
193
|
+
for name, values in (("ids", support), ("counts", [len(support)]))
|
|
194
|
+
}
|
|
195
|
+
choice = {"logprobs": {"content": [entry]}, "sampling_mask": mask}
|
|
196
|
+
if error is not None:
|
|
197
|
+
with pytest.raises(MalformedGenerateResponseError):
|
|
198
|
+
_parse_test_head(choice, [3], 2, compact)
|
|
199
|
+
else:
|
|
200
|
+
ids, logprobs = _parse_test_head(choice, [3], 2, compact)
|
|
201
|
+
assert ids == [[1, 2, 3]]
|
|
202
|
+
np.testing.assert_allclose(np.exp(logprobs), [[0.6, 0.2, 0.2]])
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@pytest.mark.parametrize("compact", [False, True])
|
|
206
|
+
def test_top_logprobs_preserve_filtered_candidates(compact):
|
|
207
|
+
entry = {
|
|
208
|
+
"token": "token_id:1",
|
|
209
|
+
"logprob": -0.5,
|
|
210
|
+
"top_logprobs": [
|
|
211
|
+
{"token": f"token_id:{i}", "logprob": p}
|
|
212
|
+
for i, p in [(1, -0.5), (2, -2.0), (3, -9999.0)]
|
|
213
|
+
],
|
|
214
|
+
}
|
|
215
|
+
choice = {"logprobs": {"content": [entry]}}
|
|
216
|
+
assert _parse_test_head(choice, [1], 1, compact) == ([[1]], [[-0.5]])
|
|
217
|
+
entry["top_logprobs"][0]["logprob"] = -0.6
|
|
218
|
+
with pytest.raises(MalformedGenerateResponseError, match="disagree"):
|
|
219
|
+
_parse_test_head(choice, [1], 1, compact)
|
|
16
220
|
|
|
17
221
|
|
|
18
222
|
class _FakeRenderer:
|
|
@@ -130,10 +334,38 @@ def _run_generate(client, renderer=None):
|
|
|
130
334
|
},
|
|
131
335
|
],
|
|
132
336
|
)
|
|
133
|
-
|
|
337
|
+
@pytest.mark.parametrize("head_size", [None, 2])
|
|
338
|
+
@pytest.mark.parametrize("transport", ["native", "compact", "opt_out"])
|
|
339
|
+
@pytest.mark.parametrize("numeric_ids", [False, True])
|
|
340
|
+
def test_generate_builds_request_body_and_parses_response(
|
|
341
|
+
usage, head_size, transport, numeric_ids
|
|
342
|
+
):
|
|
134
343
|
client = _FakeClient()
|
|
135
344
|
client.usage = usage
|
|
136
345
|
renderer = _FakeRenderer()
|
|
346
|
+
sampling_params = {"temperature": 0.3, "max_tokens": 7, "min_tokens": 2}
|
|
347
|
+
if head_size is not None:
|
|
348
|
+
sampling_params["logprobs"] = head_size
|
|
349
|
+
for entry in client.choice["logprobs"]["content"]:
|
|
350
|
+
entry["top_logprobs"] = [
|
|
351
|
+
{"token": entry["token"], "logprob": entry["logprob"]},
|
|
352
|
+
{"token": "token_id:20", "logprob": -4.0},
|
|
353
|
+
{"token": "token_id:21", "logprob": -5.0},
|
|
354
|
+
{"token": "token_id:22", "logprob": -6.0},
|
|
355
|
+
]
|
|
356
|
+
if numeric_ids:
|
|
357
|
+
for entry in client.choice["logprobs"]["content"]:
|
|
358
|
+
for candidate in [entry, *entry.get("top_logprobs", [])]:
|
|
359
|
+
candidate["token_id"] = int(candidate.pop("token")[9:])
|
|
360
|
+
extra_args = (
|
|
361
|
+
{"prl_compact_logprobs": False, "custom_arg": 7}
|
|
362
|
+
if transport == "opt_out"
|
|
363
|
+
else None
|
|
364
|
+
)
|
|
365
|
+
if extra_args is not None:
|
|
366
|
+
sampling_params["extra_args"] = extra_args
|
|
367
|
+
if head_size is not None and transport == "compact":
|
|
368
|
+
client.choice = _compact_choice(client.choice)
|
|
137
369
|
|
|
138
370
|
result = asyncio.run(
|
|
139
371
|
generate(
|
|
@@ -142,7 +374,7 @@ def test_generate_builds_request_body_and_parses_response(usage):
|
|
|
142
374
|
messages=[{"role": "user", "content": "hi"}],
|
|
143
375
|
model="test-model",
|
|
144
376
|
tools=[{"type": "function", "function": {"name": "echo"}}],
|
|
145
|
-
sampling_params=
|
|
377
|
+
sampling_params=sampling_params,
|
|
146
378
|
cache_salt="ckpt-42",
|
|
147
379
|
)
|
|
148
380
|
)
|
|
@@ -167,7 +399,15 @@ def test_generate_builds_request_body_and_parses_response(usage):
|
|
|
167
399
|
"max_tokens": 7,
|
|
168
400
|
"min_tokens": 2,
|
|
169
401
|
"stop_token_ids": [99],
|
|
170
|
-
"logprobs": 1,
|
|
402
|
+
"logprobs": head_size + 2 if head_size is not None else 1,
|
|
403
|
+
**(
|
|
404
|
+
{
|
|
405
|
+
"extra_args": {"prl_compact_logprobs": True},
|
|
406
|
+
}
|
|
407
|
+
if head_size
|
|
408
|
+
else {}
|
|
409
|
+
),
|
|
410
|
+
**({"extra_args": extra_args} if extra_args is not None else {}),
|
|
171
411
|
"skip_special_tokens": False,
|
|
172
412
|
},
|
|
173
413
|
}
|
|
@@ -178,7 +418,17 @@ def test_generate_builds_request_body_and_parses_response(usage):
|
|
|
178
418
|
assert result["reasoning_content"] == "think"
|
|
179
419
|
assert result["prompt_ids"] == [1, 2, 3]
|
|
180
420
|
assert result["completion_ids"] == [7, 8]
|
|
181
|
-
|
|
421
|
+
np.testing.assert_allclose(result["completion_logprobs"], [-0.1, -0.2])
|
|
422
|
+
if head_size is not None:
|
|
423
|
+
assert result["completion_top_ids"] == [[7, 20], [8, 20]]
|
|
424
|
+
np.testing.assert_allclose(
|
|
425
|
+
result["completion_top_logprobs"], [[-0.1, -4.0], [-0.2, -4.0]]
|
|
426
|
+
)
|
|
427
|
+
else:
|
|
428
|
+
assert result["completion_top_ids"] is None
|
|
429
|
+
assert result["completion_top_logprobs"] is None
|
|
430
|
+
if extra_args is not None:
|
|
431
|
+
assert extra_args == {"prl_compact_logprobs": False, "custom_arg": 7}
|
|
182
432
|
assert result["routed_experts"]["shape"] == [2, 1, 1]
|
|
183
433
|
assert isinstance(result["routed_experts"]["data"], memoryview)
|
|
184
434
|
assert result["routed_experts"]["data"].tobytes() == base64.b64encode(b"\x01\x02")
|
|
@@ -316,9 +566,20 @@ def test_generate_rejects_vllm_missing_logprob_sentinel():
|
|
|
316
566
|
_run_generate(client)
|
|
317
567
|
|
|
318
568
|
|
|
319
|
-
|
|
569
|
+
@pytest.mark.parametrize(
|
|
570
|
+
"identity",
|
|
571
|
+
[
|
|
572
|
+
{"token": "token_id:8"},
|
|
573
|
+
{"token_id": 8},
|
|
574
|
+
{"token_id": True},
|
|
575
|
+
{"token_id": -1},
|
|
576
|
+
{"token_id": "7"},
|
|
577
|
+
{"token_id": None},
|
|
578
|
+
],
|
|
579
|
+
)
|
|
580
|
+
def test_generate_rejects_logprob_token_id_mismatch(identity):
|
|
320
581
|
client = _FakeClient()
|
|
321
|
-
client.choice["logprobs"]["content"][0]
|
|
582
|
+
client.choice["logprobs"]["content"][0].update(identity)
|
|
322
583
|
|
|
323
584
|
with pytest.raises(
|
|
324
585
|
MalformedGenerateResponseError,
|
|
@@ -494,6 +755,7 @@ def test_generate_serializes_multimodal_features_for_qwen_vl_family(
|
|
|
494
755
|
pytest.importorskip("vllm", reason="vllm needed for features serialization")
|
|
495
756
|
|
|
496
757
|
import torch as _torch
|
|
758
|
+
|
|
497
759
|
from renderers.base import (
|
|
498
760
|
MultiModalData,
|
|
499
761
|
PlaceholderRange,
|
|
@@ -570,6 +832,7 @@ def test_generate_serializes_multimodal_features_for_gemma4():
|
|
|
570
832
|
pytest.importorskip("vllm", reason="vllm needed for features serialization")
|
|
571
833
|
|
|
572
834
|
import torch as _torch
|
|
835
|
+
|
|
573
836
|
from renderers.base import MultiModalData, PlaceholderRange, load_tokenizer
|
|
574
837
|
from renderers.gemma4 import Gemma4Renderer
|
|
575
838
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/examples/sglang/multiturn_generate_sglang.py
RENAMED
|
File without changes
|
|
File without changes
|
{renderers-0.1.13.dev8 → renderers-0.1.13.dev13}/examples/tinker/multiturn_generate_tinker.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|