Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
32d5a9d
Support raw image refs for multimodal rendering
eligotts Jun 18, 2026
4bc1766
Emit generic raw multimodal refs
eligotts Jun 20, 2026
eaa07bb
Merge remote-tracking branch 'origin/main' into codex/raw-image-asset…
eligotts Jun 21, 2026
a8f4386
Fix raw image renderer style checks
eligotts Jun 25, 2026
b5167c9
Simplify v1 raw multimodal: carry the image pointer at every slot
S1ro1 Jun 27, 2026
b404f80
Renderers cleanup: drop dead render-time processor arg, backcompat, s…
S1ro1 Jun 27, 2026
f8ca354
Remove orphaned image_cache_max config field
S1ro1 Jun 27, 2026
c33805d
Align raw multimodal renderer descriptors
eligotts Jun 27, 2026
8fcd0c7
Merge remote-tracking branch 'origin/main' into codex/raw-image-asset…
eligotts Jun 27, 2026
e97c812
feat: support inline raw image refs
eligotts Jun 28, 2026
673b790
Clean up raw multimodal offload renderers
eligotts Jun 29, 2026
af84c19
Clarify raw image asset contract
eligotts Jun 29, 2026
4e3502f
Apply ruff formatting
eligotts Jun 29, 2026
998e1db
Preserve multimodal sidecar for prebuilt prompts
eligotts Jun 29, 2026
2a19d75
Use URI-based raw image refs
eligotts Jun 29, 2026
aa2d44d
Drop raw multimodal version markers
eligotts Jun 29, 2026
ed5b404
Support processed multimodal renderer output
eligotts Jun 30, 2026
a7953b9
Trim uv lock churn
eligotts Jun 30, 2026
e3c12e9
Harden raw multimodal path
eligotts Jul 4, 2026
a49e0fc
Merge commit '5904fa24aa' into codex/raw-image-assets-renderers
eligotts Jul 5, 2026
e64cc58
Merge remote-tracking branch 'origin/main' into codex/raw-image-asset…
eligotts Jul 23, 2026
7c58fd6
Stop base64-wrapping the raw multimodal ref payload
eligotts Jul 24, 2026
1af6589
Merge remote-tracking branch 'origin/main' into codex/raw-image-asset…
eligotts Jul 31, 2026
66505d2
Accept exactly one raw-mode image source: file:// URLs
eligotts Aug 1, 2026
f0d1dd9
refactor: source raw image layout knobs from checkpoint config
eligotts Aug 3, 2026
29d443c
refactor: drop layout fingerprints; resolve layout knobs once per ren…
eligotts Aug 4, 2026
aa4b0c6
refactor: drop layout knobs and envelope fields nothing reads
eligotts Aug 4, 2026
6b2d208
refactor: move shared VL image helpers into mm_image
eligotts Aug 4, 2026
acd28d2
refactor: drop processed-mode per-renderer image cache
eligotts Aug 4, 2026
2fbf8bd
refactor: tighten processed-mode image sources to local-only
eligotts Aug 4, 2026
3aeb6ee
refactor: trim the vision extra to Pillow; drop mock-based mm tests
eligotts Aug 6, 2026
ab60354
style: format mm helpers for CI ruff; pin local config to match
eligotts Aug 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions docs/renderer-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,15 @@ chat-template kwargs. Those fields are covered by parity tests against
| --- | --- | --- | --- |
| Qwen3 | `Qwen3RendererConfig` | `enable_thinking` | - |
| PrimeIntellect Qwen3 | `PrimeQwen3RendererConfig` | - | - |
| Qwen3.5 | `Qwen35RendererConfig` | `enable_thinking`, `add_vision_id` | `image_cache_max` |
| Qwen3.6 | `Qwen36RendererConfig` | `enable_thinking`, `add_vision_id`, `preserve_thinking` | `image_cache_max` |
| Qwen3-VL | `Qwen3VLRendererConfig` | `add_vision_id` | `image_cache_max` |
| Qwen3.5 | `Qwen35RendererConfig` | `enable_thinking`, `add_vision_id` | - |
| Qwen3.6 | `Qwen36RendererConfig` | `enable_thinking`, `add_vision_id`, `preserve_thinking` | - |
| Qwen3-VL | `Qwen3VLRendererConfig` | `add_vision_id` | - |
| GLM-5 / 5.1 | `GLM5RendererConfig` / `GLM51RendererConfig` | `enable_thinking`, `clear_thinking` | - |
| GLM-4.5 | `GLM45RendererConfig` | `enable_thinking` | - |
| gpt-oss | `GptOssRendererConfig` | `reasoning_effort`, `conversation_start_date` | `use_system_prompt`, `knowledge_cutoff`, `model_identity`, `auto_drop_analysis` |
| Hy3 | `Hy3RendererConfig` | `reasoning_effort`, `preserved_thinking`, `is_training`, `raw_last_assistant`, `fallback_strategy` | - |
| Kimi K2 | `KimiK2RendererConfig` | - | `enable_thinking` |
| Kimi K2.5 / 2.6 | `KimiK25RendererConfig` | `thinking` | `image_cache_max` |
| Kimi K2.5 / 2.6 | `KimiK25RendererConfig` | `thinking` | - |
| Laguna XS.2 | `LagunaXS2RendererConfig` | `enable_thinking`, `render_assistant_messages_raw` | - |
| Laguna XS-2.1 | `LagunaXS21RendererConfig` | `enable_thinking` | - |
| Llama 3 | `Llama3RendererConfig` | `date_string`, `tools_in_user_message` | - |
Expand Down
16 changes: 15 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ dependencies = [
"prime-pydantic-config>=0.3.0.dev83",
]

