diff --git a/renderers/kimi_k25.py b/renderers/kimi_k25.py index 8235933..48ea426 100644 --- a/renderers/kimi_k25.py +++ b/renderers/kimi_k25.py @@ -1183,18 +1183,20 @@ def emit_image( emit_text("", -1) # Merge prev mm_data (earlier-turn images) with the new turn's items. + # Copy the per-modality lists (not just the outer dict) so appending + # below never mutates the caller's previous_multi_modal_data. merged_hashes: dict[str, list[str]] = ( - dict(previous_multi_modal_data.mm_hashes) + {k: list(v) for k, v in previous_multi_modal_data.mm_hashes.items()} if previous_multi_modal_data else {} ) merged_placeholders: dict[str, list[PlaceholderRange]] = ( - dict(previous_multi_modal_data.mm_placeholders) + {k: list(v) for k, v in previous_multi_modal_data.mm_placeholders.items()} if previous_multi_modal_data else {} ) merged_items: dict[str, list[dict[str, Any]]] = ( - dict(previous_multi_modal_data.mm_items) + {k: list(v) for k, v in previous_multi_modal_data.mm_items.items()} if previous_multi_modal_data else {} ) diff --git a/renderers/qwen35.py b/renderers/qwen35.py index 3ebbbc6..61b342a 100644 --- a/renderers/qwen35.py +++ b/renderers/qwen35.py @@ -834,18 +834,20 @@ def flush_buf() -> None: emit_text("\n\n", -1) # Merge prev mm_data (images from earlier turns) with the new turn's. + # Copy the per-modality lists (not just the outer dict) so appending + # below never mutates the caller's previous_multi_modal_data. merged_hashes: dict[str, list[str]] = ( - dict(previous_multi_modal_data.mm_hashes) + {k: list(v) for k, v in previous_multi_modal_data.mm_hashes.items()} if previous_multi_modal_data else {} ) merged_placeholders: dict[str, list[PlaceholderRange]] = ( - dict(previous_multi_modal_data.mm_placeholders) + {k: list(v) for k, v in previous_multi_modal_data.mm_placeholders.items()} if previous_multi_modal_data else {} ) merged_items: dict[str, list[dict[str, Any]]] = ( - dict(previous_multi_modal_data.mm_items) + {k: list(v) for k, v in previous_multi_modal_data.mm_items.items()} if previous_multi_modal_data else {} ) diff --git a/renderers/qwen3_vl.py b/renderers/qwen3_vl.py index 4823e78..97072d2 100644 --- a/renderers/qwen3_vl.py +++ b/renderers/qwen3_vl.py @@ -828,19 +828,21 @@ def render_media_content(content: Any) -> None: em.text("assistant\n", is_sampled=False, is_content=False) em.finalize() - # Merge prev mm_data with the new turn's items. + # Merge prev mm_data with the new turn's items. Copy the per-modality + # lists (not just the outer dict) so appending below never mutates the + # caller's previous_multi_modal_data. merged_hashes = ( - dict(previous_multi_modal_data.mm_hashes) + {k: list(v) for k, v in previous_multi_modal_data.mm_hashes.items()} if previous_multi_modal_data else {} ) merged_placeholders = ( - dict(previous_multi_modal_data.mm_placeholders) + {k: list(v) for k, v in previous_multi_modal_data.mm_placeholders.items()} if previous_multi_modal_data else {} ) merged_items = ( - dict(previous_multi_modal_data.mm_items) + {k: list(v) for k, v in previous_multi_modal_data.mm_items.items()} if previous_multi_modal_data else {} ) diff --git a/tests/test_multimodal.py b/tests/test_multimodal.py index 6b06add..2bba9d2 100644 --- a/tests/test_multimodal.py +++ b/tests/test_multimodal.py @@ -583,6 +583,12 @@ def test_multimodal_bridge_extends_and_carries_mm_data( ] initial_rendered = renderer.render(initial, add_generation_prompt=True) + prior_mm = initial_rendered.multi_modal_data + prior_counts = ( + len(prior_mm.mm_placeholders.get(modality, [])), + len(prior_mm.mm_items.get(modality, [])), + len(prior_mm.mm_hashes.get(modality, [])), + ) # ``previous_completion_ids`` mirrors what a sampler would emit # starting AFTER the prompt's assistant role opener — i.e. the # response text followed by ``<|im_end|>``. @@ -632,6 +638,19 @@ def test_multimodal_bridge_extends_and_carries_mm_data( hashes = bridged_mm.mm_hashes.get(modality, []) assert len(items) == 2 and len(hashes) == 2 + # (2b) The prior turn's sidecar is unchanged — the bridge copies the + # per-modality lists, so the carried-forward item doesn't grow the + # caller's previous_multi_modal_data in place. + assert ( + ( + len(prior_mm.mm_placeholders.get(modality, [])), + len(prior_mm.mm_items.get(modality, [])), + len(prior_mm.mm_hashes.get(modality, [])), + ) + == prior_counts + == (1, 1, 1) + ), f"{mm_model_name} / {modality}: bridge mutated previous_multi_modal_data" + # (3) Extension contains the new turn's pad run, and its # placeholder offset lands inside the extension region. pad_id = tokenizer.convert_tokens_to_ids(kit["placeholder_token"])