"""Verify dpo_trainer_vision_process_row forwards prompt and images verbatim.""" import ast import os import numpy as np REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) RL_PATH = os.path.join(REPO_ROOT, "unsloth", "models", "rl_replacements.py") def _load_helpers(): src = open(RL_PATH).read() tree = ast.parse(src) import torch as _torch ns = {"torch": _torch} for node in tree.body: if isinstance(node, ast.Assign) and any( isinstance(t, ast.Name) and t.id == "_DPO_VISION_KEYS" for t in node.targets ): exec(ast.get_source_segment(src, node), ns) for node in tree.body: if isinstance(node, ast.FunctionDef) and node.name.startswith( ("dpo_trainer_", "_dpo_trainer_") ): exec(ast.get_source_segment(src, node), ns) return ns class _Tok: eos_token_id = 99 bos_token_id = None def __call__( self, t, add_special_tokens = False, ): return {"input_ids": [10]} class _Capture: image_token = "" boi_token = "" def __init__(self): self.tokenizer = _Tok() self.last_text = None self.last_images = "__sentinel__" def __call__( self, images = None, text = None, add_special_tokens = False, ): self.last_text = text self.last_images = images out = {"input_ids": [[1, 2]]} if images is not None: out["pixel_values"] = [object()] return out def test_prompt_passes_through_without_image_token_synthesis(): ns = _load_helpers() proc = _Capture() ns["dpo_trainer_vision_process_row"]( {"prompt": "describe", "chosen": "c", "rejected": "r", "images": ["i"]}, proc, ) assert proc.last_text == "describe" def test_prompt_with_existing_image_token_unchanged(): ns = _load_helpers() proc = _Capture() ns["dpo_trainer_vision_process_row"]( {"prompt": " describe", "chosen": "c", "rejected": "r", "images": ["i"]}, proc, ) assert proc.last_text == " describe" def test_gemma3_style_boi_token_prompt_not_corrupted(): ns = _load_helpers() proc = _Capture() ns["dpo_trainer_vision_process_row"]( {"prompt": " describe", "chosen": "c", "rejected": "r", "images": ["i"]}, proc, ) assert proc.last_text == " describe" assert "" not in proc.last_text def test_multi_image_prompt_unchanged_no_extra_placeholders(): ns = _load_helpers() proc = _Capture() ns["dpo_trainer_vision_process_row"]( { "prompt": "compare", "chosen": "c", "rejected": "r", "images": ["a", "b", "c"], }, proc, ) assert proc.last_text == "compare" def test_list_images_forwarded_verbatim(): ns = _load_helpers() proc = _Capture() payload = ["a", "b"] ns["dpo_trainer_vision_process_row"]( {"prompt": "p", "chosen": "c", "rejected": "r", "images": payload}, proc, ) assert proc.last_images is payload def test_single_pil_like_image_forwarded_verbatim(): ns = _load_helpers() class PIL: def __bool__(self): return True proc = _Capture() pil = PIL() ns["dpo_trainer_vision_process_row"]( {"prompt": "p", "chosen": "c", "rejected": "r", "images": pil}, proc, ) assert proc.last_images is pil def test_numpy_ndarray_image_forwarded_verbatim(): ns = _load_helpers() proc = _Capture() arr = np.zeros((2, 3, 3), dtype = np.uint8) ns["dpo_trainer_vision_process_row"]( {"prompt": "p", "chosen": "c", "rejected": "r", "images": arr}, proc, ) assert proc.last_images is arr def test_missing_images_key_passes_none_to_processor(): ns = _load_helpers() proc = _Capture() ns["dpo_trainer_vision_process_row"]( {"prompt": "p", "chosen": "c", "rejected": "r"}, proc, ) assert proc.last_images is None