[project.optional-dependencies]
# Raw multimodal needs Pillow for dimension / PIL decode. Torch stays a
# caller/dev concern (processed mode + processor-backed tests).
vision = [
"pillow>=12.2.0",
]
Comment thread
cursor[bot] marked this conversation as resolved.

[tool.hatch.version]
source = "vcs"
# Tags look like ``renderers-v0.1.8`` (prefix matches the publish.yml
Expand Down Expand Up @@ -76,13 +83,20 @@ version-file = "renderers/_version.py"
[tool.hatch.build.targets.wheel]
packages = ["renderers"]

# Explicit so a checkout under prime-rl does not inherit prime-rl's
# line-length=120. CI runs this repo standalone against ruff defaults (88)
# via .github/workflows/style.yml — keep that pin and this length in sync.
[tool.ruff]
line-length = 88
required-version = "==0.15.12"

[dependency-groups]
dev = [
"pillow>=12.2.0",
"pre-commit",
"pytest>=7.0.0",
"pytest-asyncio>=0.21.0",
"ruff",
"ruff==0.15.12",
"torch>=2.11.0",
"torchvision>=0.26.0",
"ty>=0.0.1a29,<0.0.22",
Expand Down
2 changes: 2 additions & 0 deletions renderers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
LagunaXS21RendererConfig,
Llama3RendererConfig,
MiniMaxM2RendererConfig,
MultimodalOutput,
Nemotron3RendererConfig,
Nemotron3UltraRendererConfig,
PrimeQwen3RendererConfig,
Expand Down Expand Up @@ -156,6 +157,7 @@ def __dir__() -> list[str]:
"Message",
"MiniMaxM2Renderer",
"MiniMaxM2RendererConfig",
"MultimodalOutput",
"MultiModalData",
"MultimodalRenderer",
"Nemotron3Renderer",
Expand Down
67 changes: 51 additions & 16 deletions renderers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,11 @@ class ThinkingPart(TypedDict):
class ImagePart(TypedDict, total=False):
"""An image attached to a message.

Accepts several source shapes so callers can pass whatever they have
on hand — a pre-loaded PIL Image, a filesystem path, a URL, or the
OpenAI ``image_url`` content part verbatim. The renderer resolves
these to a PIL Image at render time.
Part shape may use ``image`` or OpenAI-style ``image_url``. For
``multimodal_output="processed"``, the value must be a local source the
renderer can resolve without network I/O: a PIL Image, filesystem path,
``file://`` URL, or ``data:image/...;base64,...`` URI. Raw mode accepts
only offloaded ``file://`` assets.
"""

type: Literal["image", "image_url"]
Expand Down Expand Up @@ -205,12 +206,10 @@ class PlaceholderRange:
class MultiModalData:
"""Multimodal sidecar produced alongside the token stream.

Renderer output is framework-agnostic: ``mm_items[modality][i]`` is a
plain ``dict`` mirroring the per-item output of a HuggingFace processor
(e.g. ``{"pixel_values": Tensor, "image_grid_thw": Tensor}`` for
Qwen3-VL images). Translation to engine-specific wire formats — vLLM's
``MultiModalKwargsItem``, SGLang's payload, etc. — happens in the
inference glue layer (see ``renderers.client``).
``mm_items[modality][i]`` follows the renderer's configured
``multimodal_output``. The default ``"raw"`` mode emits JSON-safe image
descriptor envelopes for inference paths. ``"processed"`` emits
image-processor payloads such as ``pixel_values`` for SFT/training paths.
"""

mm_hashes: dict[str, list[str]] = field(default_factory=dict)
Expand All @@ -221,6 +220,42 @@ def is_empty(self) -> bool:
return not (self.mm_hashes or self.mm_placeholders or self.mm_items)


def merge_multi_modal_data(
previous: "MultiModalData | None",
new_hashes: dict[str, list[str]],
new_placeholders: dict[str, list[PlaceholderRange]],
new_items: dict[str, list[dict[str, Any]]],
) -> "MultiModalData | None":
"""Concatenate a prior turn's sidecar with a bridge turn's new media.

Bridge callers pass the persisted previous step's sidecar as ``previous``;
inner lists are copied so that object is never mutated. Returns ``None``
when there is no media at all.
"""
merged_hashes = (
{k: list(v) for k, v in previous.mm_hashes.items()} if previous else {}
)
merged_placeholders = (
{k: list(v) for k, v in previous.mm_placeholders.items()} if previous else {}
)
merged_items = (
{k: list(v) for k, v in previous.mm_items.items()} if previous else {}
)
for modality, hashes in new_hashes.items():
merged_hashes.setdefault(modality, []).extend(hashes)
for modality, placeholders in new_placeholders.items():
merged_placeholders.setdefault(modality, []).extend(placeholders)
for modality, items in new_items.items():
merged_items.setdefault(modality, []).extend(items)
if not (merged_hashes or merged_placeholders or merged_items):
return None
return MultiModalData(
mm_hashes=merged_hashes,
mm_placeholders=merged_placeholders,
mm_items=merged_items,
)


@dataclass
class RenderedTokens:
"""Result of rendering messages to tokens.
Expand Down Expand Up @@ -768,8 +803,8 @@ def bridge_to_next_turn(
Text-only renderers return :class:`RenderedTokens` with
``multi_modal_data=None``. Multimodal renderers (see
:class:`MultimodalRenderer`) populate ``multi_modal_data`` so
the caller can recover placeholder offsets + per-item processed
tensors for the new full prompt; they also accept a
the caller can recover placeholder offsets + per-item image
descriptors for the new full prompt; they also accept a
``previous_multi_modal_data`` kwarg via the
:class:`MultimodalRenderer` Protocol override.

Expand Down Expand Up @@ -829,8 +864,8 @@ def bridge_to_next_turn(
the combined token sequence and silently falls back to
hash-cache lookup (or errors)
- returns :class:`RenderedTokens` (not ``list[int]``) so the
caller can recover the placeholder offsets + per-item
processed tensors for the new full prompt
caller can recover the placeholder offsets + per-item image
descriptors for the new full prompt
"""
...

Expand Down Expand Up @@ -1500,7 +1535,7 @@ def _resolve_auto_config(
model_name = getattr(tokenizer, "name_or_path", "")
renderer_name = MODEL_RENDERER_MAP.get(model_name)

preserve_carry = {}
preserve_carry: dict[str, Any] = {"multimodal_output": auto.multimodal_output}
if auto.thinking_retention is not None:
preserve_carry["thinking_retention"] = auto.thinking_retention

Expand Down Expand Up @@ -1551,7 +1586,7 @@ def _resolve_auto_config(
"reasoning_parser=...) to enable structured output parsing.",
model_name or "<unnamed tokenizer>",
)
return DefaultRendererConfig()
return DefaultRendererConfig(multimodal_output=auto.multimodal_output)


# ---------------------------------------------------------------------------
Expand Down
Loading
Loading