mirror of
https://github.com/tiennm99/litellm.git
synced 2026-07-12 15:05:01 +00:00
[Feat] Add Recraft API - Image Edits Support (#12874)
* test_recraft_image_edit_api * add RecraftImageEditConfig * complete RecraftImageEditConfig * add RecraftImageEditRequestParams in types * update RecraftImageEditRequestParams * working * transform_image_edit_request * Image Edit docs recraft * working transform_image_edit_request * TestRecraftImageEditTransformation
This commit is contained in:
@@ -8,9 +8,9 @@ https://www.recraft.ai/
|
||||
| Description | Recraft is an AI-powered design tool that generates high-quality images with precise control over style and content. |
|
||||
| Provider Route on LiteLLM | `recraft/` |
|
||||
| Link to Provider Doc | [Recraft ↗](https://www.recraft.ai/docs) |
|
||||
| Supported Operations | [`/images/generations`](#image-generation) |
|
||||
| Supported Operations | [`/images/generations`](#image-generation), [`/images/edits`](#image-edit) |
|
||||
|
||||
LiteLLM supports Recraft Image Generation calls.
|
||||
LiteLLM supports Recraft Image Generation and Image Edit calls.
|
||||
|
||||
## API Base, Key
|
||||
```python
|
||||
@@ -152,6 +152,148 @@ print(response)
|
||||
|
||||
For more details on available models and features, see: https://www.recraft.ai/docs
|
||||
|
||||
## Image Edit
|
||||
|
||||
### Usage - LiteLLM Python SDK
|
||||
|
||||
```python showLineNumbers
|
||||
from litellm import image_edit
|
||||
import os
|
||||
|
||||
os.environ['RECRAFT_API_KEY'] = "your-api-key"
|
||||
|
||||
# Open the image file
|
||||
with open("reference_image.png", "rb") as image_file:
|
||||
# recraft image edit call
|
||||
response = image_edit(
|
||||
model="recraft/recraftv3",
|
||||
prompt="Create a studio ghibli style image that combines all the reference images. Make sure the person looks like a CTO.",
|
||||
image=image_file,
|
||||
)
|
||||
print(response)
|
||||
```
|
||||
|
||||
### Usage - LiteLLM Proxy Server
|
||||
|
||||
#### 1. Setup config.yaml
|
||||
|
||||
```yaml showLineNumbers
|
||||
model_list:
|
||||
- model_name: recraft-v3
|
||||
litellm_params:
|
||||
model: recraft/recraftv3
|
||||
api_key: os.environ/RECRAFT_API_KEY
|
||||
model_info:
|
||||
mode: image_edit
|
||||
|
||||
general_settings:
|
||||
master_key: sk-1234
|
||||
```
|
||||
|
||||
#### 2. Start the proxy
|
||||
|
||||
```bash showLineNumbers
|
||||
litellm --config config.yaml
|
||||
|
||||
# RUNNING on http://0.0.0.0:4000
|
||||
```
|
||||
|
||||
#### 3. Test it
|
||||
|
||||
```bash showLineNumbers
|
||||
curl --location 'http://0.0.0.0:4000/v1/images/edits' \
|
||||
--header 'Authorization: Bearer sk-1234' \
|
||||
--form 'model="recraft-v3"' \
|
||||
--form 'prompt="Create a studio ghibli style image that combines all the reference images. Make sure the person looks like a CTO."' \
|
||||
--form 'image=@"reference_image.png"'
|
||||
```
|
||||
|
||||
### Advanced Usage - With Additional Parameters
|
||||
|
||||
```python showLineNumbers
|
||||
from litellm import image_edit
|
||||
import os
|
||||
|
||||
os.environ['RECRAFT_API_KEY'] = "your-api-key"
|
||||
|
||||
with open("reference_image.png", "rb") as image_file:
|
||||
response = image_edit(
|
||||
model="recraft/recraftv3",
|
||||
prompt="Create a studio ghibli style image",
|
||||
image=image_file,
|
||||
n=2, # Generate 2 variations
|
||||
response_format="url", # Return URLs instead of base64
|
||||
style="realistic_image", # Set artistic style
|
||||
strength=0.5 # Control transformation strength (0-1)
|
||||
)
|
||||
print(response)
|
||||
```
|
||||
|
||||
### Supported Image Edit Parameters
|
||||
|
||||
Recraft supports the following OpenAI-compatible parameters for image editing:
|
||||
|
||||
| Parameter | Type | Description | Default | Example |
|
||||
|-----------|------|-------------|---------|---------|
|
||||
| `n` | integer | Number of images to generate (1-4) | `1` | `2` |
|
||||
| `response_format` | string | Format of response (`url` or `b64_json`) | `"url"` | `"b64_json"` |
|
||||
| `style` | string | Image style/artistic direction | - | `"realistic_image"` |
|
||||
| `strength` | float | Controls how much to transform the image (0.0-1.0) | `0.2` | `0.5` |
|
||||
|
||||
### Using Non-OpenAI Parameters
|
||||
|
||||
You can pass Recraft-specific parameters that are not part of the OpenAI API by including them in your request:
|
||||
|
||||
**Usage with LiteLLM Python SDK**
|
||||
|
||||
```python showLineNumbers
|
||||
from litellm import image_edit
|
||||
import os
|
||||
|
||||
os.environ['RECRAFT_API_KEY'] = "your-api-key"
|
||||
|
||||
with open("reference_image.png", "rb") as image_file:
|
||||
response = image_edit(
|
||||
model="recraft/recraftv3",
|
||||
prompt="Create a studio ghibli style image",
|
||||
image=image_file,
|
||||
style_id="your-style-id", # Recraft-specific parameter
|
||||
strength=0.7
|
||||
)
|
||||
```
|
||||
|
||||
**Usage with LiteLLM Proxy Server + OpenAI Python SDK**
|
||||
|
||||
```python showLineNumbers
|
||||
from openai import OpenAI
|
||||
import os
|
||||
|
||||
client = OpenAI(
|
||||
api_key="sk-1234", # your LiteLLM proxy master key
|
||||
base_url="http://0.0.0.0:4000" # your LiteLLM proxy URL
|
||||
)
|
||||
|
||||
with open("reference_image.png", "rb") as image_file:
|
||||
response = client.images.edit(
|
||||
model="recraft-v3",
|
||||
prompt="Create a studio ghibli style image",
|
||||
image=image_file,
|
||||
extra_body={
|
||||
"style_id": "your-style-id",
|
||||
"strength": 0.7
|
||||
}
|
||||
)
|
||||
print(response)
|
||||
```
|
||||
|
||||
### Supported Image Edit Models
|
||||
|
||||
**Note: All recraft models are supported by LiteLLM** Just pass the model name with `recraft/<model_name>` and litellm will route it to recraft.
|
||||
|
||||
| Model Name | Function Call |
|
||||
|------------|---------------|
|
||||
| recraftv3 | `image_edit(model="recraft/recraftv3", ...)` |
|
||||
|
||||
## API Key Setup
|
||||
|
||||
Get your API key from [Recraft's website](https://www.recraft.ai/) and set it as an environment variable:
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
from io import BufferedReader
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, cast
|
||||
|
||||
import httpx
|
||||
from httpx._types import RequestFiles
|
||||
|
||||
from litellm.images.utils import ImageEditRequestUtils
|
||||
from litellm.llms.base_llm.image_edit.transformation import BaseImageEditConfig
|
||||
from litellm.secret_managers.main import get_secret_str
|
||||
from litellm.types.images.main import ImageEditOptionalRequestParams
|
||||
from litellm.types.llms.recraft import RecraftImageEditRequestParams
|
||||
from litellm.types.responses.main import *
|
||||
from litellm.types.router import GenericLiteLLMParams
|
||||
from litellm.types.utils import FileTypes, ImageObject, ImageResponse
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from litellm.litellm_core_utils.litellm_logging import Logging as _LiteLLMLoggingObj
|
||||
|
||||
LiteLLMLoggingObj = _LiteLLMLoggingObj
|
||||
else:
|
||||
LiteLLMLoggingObj = Any
|
||||
|
||||
|
||||
class RecraftImageEditConfig(BaseImageEditConfig):
|
||||
DEFAULT_BASE_URL: str = "https://external.api.recraft.ai"
|
||||
IMAGE_EDIT_ENDPOINT: str = "v1/images/imageToImage"
|
||||
DEFAULT_STRENGTH: float = 0.2
|
||||
|
||||
def get_supported_openai_params(
|
||||
self, model: str
|
||||
) -> List:
|
||||
"""
|
||||
Supported OpenAI parameters that can be mapped to Recraft image edit API.
|
||||
|
||||
Based on Recraft API docs: https://www.recraft.ai/docs#image-to-image
|
||||
"""
|
||||
return [
|
||||
"n", # Maps to n (number of images)
|
||||
"response_format", # Maps to response_format (url or b64_json)
|
||||
"style" # Maps to style parameter
|
||||
]
|
||||
|
||||
def map_openai_params(
|
||||
self,
|
||||
image_edit_optional_params: ImageEditOptionalRequestParams,
|
||||
model: str,
|
||||
drop_params: bool,
|
||||
) -> Dict:
|
||||
"""
|
||||
Map OpenAI image edit parameters to Recraft parameters.
|
||||
Reuses OpenAI logic but filters to supported params only.
|
||||
"""
|
||||
# Start with all params like OpenAI does
|
||||
all_params = dict(image_edit_optional_params)
|
||||
|
||||
# Filter to only supported Recraft parameters
|
||||
supported_params = self.get_supported_openai_params(model)
|
||||
filtered_params = {k: v for k, v in all_params.items() if k in supported_params}
|
||||
|
||||
return filtered_params
|
||||
|
||||
|
||||
def get_complete_url(
|
||||
self,
|
||||
model: str,
|
||||
api_base: Optional[str],
|
||||
litellm_params: dict,
|
||||
) -> str:
|
||||
"""
|
||||
Get the complete url for the request
|
||||
|
||||
Some providers need `model` in `api_base`
|
||||
"""
|
||||
complete_url: str = (
|
||||
api_base
|
||||
or get_secret_str("RECRAFT_API_BASE")
|
||||
or self.DEFAULT_BASE_URL
|
||||
)
|
||||
|
||||
complete_url = complete_url.rstrip("/")
|
||||
complete_url = f"{complete_url}/{self.IMAGE_EDIT_ENDPOINT}"
|
||||
return complete_url
|
||||
|
||||
def validate_environment(
|
||||
self,
|
||||
headers: dict,
|
||||
model: str,
|
||||
api_key: Optional[str] = None,
|
||||
) -> dict:
|
||||
final_api_key: Optional[str] = (
|
||||
api_key or
|
||||
get_secret_str("RECRAFT_API_KEY")
|
||||
)
|
||||
if not final_api_key:
|
||||
raise ValueError("RECRAFT_API_KEY is not set")
|
||||
|
||||
headers["Authorization"] = f"Bearer {final_api_key}"
|
||||
return headers
|
||||
|
||||
|
||||
def transform_image_edit_request(
|
||||
self,
|
||||
model: str,
|
||||
prompt: str,
|
||||
image: FileTypes,
|
||||
image_edit_optional_request_params: Dict,
|
||||
litellm_params: GenericLiteLLMParams,
|
||||
headers: dict,
|
||||
) -> Tuple[Dict, RequestFiles]:
|
||||
"""
|
||||
Transform the image edit request to Recraft's multipart form format.
|
||||
Reuses OpenAI file handling logic but adapts for Recraft API structure.
|
||||
|
||||
https://www.recraft.ai/docs#image-to-image
|
||||
"""
|
||||
|
||||
request_body: RecraftImageEditRequestParams = RecraftImageEditRequestParams(
|
||||
model=model,
|
||||
prompt=prompt,
|
||||
strength=image_edit_optional_request_params.pop("strength", self.DEFAULT_STRENGTH),
|
||||
**image_edit_optional_request_params,
|
||||
)
|
||||
request_dict = cast(Dict, request_body)
|
||||
#########################################################
|
||||
# Reuse OpenAI logic: Separate images as `files` and send other parameters as `data`
|
||||
#########################################################
|
||||
files_list = self._get_image_files_for_request(image=image)
|
||||
data_without_images = {k: v for k, v in request_dict.items() if k != "image"}
|
||||
|
||||
return data_without_images, files_list
|
||||
|
||||
|
||||
def _get_image_files_for_request(
|
||||
self,
|
||||
image: FileTypes,
|
||||
) -> List[Tuple[str, Any]]:
|
||||
files_list: List[Tuple[str, Any]] = []
|
||||
|
||||
# Handle single image (Recraft expects single image, not array)
|
||||
if image:
|
||||
# OpenAI wraps images in arrays, but for Recraft we need single image
|
||||
if isinstance(image, list):
|
||||
_image = image[0] if image else None # Take first image for Recraft
|
||||
else:
|
||||
_image = image
|
||||
|
||||
if _image is not None:
|
||||
image_content_type: str = ImageEditRequestUtils.get_image_content_type(_image)
|
||||
if isinstance(_image, BufferedReader):
|
||||
files_list.append(
|
||||
("image", (_image.name, _image, image_content_type))
|
||||
)
|
||||
else:
|
||||
files_list.append(
|
||||
("image", ("image.png", _image, image_content_type))
|
||||
)
|
||||
|
||||
return files_list
|
||||
|
||||
def transform_image_edit_response(
|
||||
self,
|
||||
model: str,
|
||||
raw_response: httpx.Response,
|
||||
logging_obj: LiteLLMLoggingObj,
|
||||
) -> ImageResponse:
|
||||
model_response = ImageResponse()
|
||||
try:
|
||||
response_data = raw_response.json()
|
||||
except Exception as e:
|
||||
raise self.get_error_class(
|
||||
error_message=f"Error transforming image edit response: {e}",
|
||||
status_code=raw_response.status_code,
|
||||
headers=raw_response.headers,
|
||||
)
|
||||
if not model_response.data:
|
||||
model_response.data = []
|
||||
|
||||
for image_data in response_data["data"]:
|
||||
model_response.data.append(ImageObject(
|
||||
url=image_data.get("url", None),
|
||||
b64_json=image_data.get("b64_json", None),
|
||||
))
|
||||
|
||||
return model_response
|
||||
@@ -14,4 +14,22 @@ class RecraftImageGenerationRequestParams(TypedDict, total=False):
|
||||
response_format: Optional[str]
|
||||
size: Optional[str]
|
||||
negative_prompt: Optional[str]
|
||||
controls: Optional[Dict]
|
||||
controls: Optional[Dict]
|
||||
|
||||
|
||||
class RecraftImageEditRequestParams(TypedDict, total=False):
|
||||
"""
|
||||
TypedDict for Recraft image edit request parameters.
|
||||
|
||||
Based on Recraft API docs: https://www.recraft.ai/docs#image-to-image
|
||||
"""
|
||||
prompt: str # required - A text description of areas to change. Max 1000 bytes
|
||||
strength: float # required - Defines difference with original image, [0, 1]
|
||||
model: Optional[str] # The model to use, default is recraftv3
|
||||
n: Optional[int] # The number of images to generate, must be between 1 and 6
|
||||
style_id: Optional[str] # Use a previously uploaded style as reference
|
||||
style: Optional[str] # The style of generated images, default is realistic_image
|
||||
substyle: Optional[str] # Additional style specification
|
||||
response_format: Optional[str] # Format of returned images: url or b64_json
|
||||
negative_prompt: Optional[str] # Description of undesired elements
|
||||
controls: Optional[Dict] # Custom parameters to tweak generation process
|
||||
+7
-1
@@ -7177,12 +7177,18 @@ class ProviderConfigManager:
|
||||
)
|
||||
|
||||
return OpenAIImageEditConfig()
|
||||
if LlmProviders.AZURE == provider:
|
||||
elif LlmProviders.AZURE == provider:
|
||||
from litellm.llms.azure.image_edit.transformation import (
|
||||
AzureImageEditConfig,
|
||||
)
|
||||
|
||||
return AzureImageEditConfig()
|
||||
elif LlmProviders.RECRAFT == provider:
|
||||
from litellm.llms.recraft.image_edit.transformation import (
|
||||
RecraftImageEditConfig,
|
||||
)
|
||||
|
||||
return RecraftImageEditConfig()
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 70 B After Width: | Height: | Size: 2.1 MiB |
@@ -411,3 +411,93 @@ async def test_azure_image_edit_cost_tracking():
|
||||
# check response_cost
|
||||
assert test_custom_logger.standard_logging_payload["response_cost"] is not None
|
||||
assert test_custom_logger.standard_logging_payload["response_cost"] > 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_recraft_image_edit_api():
|
||||
from litellm import aimage_edit
|
||||
import requests
|
||||
litellm._turn_on_debug()
|
||||
global TEST_IMAGES
|
||||
try:
|
||||
prompt = """
|
||||
Create a studio ghibli style image that combines all the reference images. Make sure the person looks like a CTO.
|
||||
"""
|
||||
result = await aimage_edit(
|
||||
prompt=prompt,
|
||||
model="recraft/recraftv3",
|
||||
image=TEST_IMAGES,
|
||||
)
|
||||
print("result from image edit", result)
|
||||
|
||||
# Validate the response meets expected schema
|
||||
ImageResponse.model_validate(result)
|
||||
|
||||
if isinstance(result, ImageResponse) and result.data:
|
||||
image_url = result.data[0].url
|
||||
|
||||
# download the image
|
||||
image_bytes = requests.get(image_url).content
|
||||
with open("test_image_edit.png", "wb") as f:
|
||||
f.write(image_bytes)
|
||||
except litellm.ContentPolicyViolationError as e:
|
||||
pass
|
||||
|
||||
|
||||
def test_recraft_image_edit_config():
|
||||
"""
|
||||
Test Recraft image edit configuration parameter mapping and request transformation.
|
||||
"""
|
||||
from litellm.llms.recraft.image_edit.transformation import RecraftImageEditConfig
|
||||
from litellm.types.images.main import ImageEditOptionalRequestParams
|
||||
from litellm.types.router import GenericLiteLLMParams
|
||||
|
||||
config = RecraftImageEditConfig()
|
||||
|
||||
# Test supported OpenAI params
|
||||
supported_params = config.get_supported_openai_params("recraftv3")
|
||||
expected_params = ["n", "response_format", "style"]
|
||||
assert supported_params == expected_params
|
||||
|
||||
# Test parameter mapping (reuses OpenAI logic with filtering)
|
||||
image_edit_params = ImageEditOptionalRequestParams({
|
||||
"n": 2,
|
||||
"response_format": "b64_json",
|
||||
"style": "realistic_image",
|
||||
"size": "1024x1024", # Should be dropped
|
||||
"quality": "high" # Should be dropped
|
||||
})
|
||||
|
||||
mapped_params = config.map_openai_params(image_edit_params, "recraftv3", drop_params=True)
|
||||
|
||||
# Should only contain supported params
|
||||
assert mapped_params["n"] == 2
|
||||
assert mapped_params["response_format"] == "b64_json"
|
||||
assert mapped_params["style"] == "realistic_image"
|
||||
assert "size" not in mapped_params # Should be dropped
|
||||
assert "quality" not in mapped_params # Should be dropped
|
||||
|
||||
# Test request transformation (reuses OpenAI file handling)
|
||||
mock_image = b"fake_image_data"
|
||||
prompt = "winter landscape"
|
||||
litellm_params = GenericLiteLLMParams(api_key="test_key")
|
||||
|
||||
data, files = config.transform_image_edit_request(
|
||||
model="recraftv3",
|
||||
prompt=prompt,
|
||||
image=mock_image,
|
||||
image_edit_optional_request_params={"strength": 0.7, "n": 1},
|
||||
litellm_params=litellm_params,
|
||||
headers={}
|
||||
)
|
||||
|
||||
# Check data structure (like OpenAI but with Recraft additions)
|
||||
assert data["prompt"] == prompt
|
||||
assert data["strength"] == 0.7 # Recraft-specific parameter
|
||||
assert data["model"] == "recraftv3"
|
||||
|
||||
# Check file structure (reuses OpenAI logic)
|
||||
assert len(files) == 1
|
||||
assert files[0][0] == "image" # Field name (not image[] like OpenAI)
|
||||
assert files[0][1][1] == mock_image # Image data
|
||||
assert files[0][1][2] == "image/png" # Content type
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from io import BufferedReader, BytesIO
|
||||
from typing import Dict, List
|
||||
from unittest.mock import MagicMock, mock_open, patch
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
sys.path.insert(
|
||||
0, os.path.abspath("../../../../..")
|
||||
) # Adds the parent directory to the system path
|
||||
|
||||
from litellm.llms.recraft.image_edit.transformation import RecraftImageEditConfig
|
||||
from litellm.types.images.main import ImageEditOptionalRequestParams
|
||||
from litellm.types.router import GenericLiteLLMParams
|
||||
from litellm.types.utils import ImageObject, ImageResponse
|
||||
|
||||
|
||||
class TestRecraftImageEditTransformation:
|
||||
"""
|
||||
Unit tests for Recraft image edit transformation functionality.
|
||||
"""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test fixtures before each test method."""
|
||||
self.config = RecraftImageEditConfig()
|
||||
self.model = "recraft-v3"
|
||||
self.logging_obj = MagicMock()
|
||||
self.prompt = "Add more trees to this landscape"
|
||||
|
||||
def test_transform_image_edit_request(self):
|
||||
"""
|
||||
Test that transform_image_edit_request correctly transforms request parameters
|
||||
and separates files from data.
|
||||
"""
|
||||
# Mock image data
|
||||
image_data = b"fake_image_data"
|
||||
image = BytesIO(image_data)
|
||||
|
||||
image_edit_optional_params = {
|
||||
"n": 2,
|
||||
"response_format": "url",
|
||||
"strength": 0.5,
|
||||
"style": "photographic"
|
||||
}
|
||||
|
||||
litellm_params = GenericLiteLLMParams()
|
||||
headers = {}
|
||||
|
||||
data, files = self.config.transform_image_edit_request(
|
||||
model=self.model,
|
||||
prompt=self.prompt,
|
||||
image=image,
|
||||
image_edit_optional_request_params=image_edit_optional_params,
|
||||
litellm_params=litellm_params,
|
||||
headers=headers
|
||||
)
|
||||
|
||||
# Check that data contains the expected parameters
|
||||
assert data["model"] == self.model
|
||||
assert data["prompt"] == self.prompt
|
||||
assert data["strength"] == 0.5
|
||||
assert data["n"] == 2
|
||||
assert data["response_format"] == "url"
|
||||
assert data["style"] == "photographic"
|
||||
|
||||
# Check that image is not in data (should be in files)
|
||||
assert "image" not in data
|
||||
|
||||
# Check that files contains the image
|
||||
assert len(files) == 1
|
||||
assert files[0][0] == "image" # field name
|
||||
assert files[0][1][0] == "image.png" # filename (default for non-BufferedReader)
|
||||
assert files[0][1][1] == image # file object
|
||||
|
||||
def test_get_image_files_for_request_single_image(self):
|
||||
"""
|
||||
Test that _get_image_files_for_request correctly handles a single image.
|
||||
"""
|
||||
image_data = b"fake_image_data"
|
||||
image = BytesIO(image_data)
|
||||
|
||||
files = self.config._get_image_files_for_request(image=image)
|
||||
|
||||
assert len(files) == 1
|
||||
assert files[0][0] == "image"
|
||||
assert files[0][1][0] == "image.png" # Default filename for non-BufferedReader
|
||||
assert files[0][1][1] == image
|
||||
assert "image/png" in files[0][1][2]
|
||||
|
||||
def test_get_image_files_for_request_list_with_single_image(self):
|
||||
"""
|
||||
Test that _get_image_files_for_request correctly handles a list containing a single image
|
||||
(takes the first image for Recraft API).
|
||||
"""
|
||||
image_data = b"fake_image_data"
|
||||
image = BytesIO(image_data)
|
||||
|
||||
# Pass as list (OpenAI format)
|
||||
files = self.config._get_image_files_for_request(image=[image])
|
||||
|
||||
assert len(files) == 1
|
||||
assert files[0][0] == "image"
|
||||
assert files[0][1][0] == "image.png" # Default filename for non-BufferedReader
|
||||
assert files[0][1][1] == image
|
||||
|
||||
def test_get_image_files_for_request_buffered_reader(self):
|
||||
"""
|
||||
Test that _get_image_files_for_request correctly handles BufferedReader objects.
|
||||
"""
|
||||
# Create a mock BufferedReader
|
||||
mock_file = MagicMock(spec=BufferedReader)
|
||||
mock_file.name = "buffered_image.jpg"
|
||||
|
||||
files = self.config._get_image_files_for_request(image=mock_file)
|
||||
|
||||
assert len(files) == 1
|
||||
assert files[0][0] == "image"
|
||||
assert files[0][1][0] == "buffered_image.jpg"
|
||||
assert files[0][1][1] == mock_file
|
||||
|
||||
def test_get_image_files_for_request_no_image(self):
|
||||
"""
|
||||
Test that _get_image_files_for_request returns empty list when no image is provided.
|
||||
"""
|
||||
files = self.config._get_image_files_for_request(image=None)
|
||||
assert files == []
|
||||
|
||||
def test_transform_image_edit_response_success(self):
|
||||
"""
|
||||
Test that transform_image_edit_response correctly transforms a successful response.
|
||||
"""
|
||||
# Mock response data
|
||||
response_data = {
|
||||
"data": [
|
||||
{"url": "https://example.com/edited_image1.jpg", "b64_json": None},
|
||||
{"url": None, "b64_json": "base64encodeddata"}
|
||||
]
|
||||
}
|
||||
|
||||
# Create mock response
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = response_data
|
||||
|
||||
result = self.config.transform_image_edit_response(
|
||||
model=self.model,
|
||||
raw_response=mock_response,
|
||||
logging_obj=self.logging_obj
|
||||
)
|
||||
|
||||
assert isinstance(result, ImageResponse)
|
||||
assert len(result.data) == 2
|
||||
assert result.data[0].url == "https://example.com/edited_image1.jpg"
|
||||
assert result.data[0].b64_json is None
|
||||
assert result.data[1].url is None
|
||||
assert result.data[1].b64_json == "base64encodeddata"
|
||||
|
||||
def test_transform_image_edit_response_json_error(self):
|
||||
"""
|
||||
Test that transform_image_edit_response raises appropriate error when response JSON is invalid.
|
||||
"""
|
||||
# Create mock response that raises JSON decode error
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.side_effect = json.JSONDecodeError("Invalid JSON", "", 0)
|
||||
mock_response.status_code = 500
|
||||
mock_response.headers = {}
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
self.config.transform_image_edit_response(
|
||||
model=self.model,
|
||||
raw_response=mock_response,
|
||||
logging_obj=self.logging_obj
|
||||
)
|
||||
|
||||
assert "Error transforming image edit response" in str(exc_info.value)
|
||||
Reference in New Issue
Block a user