Compare commits

..

No commits in common. "f883d94290783380b307ee5423778d07a8d8ea4b" and "770f408dad55008dad47d28de430254bf7be8f6f" have entirely different histories.

6 changed files with 22 additions and 39 deletions

View file

@ -91,9 +91,9 @@ def _collect_extra_params(target_name: str, config: ProjectConfig) -> dict[str,
if target_cfg.height is not None: if target_cfg.height is not None:
params["height"] = target_cfg.height params["height"] = target_cfg.height
if target_cfg.reference_images: if target_cfg.reference_images:
params["reference_images"] = list(target_cfg.reference_images) params["reference_images"] = tuple(target_cfg.reference_images)
if target_cfg.control_images: if target_cfg.control_images:
params["control_images"] = list(target_cfg.control_images) params["control_images"] = tuple(target_cfg.control_images)
return params return params

View file

@ -3,7 +3,6 @@
from __future__ import annotations from __future__ import annotations
import base64 import base64
import io
from pathlib import Path from pathlib import Path
from typing import Literal, override from typing import Literal, override
@ -129,6 +128,7 @@ class OpenAIImageProvider(Provider):
target_config.reference_images, target_config.reference_images,
project_dir, project_dir,
size, size,
output_format,
) )
else: else:
response = await _generate_new( response = await _generate_new(
@ -183,30 +183,17 @@ async def _generate_edit(
reference_images: list[str], reference_images: list[str],
project_dir: Path, project_dir: Path,
size: _SIZE | None, size: _SIZE | None,
output_format: str | None = None,
) -> ImagesResponse: ) -> ImagesResponse:
"""Generate an image using reference images via the edits endpoint. """Generate an image using reference images via the edits endpoint.
gpt-image-* models accept up to 16 images and return b64 by default gpt-image-* models accept up to 16 images and return b64 by default
(they reject ``response_format`` and ``output_format``). (they reject ``response_format``). DALL-E 2 accepts only one image.
DALL-E 2 accepts only one image.
""" """
raw_images = [(project_dir / name).read_bytes() for name in reference_images] images = [(project_dir / name).read_bytes() for name in reference_images]
image: bytes | list[bytes] = images[0] if len(images) == 1 else images
if model.startswith("gpt-image-"): if model.startswith("gpt-image-"):
# gpt-image-* models require file-like objects with a name attribute;
# raw bytes trigger the legacy multipart path that only accepts dall-e-2.
def _to_named_buf(data: bytes, name: str) -> io.BytesIO:
buf = io.BytesIO(data)
buf.name = name
return buf
file_images = [
_to_named_buf(data, name)
for data, name in zip(raw_images, reference_images, strict=True)
]
image: io.BytesIO | list[io.BytesIO] = (
file_images[0] if len(file_images) == 1 else file_images
)
kwargs: dict[str, object] = { kwargs: dict[str, object] = {
"image": image, "image": image,
"prompt": prompt, "prompt": prompt,
@ -215,13 +202,12 @@ async def _generate_edit(
} }
if size is not None: if size is not None:
kwargs["size"] = size kwargs["size"] = size
if output_format is not None:
kwargs["output_format"] = output_format
return await client.images.edit(**kwargs) # pyright: ignore[reportCallIssue,reportArgumentType,reportUnknownVariableType] return await client.images.edit(**kwargs) # pyright: ignore[reportCallIssue,reportArgumentType,reportUnknownVariableType]
dalle_image: bytes | list[bytes] = (
raw_images[0] if len(raw_images) == 1 else raw_images
)
kwargs = { kwargs = {
"image": dalle_image, "image": image,
"prompt": prompt, "prompt": prompt,
"model": model, "model": model,
"n": 1, "n": 1,

View file

@ -9,7 +9,7 @@ dependencies = [
"httpx>=0.27.0", "httpx>=0.27.0",
"mistralai>=1.0.0", "mistralai>=1.0.0",
"networkx>=3.6.1", "networkx>=3.6.1",
"openai>=2.25.0", "openai>=2.21.0",
"pillow>=11.0.0", "pillow>=11.0.0",
"pydantic>=2.12.5", "pydantic>=2.12.5",
"pyyaml>=6.0", "pyyaml>=6.0",

View file

@ -152,7 +152,7 @@ class TestCollectHelpers:
params = _collect_extra_params("out.png", config) params = _collect_extra_params("out.png", config)
assert params["width"] == 512 assert params["width"] == 512
assert params["height"] == 256 assert params["height"] == 256
assert params["reference_images"] == ["ref.png"] assert params["reference_images"] == ("ref.png",)
def test_collect_extra_params_empty(self, write_config: WriteConfig) -> None: def test_collect_extra_params_empty(self, write_config: WriteConfig) -> None:
config = write_config({"targets": {"out.txt": {"prompt": "x"}}}) config = write_config({"targets": {"out.txt": {"prompt": "x"}}})

View file

@ -7,7 +7,6 @@ Mock-heavy tests produce many Any-typed expressions from MagicMock.
from __future__ import annotations from __future__ import annotations
import base64 import base64
import io
from pathlib import Path from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch from unittest.mock import AsyncMock, MagicMock, patch
@ -481,10 +480,8 @@ class TestOpenAIImageProvider:
) )
call_args = mock_client.images.edit.call_args call_args = mock_client.images.edit.call_args
# gpt-image-* models pass a BytesIO with a name attribute # Single reference image should be passed as raw bytes
img_arg = call_args.kwargs["image"] assert call_args.kwargs["image"] == b"reference data"
assert img_arg.read() == b"reference data"
assert hasattr(img_arg, "name")
output = project_dir / "out.png" output = project_dir / "out.png"
assert output.exists() assert output.exists()
@ -517,9 +514,9 @@ class TestOpenAIImageProvider:
) )
call_args = mock_client.images.edit.call_args call_args = mock_client.images.edit.call_args
# gpt-image-* models pass a list of BytesIO with name attributes # Multiple reference images should be passed as a list of bytes
image_arg: list[io.BytesIO] = call_args.kwargs["image"] image_arg: list[bytes] = call_args.kwargs["image"]
assert isinstance(image_arg, list) assert isinstance(image_arg, list)
assert len(image_arg) == 2 assert len(image_arg) == 2
assert image_arg[0].read() == b"ref1 data" assert image_arg[0] == b"ref1 data"
assert image_arg[1].read() == b"ref2 data" assert image_arg[1] == b"ref2 data"

8
uv.lock generated
View file

@ -184,7 +184,7 @@ requires-dist = [
{ name = "httpx", specifier = ">=0.27.0" }, { name = "httpx", specifier = ">=0.27.0" },
{ name = "mistralai", specifier = ">=1.0.0" }, { name = "mistralai", specifier = ">=1.0.0" },
{ name = "networkx", specifier = ">=3.6.1" }, { name = "networkx", specifier = ">=3.6.1" },
{ name = "openai", specifier = ">=2.25.0" }, { name = "openai", specifier = ">=2.21.0" },
{ name = "pillow", specifier = ">=11.0.0" }, { name = "pillow", specifier = ">=11.0.0" },
{ name = "pydantic", specifier = ">=2.12.5" }, { name = "pydantic", specifier = ">=2.12.5" },
{ name = "pyyaml", specifier = ">=6.0" }, { name = "pyyaml", specifier = ">=6.0" },
@ -386,7 +386,7 @@ wheels = [
[[package]] [[package]]
name = "openai" name = "openai"
version = "2.25.0" version = "2.21.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "anyio" }, { name = "anyio" },
@ -398,9 +398,9 @@ dependencies = [
{ name = "tqdm" }, { name = "tqdm" },
{ name = "typing-extensions" }, { name = "typing-extensions" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/43/2b/a442b206ed74908dd79e2ad1ef3ffaeae66422b1fb506af981f0ef671ba0/openai-2.25.0.tar.gz", hash = "sha256:c3e1965d83c333dbd341eb2c7c8aceb783c272cd57fc57353404d9a443634e29", size = 666577, upload-time = "2026-03-05T18:35:38.292Z" } sdist = { url = "https://files.pythonhosted.org/packages/92/e5/3d197a0947a166649f566706d7a4c8f7fe38f1fa7b24c9bcffe4c7591d44/openai-2.21.0.tar.gz", hash = "sha256:81b48ce4b8bbb2cc3af02047ceb19561f7b1dc0d4e52d1de7f02abfd15aa59b7", size = 644374, upload-time = "2026-02-14T00:12:01.577Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/5b/e6/ca496976bacdd7f86d4fa69359c2d9b5a547d7acbf0ae5cac7bff107ff50/openai-2.25.0-py3-none-any.whl", hash = "sha256:7c7c01a0a4b69771a29913e28d06bc1e9cee8781b4d206bb56cb946d8d2fcb23", size = 1136347, upload-time = "2026-03-05T18:35:36.497Z" }, { url = "https://files.pythonhosted.org/packages/cc/56/0a89092a453bb2c676d66abee44f863e742b2110d4dbb1dbcca3f7e5fc33/openai-2.21.0-py3-none-any.whl", hash = "sha256:0bc1c775e5b1536c294eded39ee08f8407656537ccc71b1004104fe1602e267c", size = 1103065, upload-time = "2026-02-14T00:11:59.603Z" },
] ]
[[package]] [[package]]