|
| 1 | +from io import BufferedReader, BytesIO |
| 2 | +from typing import Dict |
| 3 | +import pytest |
| 4 | +from litellm import image_edit |
| 5 | +from litellm.llms.openai.image_edit.transformation import OpenAIImageEditConfig |
| 6 | +from litellm.types.router import GenericLiteLLMParams |
| 7 | + |
| 8 | + |
| 9 | +@pytest.fixture |
| 10 | +def image_edit_config() -> OpenAIImageEditConfig: |
| 11 | + return OpenAIImageEditConfig() |
| 12 | + |
| 13 | + |
| 14 | +def test_transform_image_edit_request_basic(image_edit_config: OpenAIImageEditConfig): |
| 15 | + """Test basic image edit request transformation with image and prompt""" |
| 16 | + model = "dall-e-2" |
| 17 | + prompt = "Make the background blue" |
| 18 | + image = b"fake_image_data" |
| 19 | + image_edit_optional_request_params = {} |
| 20 | + litellm_params = GenericLiteLLMParams() |
| 21 | + headers = {} |
| 22 | + |
| 23 | + data, files = image_edit_config.transform_image_edit_request( |
| 24 | + model=model, |
| 25 | + prompt=prompt, |
| 26 | + image=image, |
| 27 | + image_edit_optional_request_params=image_edit_optional_request_params, |
| 28 | + litellm_params=litellm_params, |
| 29 | + headers=headers, |
| 30 | + ) |
| 31 | + |
| 32 | + # Check that data contains model and prompt but not image |
| 33 | + assert data["model"] == model |
| 34 | + assert data["prompt"] == prompt |
| 35 | + assert "image" not in data |
| 36 | + assert "mask" not in data |
| 37 | + |
| 38 | + # Check that files contains the image |
| 39 | + assert len(files) == 1 |
| 40 | + assert files[0][0] == "image" # field name |
| 41 | + assert files[0][1][0] == "image.png" # filename |
| 42 | + assert files[0][1][1] == image # image data |
| 43 | + assert "image/png" in files[0][1][2] # content type |
| 44 | + |
| 45 | + |
| 46 | +def test_transform_image_edit_request_with_mask(image_edit_config: OpenAIImageEditConfig): |
| 47 | + """Test transformation with mask parameter""" |
| 48 | + model = "dall-e-2" |
| 49 | + prompt = "Make the background blue" |
| 50 | + image = b"fake_image_data" |
| 51 | + mask = b"fake_mask_data" |
| 52 | + image_edit_optional_request_params = {"mask": mask, "size": "1024x1024"} |
| 53 | + litellm_params = GenericLiteLLMParams() |
| 54 | + headers = {} |
| 55 | + |
| 56 | + data, files = image_edit_config.transform_image_edit_request( |
| 57 | + model=model, |
| 58 | + prompt=prompt, |
| 59 | + image=image, |
| 60 | + image_edit_optional_request_params=image_edit_optional_request_params, |
| 61 | + litellm_params=litellm_params, |
| 62 | + headers=headers, |
| 63 | + ) |
| 64 | + |
| 65 | + # Check that data contains model, prompt, and size but not image or mask |
| 66 | + assert data["model"] == model |
| 67 | + assert data["prompt"] == prompt |
| 68 | + assert data["size"] == "1024x1024" |
| 69 | + assert "image" not in data |
| 70 | + assert "mask" not in data |
| 71 | + |
| 72 | + # Check that files contains both image and mask |
| 73 | + assert len(files) == 2 |
| 74 | + |
| 75 | + # Find image and mask in files |
| 76 | + image_file = next(f for f in files if f[0] == "image") |
| 77 | + mask_file = next(f for f in files if f[0] == "mask") |
| 78 | + |
| 79 | + assert image_file[1][0] == "image.png" |
| 80 | + assert image_file[1][1] == image |
| 81 | + assert "image/png" in image_file[1][2] |
| 82 | + |
| 83 | + assert mask_file[1][0] == "mask.png" |
| 84 | + assert mask_file[1][1] == mask |
| 85 | + assert "image/png" in mask_file[1][2] |
| 86 | + |
| 87 | + |
| 88 | +def test_transform_image_edit_request_with_buffered_reader(image_edit_config: OpenAIImageEditConfig): |
| 89 | + """Test transformation with BufferedReader as image input""" |
| 90 | + import tempfile |
| 91 | + import os |
| 92 | + |
| 93 | + model = "dall-e-2" |
| 94 | + prompt = "Make the background blue" |
| 95 | + |
| 96 | + # Create a real file to get a proper BufferedReader |
| 97 | + image_data = b"fake_image_data" |
| 98 | + with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as temp_file: |
| 99 | + temp_file.write(image_data) |
| 100 | + temp_file_path = temp_file.name |
| 101 | + |
| 102 | + try: |
| 103 | + # Open the file as BufferedReader |
| 104 | + with open(temp_file_path, 'rb') as image_buffer: |
| 105 | + image_edit_optional_request_params = {} |
| 106 | + litellm_params = GenericLiteLLMParams() |
| 107 | + headers = {} |
| 108 | + |
| 109 | + data, files = image_edit_config.transform_image_edit_request( |
| 110 | + model=model, |
| 111 | + prompt=prompt, |
| 112 | + image=image_buffer, |
| 113 | + image_edit_optional_request_params=image_edit_optional_request_params, |
| 114 | + litellm_params=litellm_params, |
| 115 | + headers=headers, |
| 116 | + ) |
| 117 | + |
| 118 | + # Check that data contains model and prompt but not image |
| 119 | + assert data["model"] == model |
| 120 | + assert data["prompt"] == prompt |
| 121 | + assert "image" not in data |
| 122 | + |
| 123 | + # Check that files contains the image with the original filename |
| 124 | + assert len(files) == 1 |
| 125 | + assert files[0][0] == "image" |
| 126 | + # Should use the buffer's name (full path from the BufferedReader.name) |
| 127 | + assert files[0][1][0] == temp_file_path # Uses full path from buffer.name |
| 128 | + assert files[0][1][1] == image_buffer # Should be the buffer object |
| 129 | + # Content type detection defaults to PNG for fake data without image headers |
| 130 | + assert files[0][1][2].startswith("image/") # Should detect some image type |
| 131 | + finally: |
| 132 | + # Clean up the temp file |
| 133 | + os.unlink(temp_file_path) |
| 134 | + |
| 135 | + |
| 136 | +def test_transform_image_edit_request_with_optional_params(image_edit_config: OpenAIImageEditConfig): |
| 137 | + """Test transformation with optional parameters like size, quality, etc.""" |
| 138 | + model = "dall-e-2" |
| 139 | + prompt = "Make the background blue" |
| 140 | + image = b"fake_image_data" |
| 141 | + image_edit_optional_request_params = { |
| 142 | + "size": "512x512", |
| 143 | + "response_format": "b64_json", |
| 144 | + "n": 2, |
| 145 | + "user": "test_user" |
| 146 | + } |
| 147 | + litellm_params = GenericLiteLLMParams() |
| 148 | + headers = {} |
| 149 | + |
| 150 | + data, files = image_edit_config.transform_image_edit_request( |
| 151 | + model=model, |
| 152 | + prompt=prompt, |
| 153 | + image=image, |
| 154 | + image_edit_optional_request_params=image_edit_optional_request_params, |
| 155 | + litellm_params=litellm_params, |
| 156 | + headers=headers, |
| 157 | + ) |
| 158 | + |
| 159 | + # Check that data contains all the optional parameters |
| 160 | + assert data["model"] == model |
| 161 | + assert data["prompt"] == prompt |
| 162 | + assert data["size"] == "512x512" |
| 163 | + assert data["response_format"] == "b64_json" |
| 164 | + assert data["n"] == 2 |
| 165 | + assert data["user"] == "test_user" |
| 166 | + assert "image" not in data |
| 167 | + assert "mask" not in data |
| 168 | + |
| 169 | + # Check that files contains only the image |
| 170 | + assert len(files) == 1 |
| 171 | + assert files[0][0] == "image" |
| 172 | + assert files[0][1][1] == image |
| 173 | + |
| 174 | + |
| 175 | +def test_transform_image_edit_request_with_multiple_images(image_edit_config: OpenAIImageEditConfig): |
| 176 | + """Test transformation with multiple images and no mask""" |
| 177 | + model = "dall-e-2" |
| 178 | + prompt = "Make the background blue" |
| 179 | + image1 = b"fake_image_data_1" |
| 180 | + image2 = b"fake_image_data_2" |
| 181 | + image3 = b"fake_image_data_3" |
| 182 | + images = [image1, image2, image3] |
| 183 | + image_edit_optional_request_params = {"size": "1024x1024", "n": 1} |
| 184 | + litellm_params = GenericLiteLLMParams() |
| 185 | + headers = {} |
| 186 | + |
| 187 | + data, files = image_edit_config.transform_image_edit_request( |
| 188 | + model=model, |
| 189 | + prompt=prompt, |
| 190 | + image=images, |
| 191 | + image_edit_optional_request_params=image_edit_optional_request_params, |
| 192 | + litellm_params=litellm_params, |
| 193 | + headers=headers, |
| 194 | + ) |
| 195 | + |
| 196 | + # Check that data contains model, prompt, and optional params but not image or mask |
| 197 | + assert data["model"] == model |
| 198 | + assert data["prompt"] == prompt |
| 199 | + assert data["size"] == "1024x1024" |
| 200 | + assert data["n"] == 1 |
| 201 | + assert "image" not in data |
| 202 | + assert "mask" not in data |
| 203 | + |
| 204 | + # Check that files contains all three images and no mask |
| 205 | + assert len(files) == 3 |
| 206 | + |
| 207 | + # All files should be image entries |
| 208 | + image_files = [f for f in files if f[0] == "image"] |
| 209 | + assert len(image_files) == 3 |
| 210 | + |
| 211 | + # Check that all image data is present |
| 212 | + image_data_in_files = [f[1][1] for f in image_files] |
| 213 | + assert image1 in image_data_in_files |
| 214 | + assert image2 in image_data_in_files |
| 215 | + assert image3 in image_data_in_files |
| 216 | + |
| 217 | + # Check that all files have proper content type |
| 218 | + for file_entry in image_files: |
| 219 | + assert file_entry[1][0] == "image.png" # filename |
| 220 | + assert file_entry[1][2].startswith("image/") # content type |
| 221 | + |
| 222 | + |
| 223 | +def test_transform_image_edit_request_with_mask_list(image_edit_config: OpenAIImageEditConfig): |
| 224 | + """Test transformation with mask as list (should take first element)""" |
| 225 | + model = "dall-e-2" |
| 226 | + prompt = "Make the background blue" |
| 227 | + image = b"fake_image_data" |
| 228 | + mask1 = b"fake_mask_data_1" |
| 229 | + mask2 = b"fake_mask_data_2" |
| 230 | + image_edit_optional_request_params = {"mask": [mask1, mask2]} |
| 231 | + litellm_params = GenericLiteLLMParams() |
| 232 | + headers = {} |
| 233 | + |
| 234 | + data, files = image_edit_config.transform_image_edit_request( |
| 235 | + model=model, |
| 236 | + prompt=prompt, |
| 237 | + image=image, |
| 238 | + image_edit_optional_request_params=image_edit_optional_request_params, |
| 239 | + litellm_params=litellm_params, |
| 240 | + headers=headers, |
| 241 | + ) |
| 242 | + |
| 243 | + # Check that data contains model and prompt but not image or mask |
| 244 | + assert data["model"] == model |
| 245 | + assert data["prompt"] == prompt |
| 246 | + assert "image" not in data |
| 247 | + assert "mask" not in data |
| 248 | + |
| 249 | + # Check that files contains image and only the first mask |
| 250 | + assert len(files) == 2 |
| 251 | + |
| 252 | + mask_file = next(f for f in files if f[0] == "mask") |
| 253 | + assert mask_file[1][1] == mask1 # Should be the first mask, not the second |
| 254 | + |
0 commit comments