renderers 0.1.11.dev5__tar.gz → 0.1.12.dev2__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.11.dev5 → renderers-0.1.12.dev2}/PKG-INFO +1 -1
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/renderers/_version.py +2 -2
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/renderers/base.py +1 -1
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/renderers/client.py +92 -18
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/renderers/kimi_k25.py +21 -5
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/renderers/qwen35.py +44 -26
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/renderers/qwen3_vl.py +45 -26
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/tests/test_client.py +46 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/.github/workflows/publish-dev.yml +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/.github/workflows/publish.yml +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/.github/workflows/style.yml +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/.github/workflows/test.yml +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/.gitignore +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/.pre-commit-config.yaml +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/LICENSE +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/README.md +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/docs/renderer-config.md +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/examples/README.md +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/examples/sglang/multiturn_generate_sglang.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/examples/sglang/online_multiturn_sglang.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/examples/tinker/multiturn_generate_tinker.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/examples/transformers/multiturn_generate_transformers.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/examples/vllm/multiturn_generate_vllm.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/pyproject.toml +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/renderers/__init__.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/renderers/configs.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/renderers/deepseek_r1.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/renderers/deepseek_v3.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/renderers/deepseek_v4.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/renderers/default.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/renderers/gemma4.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/renderers/glm45.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/renderers/glm5.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/renderers/gpt_oss.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/renderers/hy3.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/renderers/inkling.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/renderers/kimi_k2.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/renderers/laguna_s21.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/renderers/laguna_xs2.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/renderers/llama_3.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/renderers/minimax_m2.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/renderers/nemotron3.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/renderers/parsers.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/renderers/parsing.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/renderers/prime_qwen3.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/renderers/qwen3.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/renderers/qwen36.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/renderers/qwen38.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/tests/conftest.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/tests/parity.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/tests/reference_rendering.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/tests/test_bridge.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/tests/test_build_helpers.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/tests/test_deepseek_r1.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/tests/test_deepseek_v4.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/tests/test_disabled_thinking_stability.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/tests/test_gemma4.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/tests/test_glm_tool_name_validation.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/tests/test_hy3.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/tests/test_incremental.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/tests/test_inkling.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/tests/test_is_content.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/tests/test_kimi_k25_tool_schema.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/tests/test_laguna_m1.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/tests/test_laguna_xs21.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/tests/test_llama_3.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/tests/test_load_tokenizer.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/tests/test_message_indices.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/tests/test_message_tool_names.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/tests/test_multimodal.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/tests/test_nemotron3_ultra.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/tests/test_offsetless_tokenizers.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/tests/test_parity.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/tests/test_parse_response.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/tests/test_parse_response_robustness.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/tests/test_parsers.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/tests/test_preserve_thinking.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/tests/test_qwen35_size_coverage.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/tests/test_qwen38.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/tests/test_renderer_config.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/tests/test_roundtrip.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/tests/test_sampled_mask.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/tests/test_tokens_per_message.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/tests/test_tool_arg_type_preservation.py +0 -0
- {renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/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.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 1,
|
|
21
|
+
__version__ = version = '0.1.12.dev2'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 12, 'dev2')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -2095,7 +2095,7 @@ def build_trajectory_step(
|
|
|
2095
2095
|
"completion_mask": [True] * len(completion_ids),
|
|
2096
2096
|
"completion_logprobs": [0.0] * len(completion_ids),
|
|
2097
2097
|
"routed_experts": None,
|
|
2098
|
-
"
|
|
2098
|
+
"sampling_mask": None,
|
|
2099
2099
|
}
|
|
2100
2100
|
if (
|
|
2101
2101
|
full_rendered.multi_modal_data is not None
|
|
@@ -28,7 +28,6 @@ from renderers.base import (
|
|
|
28
28
|
|
|
29
29
|
_request_logger = logging.getLogger("renderers.client")
|
|
30
30
|
ROUTED_EXPERTS_DATA_PREFIX = b'"routed_experts":{"data":"'
|
|
31
|
-
KEPT_TOKENS_IDS_PREFIX = b'"kept_tokens":{"ids":"'
|
|
32
31
|
# vLLM uses this value both when sampled-token evidence is missing and as a
|
|
33
32
|
# lower-bound clamp, so receiving it cannot prove the real logprob was returned.
|
|
34
33
|
VLLM_LOGPROB_SENTINEL = -9999.0
|
|
@@ -129,12 +128,9 @@ def _strip_base64_field(raw: bytes, prefix: bytes) -> tuple[bytes, memoryview |
|
|
|
129
128
|
|
|
130
129
|
def parse_generate_response(raw: bytes) -> dict[str, Any]:
|
|
131
130
|
stripped, routed_data = _strip_base64_field(raw, ROUTED_EXPERTS_DATA_PREFIX)
|
|
132
|
-
stripped, kept_ids_data = _strip_base64_field(stripped, KEPT_TOKENS_IDS_PREFIX)
|
|
133
131
|
payload: dict[str, Any] = json.loads(stripped)
|
|
134
132
|
if routed_data is not None:
|
|
135
133
|
payload["choices"][0]["routed_experts"]["data"] = routed_data
|
|
136
|
-
if kept_ids_data is not None:
|
|
137
|
-
payload["choices"][0]["kept_tokens"]["ids"] = kept_ids_data
|
|
138
134
|
return payload
|
|
139
135
|
|
|
140
136
|
|
|
@@ -207,6 +203,7 @@ async def generate(
|
|
|
207
203
|
priority: int | None = None,
|
|
208
204
|
extra_headers: dict[str, str] | None = None,
|
|
209
205
|
max_prompt_len: int | None = None,
|
|
206
|
+
process_multimodal: bool = True,
|
|
210
207
|
) -> dict[str, Any]:
|
|
211
208
|
"""Tokenize messages, call vLLM /inference/v1/generate, parse the response.
|
|
212
209
|
|
|
@@ -226,6 +223,9 @@ async def generate(
|
|
|
226
223
|
sidecar, then serializes it to vLLM's ``features`` schema (mm_hashes,
|
|
227
224
|
mm_placeholders, kwargs_data) before POSTing. The serializer imports
|
|
228
225
|
``vllm.*`` lazily so text-only consumers never pay for the import.
|
|
226
|
+
With ``process_multimodal=False``, rendering skips image processing and
|
|
227
|
+
the request carries ``content_parts`` instead; vLLM must return the
|
|
228
|
+
expanded prompt as ``prompt_token_ids``.
|
|
229
229
|
|
|
230
230
|
``max_prompt_len`` controls the pre-flight overflow check. When the
|
|
231
231
|
rendered prompt is strictly longer than the cap, the request is never
|
|
@@ -236,10 +236,15 @@ async def generate(
|
|
|
236
236
|
cache a ``None`` cap and the pre-flight silently disables. Engine 4xx
|
|
237
237
|
that still slip through propagate raw — converting them into a domain
|
|
238
238
|
error is the calling client's job (its error shape is engine-specific).
|
|
239
|
+
Calls with ``process_multimodal=False`` skip this pre-flight because only
|
|
240
|
+
vLLM knows the expanded prompt length.
|
|
239
241
|
|
|
240
|
-
Returns a dict with: request_id, prompt_ids,
|
|
241
|
-
|
|
242
|
-
|
|
242
|
+
Returns a dict with: request_id, prompt_ids, renderer_prompt_ids,
|
|
243
|
+
mm_placeholders, completion_ids, completion_logprobs, content,
|
|
244
|
+
reasoning_content, tool_calls, finish_reason, routed_experts,
|
|
245
|
+
multi_modal_data, prompt_attribution. ``renderer_prompt_ids`` is the
|
|
246
|
+
unexpanded logical prompt when ``process_multimodal=False`` and ``None``
|
|
247
|
+
otherwise.
|
|
243
248
|
|
|
244
249
|
``prompt_attribution`` is the renderer's :class:`RenderedTokens` for
|
|
245
250
|
the prompt — either the one this call computed via
|
|
@@ -258,6 +263,12 @@ async def generate(
|
|
|
258
263
|
f"{type(renderer).__name__} does not support tools. "
|
|
259
264
|
"Choose a model-specific renderer instead of the default fallback."
|
|
260
265
|
)
|
|
266
|
+
if not process_multimodal and not getattr(
|
|
267
|
+
renderer, "supports_process_multimodal", False
|
|
268
|
+
):
|
|
269
|
+
raise NotImplementedError(
|
|
270
|
+
f"{type(renderer).__name__} does not support process_multimodal=False"
|
|
271
|
+
)
|
|
261
272
|
|
|
262
273
|
def _prepare():
|
|
263
274
|
if prompt_ids is not None:
|
|
@@ -270,7 +281,15 @@ async def generate(
|
|
|
270
281
|
multi_modal_data,
|
|
271
282
|
prompt_attribution,
|
|
272
283
|
)
|
|
273
|
-
|
|
284
|
+
render_kwargs: dict[str, Any] = {}
|
|
285
|
+
if not process_multimodal:
|
|
286
|
+
render_kwargs["process_multimodal"] = False
|
|
287
|
+
rendered = renderer.render(
|
|
288
|
+
messages,
|
|
289
|
+
tools=tools,
|
|
290
|
+
add_generation_prompt=True,
|
|
291
|
+
**render_kwargs,
|
|
292
|
+
)
|
|
274
293
|
return (
|
|
275
294
|
rendered.token_ids,
|
|
276
295
|
renderer.get_stop_token_ids(),
|
|
@@ -280,12 +299,13 @@ async def generate(
|
|
|
280
299
|
|
|
281
300
|
prompt_ids, stop_token_ids, mm_data, prompt_attr = _prepare()
|
|
282
301
|
|
|
283
|
-
if
|
|
284
|
-
max_prompt_len
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
302
|
+
if process_multimodal:
|
|
303
|
+
if max_prompt_len is None:
|
|
304
|
+
max_prompt_len = await _resolve_max_prompt_len(client, model)
|
|
305
|
+
if max_prompt_len is not None and len(prompt_ids) > max_prompt_len:
|
|
306
|
+
raise OverlongPromptError(
|
|
307
|
+
prompt_len=len(prompt_ids), max_prompt_len=max_prompt_len
|
|
308
|
+
)
|
|
289
309
|
|
|
290
310
|
sp: dict[str, Any] = dict(sampling_params or {})
|
|
291
311
|
sp["stop_token_ids"] = stop_token_ids
|
|
@@ -297,11 +317,14 @@ async def generate(
|
|
|
297
317
|
"token_ids": prompt_ids,
|
|
298
318
|
"sampling_params": sp,
|
|
299
319
|
}
|
|
320
|
+
content_parts = _content_parts(messages) if not process_multimodal else None
|
|
300
321
|
features = (
|
|
301
322
|
_build_mm_features(renderer, mm_data)
|
|
302
|
-
if mm_data and not mm_data.is_empty()
|
|
323
|
+
if process_multimodal and mm_data and not mm_data.is_empty()
|
|
303
324
|
else None
|
|
304
325
|
)
|
|
326
|
+
if content_parts:
|
|
327
|
+
body["content_parts"] = content_parts
|
|
305
328
|
if features is not None:
|
|
306
329
|
body["features"] = features
|
|
307
330
|
if cache_salt is not None:
|
|
@@ -331,13 +354,25 @@ async def generate(
|
|
|
331
354
|
|
|
332
355
|
choice = (data.get("choices") or [{}])[0]
|
|
333
356
|
completion_ids = choice.get("token_ids") or []
|
|
357
|
+
effective_prompt_ids = data.get("prompt_token_ids")
|
|
358
|
+
if content_parts and not isinstance(effective_prompt_ids, list):
|
|
359
|
+
raise MalformedGenerateResponseError(
|
|
360
|
+
"Engine response must include prompt_token_ids when process_multimodal=False."
|
|
361
|
+
)
|
|
362
|
+
mm_placeholders = data.get("mm_placeholders")
|
|
363
|
+
if content_parts and not isinstance(mm_placeholders, dict):
|
|
364
|
+
raise MalformedGenerateResponseError(
|
|
365
|
+
"Engine response must include mm_placeholders when process_multimodal=False."
|
|
366
|
+
)
|
|
334
367
|
|
|
335
368
|
completion_logprobs = _parse_completion_logprobs(choice, completion_ids)
|
|
336
369
|
|
|
337
370
|
parsed = renderer.parse_response(completion_ids, tools=tools)
|
|
338
371
|
|
|
339
372
|
routed_experts = choice.get("routed_experts")
|
|
340
|
-
|
|
373
|
+
# vLLM's native kept-set sampling masks (``--return-sampling-mask``):
|
|
374
|
+
# one list of surviving vocab ids per completion token.
|
|
375
|
+
sampling_mask = choice.get("sampling_mask")
|
|
341
376
|
|
|
342
377
|
# /inference/v1/generate returns finish_reason in {"stop","length",...} —
|
|
343
378
|
# never "tool_calls" (a chat-completions concept). Promote stop→tool_calls
|
|
@@ -356,7 +391,9 @@ async def generate(
|
|
|
356
391
|
|
|
357
392
|
return {
|
|
358
393
|
"request_id": data.get("request_id") or "",
|
|
359
|
-
"prompt_ids": list(prompt_ids),
|
|
394
|
+
"prompt_ids": list(effective_prompt_ids or prompt_ids),
|
|
395
|
+
"renderer_prompt_ids": list(prompt_ids) if content_parts else None,
|
|
396
|
+
"mm_placeholders": mm_placeholders,
|
|
360
397
|
"completion_ids": list(completion_ids),
|
|
361
398
|
"completion_logprobs": completion_logprobs,
|
|
362
399
|
"content": parsed.content,
|
|
@@ -364,7 +401,7 @@ async def generate(
|
|
|
364
401
|
"tool_calls": parsed.tool_calls,
|
|
365
402
|
"finish_reason": finish_reason,
|
|
366
403
|
"routed_experts": routed_experts,
|
|
367
|
-
"
|
|
404
|
+
"sampling_mask": sampling_mask,
|
|
368
405
|
# The mm sidecar consumed on the request side, surfaced back so
|
|
369
406
|
# callers can persist it on the trajectory step for downstream
|
|
370
407
|
# multi-turn bridging and training-sample construction.
|
|
@@ -380,6 +417,43 @@ async def generate(
|
|
|
380
417
|
}
|
|
381
418
|
|
|
382
419
|
|
|
420
|
+
_MEDIA_URL_TYPES = {
|
|
421
|
+
"image": "image_url",
|
|
422
|
+
"image_url": "image_url",
|
|
423
|
+
"audio": "audio_url",
|
|
424
|
+
"audio_url": "audio_url",
|
|
425
|
+
"video": "video_url",
|
|
426
|
+
"video_url": "video_url",
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def _content_parts(messages: list[Message]) -> list[dict[str, Any]]:
|
|
431
|
+
"""Flatten raw media in prompt order for vLLM's token generate endpoint."""
|
|
432
|
+
parts: list[dict[str, Any]] = []
|
|
433
|
+
for message in messages:
|
|
434
|
+
content = message.get("content")
|
|
435
|
+
if not isinstance(content, list):
|
|
436
|
+
continue
|
|
437
|
+
for part in content:
|
|
438
|
+
if not isinstance(part, Mapping):
|
|
439
|
+
continue
|
|
440
|
+
part_type = part.get("type")
|
|
441
|
+
if part_type is None:
|
|
442
|
+
part_type = next(
|
|
443
|
+
(key for key in _MEDIA_URL_TYPES if part.get(key)), None
|
|
444
|
+
)
|
|
445
|
+
if not isinstance(part_type, str) or part_type not in _MEDIA_URL_TYPES:
|
|
446
|
+
continue
|
|
447
|
+
source = part.get(part_type)
|
|
448
|
+
if source is None:
|
|
449
|
+
source = part.get(_MEDIA_URL_TYPES[part_type]) or part.get("url")
|
|
450
|
+
url = source.get("url") if isinstance(source, Mapping) else source
|
|
451
|
+
if not isinstance(url, str) or not url:
|
|
452
|
+
raise ValueError(f"{part_type} content part is missing a URL")
|
|
453
|
+
parts.append({"type": _MEDIA_URL_TYPES[part_type], "url": url})
|
|
454
|
+
return parts
|
|
455
|
+
|
|
456
|
+
|
|
383
457
|
def _build_mm_features(
|
|
384
458
|
renderer: Renderer,
|
|
385
459
|
mm_data: MultiModalData,
|
|
@@ -591,6 +591,8 @@ class KimiK25Renderer:
|
|
|
591
591
|
The tokenizer should be ``moonshotai/Kimi-K2-Instruct`` (same as K2).
|
|
592
592
|
"""
|
|
593
593
|
|
|
594
|
+
supports_process_multimodal = True
|
|
595
|
+
|
|
594
596
|
def __init__(
|
|
595
597
|
self,
|
|
596
598
|
tokenizer: Tokenizer,
|
|
@@ -734,6 +736,7 @@ class KimiK25Renderer:
|
|
|
734
736
|
*,
|
|
735
737
|
tools: list[ToolSpec] | None = None,
|
|
736
738
|
add_generation_prompt: bool = False,
|
|
739
|
+
process_multimodal: bool = True,
|
|
737
740
|
) -> RenderedTokens:
|
|
738
741
|
"""Render messages to tokens, matching the K2.5 chat template.
|
|
739
742
|
|
|
@@ -821,7 +824,10 @@ class KimiK25Renderer:
|
|
|
821
824
|
``<|media_content|>``, ``<|media_end|>``, the trailing
|
|
822
825
|
``\\n``) are template-injected scaffold.
|
|
823
826
|
"""
|
|
824
|
-
|
|
827
|
+
if process_multimodal:
|
|
828
|
+
_, out, _num_patches, h = self._process_image(part)
|
|
829
|
+
else:
|
|
830
|
+
out = h = None
|
|
825
831
|
emit_special(
|
|
826
832
|
self._media_begin, msg_idx, is_sampled=is_sampled, is_content=False
|
|
827
833
|
)
|
|
@@ -840,6 +846,9 @@ class KimiK25Renderer:
|
|
|
840
846
|
self._media_end, msg_idx, is_sampled=is_sampled, is_content=False
|
|
841
847
|
)
|
|
842
848
|
emit_text("\n", msg_idx, is_sampled=is_sampled, is_content=False)
|
|
849
|
+
if not process_multimodal:
|
|
850
|
+
return
|
|
851
|
+
assert out is not None and h is not None
|
|
843
852
|
mm_hashes.setdefault("image", []).append(h)
|
|
844
853
|
mm_placeholders.setdefault("image", []).append(
|
|
845
854
|
PlaceholderRange(offset=offset, length=1)
|
|
@@ -1027,6 +1036,7 @@ class KimiK25Renderer:
|
|
|
1027
1036
|
*,
|
|
1028
1037
|
tools: list[ToolSpec] | None = None,
|
|
1029
1038
|
previous_multi_modal_data: MultiModalData | None = None,
|
|
1039
|
+
process_multimodal: bool = True,
|
|
1030
1040
|
) -> "RenderedTokens | None":
|
|
1031
1041
|
if (
|
|
1032
1042
|
not previous_prompt_ids
|
|
@@ -1115,7 +1125,10 @@ class KimiK25Renderer:
|
|
|
1115
1125
|
is_sampled: bool = False,
|
|
1116
1126
|
is_content: bool = False,
|
|
1117
1127
|
) -> None:
|
|
1118
|
-
|
|
1128
|
+
if process_multimodal:
|
|
1129
|
+
_, out, _num_patches, h = self._process_image(part)
|
|
1130
|
+
else:
|
|
1131
|
+
out = h = None
|
|
1119
1132
|
emit_special(self._media_begin, msg_idx)
|
|
1120
1133
|
emit_text("image", msg_idx)
|
|
1121
1134
|
emit_special(self._media_content, msg_idx)
|
|
@@ -1125,6 +1138,9 @@ class KimiK25Renderer:
|
|
|
1125
1138
|
emit_special(self._media_pad, msg_idx, is_content=is_content)
|
|
1126
1139
|
emit_special(self._media_end, msg_idx)
|
|
1127
1140
|
emit_text("\n", msg_idx)
|
|
1141
|
+
if not process_multimodal:
|
|
1142
|
+
return
|
|
1143
|
+
assert out is not None and h is not None
|
|
1128
1144
|
new_hashes.setdefault("image", []).append(h)
|
|
1129
1145
|
new_placeholders.setdefault("image", []).append(
|
|
1130
1146
|
PlaceholderRange(offset=offset, length=1)
|
|
@@ -1188,17 +1204,17 @@ class KimiK25Renderer:
|
|
|
1188
1204
|
# below never mutates the caller's previous_multi_modal_data.
|
|
1189
1205
|
merged_hashes: dict[str, list[str]] = (
|
|
1190
1206
|
{k: list(v) for k, v in previous_multi_modal_data.mm_hashes.items()}
|
|
1191
|
-
if previous_multi_modal_data
|
|
1207
|
+
if process_multimodal and previous_multi_modal_data
|
|
1192
1208
|
else {}
|
|
1193
1209
|
)
|
|
1194
1210
|
merged_placeholders: dict[str, list[PlaceholderRange]] = (
|
|
1195
1211
|
{k: list(v) for k, v in previous_multi_modal_data.mm_placeholders.items()}
|
|
1196
|
-
if previous_multi_modal_data
|
|
1212
|
+
if process_multimodal and previous_multi_modal_data
|
|
1197
1213
|
else {}
|
|
1198
1214
|
)
|
|
1199
1215
|
merged_items: dict[str, list[dict[str, Any]]] = (
|
|
1200
1216
|
{k: list(v) for k, v in previous_multi_modal_data.mm_items.items()}
|
|
1201
|
-
if previous_multi_modal_data
|
|
1217
|
+
if process_multimodal and previous_multi_modal_data
|
|
1202
1218
|
else {}
|
|
1203
1219
|
)
|
|
1204
1220
|
for modality, vals in new_hashes.items():
|
|
@@ -123,6 +123,7 @@ def _default_enable_thinking(tokenizer) -> bool:
|
|
|
123
123
|
class Qwen35Renderer:
|
|
124
124
|
"""Deterministic message → token renderer for Qwen3.5 models."""
|
|
125
125
|
|
|
126
|
+
supports_process_multimodal = True
|
|
126
127
|
_config_cls: type = Qwen35RendererConfig
|
|
127
128
|
|
|
128
129
|
def __init__(
|
|
@@ -353,6 +354,7 @@ class Qwen35Renderer:
|
|
|
353
354
|
*,
|
|
354
355
|
tools: list[ToolSpec] | None = None,
|
|
355
356
|
add_generation_prompt: bool = False,
|
|
357
|
+
process_multimodal: bool = True,
|
|
356
358
|
) -> RenderedTokens:
|
|
357
359
|
if not messages:
|
|
358
360
|
raise ValueError("No messages provided.")
|
|
@@ -424,7 +426,11 @@ class Qwen35Renderer:
|
|
|
424
426
|
# image data, so they ARE body content (is_content=True);
|
|
425
427
|
# the surrounding ``<|vision_start|>`` / ``<|vision_end|>``
|
|
426
428
|
# specials are template scaffold.
|
|
427
|
-
|
|
429
|
+
if process_multimodal:
|
|
430
|
+
_, out, n, h = self._process_image(part)
|
|
431
|
+
else:
|
|
432
|
+
out = h = None
|
|
433
|
+
n = 1
|
|
428
434
|
vision_counts["image"] += 1
|
|
429
435
|
if self.config.add_vision_id:
|
|
430
436
|
emit_text(
|
|
@@ -442,16 +448,18 @@ class Qwen35Renderer:
|
|
|
442
448
|
self._image_pad, msg_idx, is_sampled=False, is_content=True
|
|
443
449
|
)
|
|
444
450
|
emit_special(self._vision_end, msg_idx, is_sampled=False, is_content=False)
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
451
|
+
if process_multimodal:
|
|
452
|
+
assert out is not None and h is not None
|
|
453
|
+
mm_hashes.setdefault("image", []).append(h)
|
|
454
|
+
mm_placeholders.setdefault("image", []).append(
|
|
455
|
+
PlaceholderRange(offset=offset, length=n)
|
|
456
|
+
)
|
|
457
|
+
mm_items.setdefault("image", []).append(
|
|
458
|
+
{
|
|
459
|
+
"pixel_values": out["pixel_values"],
|
|
460
|
+
"image_grid_thw": out["image_grid_thw"],
|
|
461
|
+
}
|
|
462
|
+
)
|
|
455
463
|
|
|
456
464
|
def emit_user_with_media(content_list: list[Any], msg_idx: int) -> None:
|
|
457
465
|
"""Emit a user message whose content list contains image parts.
|
|
@@ -685,6 +693,7 @@ class Qwen35Renderer:
|
|
|
685
693
|
*,
|
|
686
694
|
tools: list[ToolSpec] | None = None,
|
|
687
695
|
previous_multi_modal_data: MultiModalData | None = None,
|
|
696
|
+
process_multimodal: bool = True,
|
|
688
697
|
) -> "RenderedTokens | None":
|
|
689
698
|
if (
|
|
690
699
|
not previous_prompt_ids
|
|
@@ -721,6 +730,7 @@ class Qwen35Renderer:
|
|
|
721
730
|
# scratch correctly.
|
|
722
731
|
if (
|
|
723
732
|
self.config.add_vision_id
|
|
733
|
+
and process_multimodal
|
|
724
734
|
and previous_multi_modal_data is None
|
|
725
735
|
and self._vision_start in previous_ids
|
|
726
736
|
):
|
|
@@ -751,9 +761,11 @@ class Qwen35Renderer:
|
|
|
751
761
|
# ``add_vision_id`` parity across turns.
|
|
752
762
|
prev_image_count = 0
|
|
753
763
|
prev_video_count = 0
|
|
754
|
-
if previous_multi_modal_data is not None:
|
|
764
|
+
if process_multimodal and previous_multi_modal_data is not None:
|
|
755
765
|
prev_image_count = len(previous_multi_modal_data.mm_items.get("image", []))
|
|
756
766
|
prev_video_count = len(previous_multi_modal_data.mm_items.get("video", []))
|
|
767
|
+
elif not process_multimodal:
|
|
768
|
+
prev_image_count = previous_ids.count(self._image_pad)
|
|
757
769
|
vision_counts = {"image": prev_image_count, "video": prev_video_count}
|
|
758
770
|
|
|
759
771
|
def emit_special(
|
|
@@ -796,7 +808,11 @@ class Qwen35Renderer:
|
|
|
796
808
|
content_mask.append(is_content)
|
|
797
809
|
|
|
798
810
|
def emit_image(part: dict[str, Any], msg_idx: int = -1) -> None:
|
|
799
|
-
|
|
811
|
+
if process_multimodal:
|
|
812
|
+
_, out, n, h = self._process_image(part)
|
|
813
|
+
else:
|
|
814
|
+
out = h = None
|
|
815
|
+
n = 1
|
|
800
816
|
vision_counts["image"] += 1
|
|
801
817
|
if self.config.add_vision_id:
|
|
802
818
|
emit_text(f"Picture {vision_counts['image']}: ", msg_idx)
|
|
@@ -805,16 +821,18 @@ class Qwen35Renderer:
|
|
|
805
821
|
for _ in range(n):
|
|
806
822
|
emit_special(self._image_pad, msg_idx, is_content=True)
|
|
807
823
|
emit_special(self._vision_end, msg_idx)
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
824
|
+
if process_multimodal:
|
|
825
|
+
assert out is not None and h is not None
|
|
826
|
+
new_hashes.setdefault("image", []).append(h)
|
|
827
|
+
new_placeholders.setdefault("image", []).append(
|
|
828
|
+
PlaceholderRange(offset=offset, length=n)
|
|
829
|
+
)
|
|
830
|
+
new_items.setdefault("image", []).append(
|
|
831
|
+
{
|
|
832
|
+
"pixel_values": out["pixel_values"],
|
|
833
|
+
"image_grid_thw": out["image_grid_thw"],
|
|
834
|
+
}
|
|
835
|
+
)
|
|
818
836
|
|
|
819
837
|
def emit_user_with_media(content_list: list[Any], msg_idx: int) -> None:
|
|
820
838
|
emit_special(self._im_start, msg_idx)
|
|
@@ -905,17 +923,17 @@ class Qwen35Renderer:
|
|
|
905
923
|
# below never mutates the caller's previous_multi_modal_data.
|
|
906
924
|
merged_hashes: dict[str, list[str]] = (
|
|
907
925
|
{k: list(v) for k, v in previous_multi_modal_data.mm_hashes.items()}
|
|
908
|
-
if previous_multi_modal_data
|
|
926
|
+
if process_multimodal and previous_multi_modal_data
|
|
909
927
|
else {}
|
|
910
928
|
)
|
|
911
929
|
merged_placeholders: dict[str, list[PlaceholderRange]] = (
|
|
912
930
|
{k: list(v) for k, v in previous_multi_modal_data.mm_placeholders.items()}
|
|
913
|
-
if previous_multi_modal_data
|
|
931
|
+
if process_multimodal and previous_multi_modal_data
|
|
914
932
|
else {}
|
|
915
933
|
)
|
|
916
934
|
merged_items: dict[str, list[dict[str, Any]]] = (
|
|
917
935
|
{k: list(v) for k, v in previous_multi_modal_data.mm_items.items()}
|
|
918
|
-
if previous_multi_modal_data
|
|
936
|
+
if process_multimodal and previous_multi_modal_data
|
|
919
937
|
else {}
|
|
920
938
|
)
|
|
921
939
|
for modality, vals in new_hashes.items():
|
|
@@ -310,6 +310,8 @@ class Qwen3VLRenderer:
|
|
|
310
310
|
``thinking_retention`` still controls whether the bridge is attempted.
|
|
311
311
|
"""
|
|
312
312
|
|
|
313
|
+
supports_process_multimodal = True
|
|
314
|
+
|
|
313
315
|
def __init__(
|
|
314
316
|
self,
|
|
315
317
|
tokenizer: Tokenizer,
|
|
@@ -457,6 +459,7 @@ class Qwen3VLRenderer:
|
|
|
457
459
|
*,
|
|
458
460
|
tools: list[ToolSpec] | None = None,
|
|
459
461
|
add_generation_prompt: bool = False,
|
|
462
|
+
process_multimodal: bool = True,
|
|
460
463
|
) -> RenderedTokens:
|
|
461
464
|
if not messages:
|
|
462
465
|
raise ValueError("No messages provided.")
|
|
@@ -480,7 +483,11 @@ class Qwen3VLRenderer:
|
|
|
480
483
|
# image data, so they ARE body content (is_content=True);
|
|
481
484
|
# the surrounding ``<|vision_start|>`` / ``<|vision_end|>``
|
|
482
485
|
# markers are renderer-emitted scaffold.
|
|
483
|
-
|
|
486
|
+
if process_multimodal:
|
|
487
|
+
_, out, n, h = self._process_image(part)
|
|
488
|
+
else:
|
|
489
|
+
out = h = None
|
|
490
|
+
n = 1
|
|
484
491
|
vision_counts["image"] += 1
|
|
485
492
|
if self.config.add_vision_id:
|
|
486
493
|
em.text(
|
|
@@ -493,16 +500,18 @@ class Qwen3VLRenderer:
|
|
|
493
500
|
for _ in range(n):
|
|
494
501
|
em.special(self._image_pad, is_sampled=False, is_content=True)
|
|
495
502
|
em.special(self._vision_end, is_sampled=False, is_content=False)
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
503
|
+
if process_multimodal:
|
|
504
|
+
assert out is not None and h is not None
|
|
505
|
+
mm_hashes.setdefault("image", []).append(h)
|
|
506
|
+
mm_placeholders.setdefault("image", []).append(
|
|
507
|
+
PlaceholderRange(offset=offset, length=n)
|
|
508
|
+
)
|
|
509
|
+
mm_items.setdefault("image", []).append(
|
|
510
|
+
{
|
|
511
|
+
"pixel_values": out["pixel_values"],
|
|
512
|
+
"image_grid_thw": out["image_grid_thw"],
|
|
513
|
+
}
|
|
514
|
+
)
|
|
506
515
|
|
|
507
516
|
def render_media_content(content: Any) -> None:
|
|
508
517
|
"""Emit a user/tool content list with media handled inline.
|
|
@@ -665,6 +674,7 @@ class Qwen3VLRenderer:
|
|
|
665
674
|
*,
|
|
666
675
|
tools: list[ToolSpec] | None = None,
|
|
667
676
|
previous_multi_modal_data: MultiModalData | None = None,
|
|
677
|
+
process_multimodal: bool = True,
|
|
668
678
|
) -> "RenderedTokens | None":
|
|
669
679
|
"""Extend the prior turn with ``new_messages``.
|
|
670
680
|
|
|
@@ -709,6 +719,7 @@ class Qwen3VLRenderer:
|
|
|
709
719
|
# correctly.
|
|
710
720
|
if (
|
|
711
721
|
self.config.add_vision_id
|
|
722
|
+
and process_multimodal
|
|
712
723
|
and previous_multi_modal_data is None
|
|
713
724
|
and self._vision_start in previous_ids
|
|
714
725
|
):
|
|
@@ -746,13 +757,19 @@ class Qwen3VLRenderer:
|
|
|
746
757
|
# ``add_vision_id`` parity across turns.
|
|
747
758
|
prev_image_count = 0
|
|
748
759
|
prev_video_count = 0
|
|
749
|
-
if previous_multi_modal_data is not None:
|
|
760
|
+
if process_multimodal and previous_multi_modal_data is not None:
|
|
750
761
|
prev_image_count = len(previous_multi_modal_data.mm_items.get("image", []))
|
|
751
762
|
prev_video_count = len(previous_multi_modal_data.mm_items.get("video", []))
|
|
763
|
+
elif not process_multimodal:
|
|
764
|
+
prev_image_count = previous_ids.count(self._image_pad)
|
|
752
765
|
vision_counts = {"image": prev_image_count, "video": prev_video_count}
|
|
753
766
|
|
|
754
767
|
def emit_image(part: dict[str, Any]) -> None:
|
|
755
|
-
|
|
768
|
+
if process_multimodal:
|
|
769
|
+
_, out, n, h = self._process_image(part)
|
|
770
|
+
else:
|
|
771
|
+
out = h = None
|
|
772
|
+
n = 1
|
|
756
773
|
vision_counts["image"] += 1
|
|
757
774
|
if self.config.add_vision_id:
|
|
758
775
|
em.text(
|
|
@@ -765,16 +782,18 @@ class Qwen3VLRenderer:
|
|
|
765
782
|
for _ in range(n):
|
|
766
783
|
em.special(self._image_pad, is_sampled=False, is_content=True)
|
|
767
784
|
em.special(self._vision_end, is_sampled=False, is_content=False)
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
785
|
+
if process_multimodal:
|
|
786
|
+
assert out is not None and h is not None
|
|
787
|
+
new_hashes.setdefault("image", []).append(h)
|
|
788
|
+
new_placeholders.setdefault("image", []).append(
|
|
789
|
+
PlaceholderRange(offset=offset, length=n)
|
|
790
|
+
)
|
|
791
|
+
new_items.setdefault("image", []).append(
|
|
792
|
+
{
|
|
793
|
+
"pixel_values": out["pixel_values"],
|
|
794
|
+
"image_grid_thw": out["image_grid_thw"],
|
|
795
|
+
}
|
|
796
|
+
)
|
|
778
797
|
|
|
779
798
|
def render_media_content(content: Any) -> None:
|
|
780
799
|
if isinstance(content, str):
|
|
@@ -833,17 +852,17 @@ class Qwen3VLRenderer:
|
|
|
833
852
|
# caller's previous_multi_modal_data.
|
|
834
853
|
merged_hashes = (
|
|
835
854
|
{k: list(v) for k, v in previous_multi_modal_data.mm_hashes.items()}
|
|
836
|
-
if previous_multi_modal_data
|
|
855
|
+
if process_multimodal and previous_multi_modal_data
|
|
837
856
|
else {}
|
|
838
857
|
)
|
|
839
858
|
merged_placeholders = (
|
|
840
859
|
{k: list(v) for k, v in previous_multi_modal_data.mm_placeholders.items()}
|
|
841
|
-
if previous_multi_modal_data
|
|
860
|
+
if process_multimodal and previous_multi_modal_data
|
|
842
861
|
else {}
|
|
843
862
|
)
|
|
844
863
|
merged_items = (
|
|
845
864
|
{k: list(v) for k, v in previous_multi_modal_data.mm_items.items()}
|
|
846
|
-
if previous_multi_modal_data
|
|
865
|
+
if process_multimodal and previous_multi_modal_data
|
|
847
866
|
else {}
|
|
848
867
|
)
|
|
849
868
|
for modality, vals in new_hashes.items():
|
|
@@ -93,6 +93,9 @@ class _FakeClient:
|
|
|
93
93
|
"request_id": "gen-test",
|
|
94
94
|
"choices": [self.choice],
|
|
95
95
|
}
|
|
96
|
+
if body and body.get("content_parts"):
|
|
97
|
+
payload["prompt_token_ids"] = [1, 2, 2, 3]
|
|
98
|
+
payload["mm_placeholders"] = {"image": [{"offset": 1, "length": 2}]}
|
|
96
99
|
return httpx.Response(
|
|
97
100
|
200,
|
|
98
101
|
content=json.dumps(payload, separators=(",", ":")).encode("utf-8"),
|
|
@@ -182,6 +185,49 @@ def test_generate_builds_request_body_and_parses_response():
|
|
|
182
185
|
assert tc.status == ToolCallParseStatus.OK
|
|
183
186
|
|
|
184
187
|
|
|
188
|
+
def test_generate_process_multimodal_false_sends_content_parts():
|
|
189
|
+
class DeferredMultimodalRenderer(_FakeRenderer):
|
|
190
|
+
supports_process_multimodal = True
|
|
191
|
+
|
|
192
|
+
def render(
|
|
193
|
+
self,
|
|
194
|
+
messages,
|
|
195
|
+
*,
|
|
196
|
+
tools=None,
|
|
197
|
+
add_generation_prompt=False,
|
|
198
|
+
process_multimodal=True,
|
|
199
|
+
):
|
|
200
|
+
assert process_multimodal is False
|
|
201
|
+
return RenderedTokens(token_ids=[1, 2, 3])
|
|
202
|
+
|
|
203
|
+
client = _FakeClient()
|
|
204
|
+
image_url = "data:image/png;base64,aW1hZ2U="
|
|
205
|
+
result = asyncio.run(
|
|
206
|
+
generate(
|
|
207
|
+
client=client,
|
|
208
|
+
renderer=DeferredMultimodalRenderer(),
|
|
209
|
+
messages=[
|
|
210
|
+
{
|
|
211
|
+
"role": "user",
|
|
212
|
+
"content": [
|
|
213
|
+
{"type": "text", "text": "look"},
|
|
214
|
+
{"type": "image_url", "image_url": {"url": image_url}},
|
|
215
|
+
],
|
|
216
|
+
}
|
|
217
|
+
],
|
|
218
|
+
model="test-model",
|
|
219
|
+
process_multimodal=False,
|
|
220
|
+
)
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
body = client.calls[0]["body"]
|
|
224
|
+
assert body["content_parts"] == [{"type": "image_url", "url": image_url}]
|
|
225
|
+
assert "features" not in body
|
|
226
|
+
assert result["renderer_prompt_ids"] == [1, 2, 3]
|
|
227
|
+
assert result["prompt_ids"] == [1, 2, 2, 3]
|
|
228
|
+
assert result["mm_placeholders"] == {"image": [{"offset": 1, "length": 2}]}
|
|
229
|
+
|
|
230
|
+
|
|
185
231
|
def test_generate_rejects_missing_completion_logprobs_before_parsing():
|
|
186
232
|
client = _FakeClient()
|
|
187
233
|
client.choice.pop("logprobs")
|
|
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.11.dev5 → renderers-0.1.12.dev2}/examples/sglang/multiturn_generate_sglang.py
RENAMED
|
File without changes
|
|
File without changes
|
{renderers-0.1.11.dev5 → renderers-0.1.12.dev2}/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
|