test(integration): cover images endpoint contracts (#44653)

* test(integration): cover images endpoint contracts

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(integration): add SDK-driven image edit contract

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(integration): cover image route aliases, repeated mask[] and variations route

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(integration): cover AzureOpenAI images.edit alias and extra_body on gpt-image generation

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: mateo <mateo@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
devin-ai-integration[bot] 2026-10-14 03:40:29 +00:00 • committed by GitHub
parent 117684cce5
commit 35124f3b49
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 580 additions and 14 deletions

View file

@ -345,3 +345,54 @@ def test_fal_flux_dev_generation_without_deployment_api_base_uses_global_api_bas
}
]
assert [(request.method, request.target) for request in wire.drain()] == [("POST", "/fal-ai/flux/dev")]
def test_fal_gpt_image_25_edit_sends_multipart_n_as_integer_num_images(gateway: Gateway) -> None:
def respond(request: Request) -> Reply:
assert request.method == "POST"
assert request.headers["authorization"] == "Key synthetic-fal-key"
assert request.target == "/openai/gpt-image-2.5/flare/edit"
assert request.headers["content-type"] == "application/json"
assert _JSON_OBJECT.validate_json(request.body) == {
"prompt": _PROMPT,
"image_urls": ["data:image/png;base64," + base64.b64encode(_PNG_BYTES).decode()],
"num_images": 2,
}
return Reply(
body=_image_response(
(
(f"{wire_url}/files/edit-1.png", 1024, 1536),
(f"{wire_url}/files/edit-2.png", 1024, 1536),
),
_PROMPT,
)
)
with wire_server(respond) as wire, gateway.scenario() as scenario:
wire_url: Final = wire.url
model: Final = scenario.model(model=f"fal_ai/{_EDIT_MODEL}", api_base=wire.url, api_key="synthetic-fal-key")
response: Final = gateway.client.post(
"/v1/images/edits",
data={"model": model, "prompt": _PROMPT, "n": "2"},
files={"image": ("red_circle.png", _PNG_BYTES, "image/png")},
headers={"Authorization": f"Bearer {gateway.key}"},
)
assert response.status_code == 200, response.text
payload: Final = _JSON_OBJECT.validate_json(response.content)
assert payload["data"] == [
{
"url": f"{wire.url}/files/edit-1.png",
"b64_json": None,
"revised_prompt": None,
"provider_specific_fields": {"width": 1024, "height": 1536, "content_type": "image/png"},
},
{
"url": f"{wire.url}/files/edit-2.png",
"b64_json": None,
"revised_prompt": None,
"provider_specific_fields": {"width": 1024, "height": 1536, "content_type": "image/png"},
},
]
assert [(request.method, request.target) for request in wire.drain()] == [
("POST", "/openai/gpt-image-2.5/flare/edit")
]

View file

@ -1,11 +1,14 @@
import json
from typing import Final
import openai
import pytest
from integration._support.client import Gateway
from integration._support.wire import Reply, Request, wire_server
from pydantic import JsonValue, TypeAdapter
_JSON_OBJECT: Final = TypeAdapter(dict[str, JsonValue])
_PROMPT: Final = "a transparent lighthouse at dawn"
def test_image_generation_additional_drop_params_reaches_provider_body(gateway: Gateway) -> None:
@ -47,3 +50,85 @@ def test_image_generation_additional_drop_params_reaches_provider_body(gateway:
assert response.status_code == 200, response.text
assert response.json()["data"][0]["b64_json"] == "aW1n"
assert [(request.method, request.target) for request in wire.drain()] == [("POST", "/images/generations")]
def _openai_client(gateway: Gateway, model: str, alias: str) -> openai.OpenAI:
base: Final = str(gateway.client.base_url)
match alias:
case "v1":
return openai.OpenAI(base_url=f"{base}/v1", api_key=gateway.key, max_retries=0)
case "unversioned":
return openai.OpenAI(base_url=base, api_key=gateway.key, max_retries=0)
case "azure_deployment":
return openai.AzureOpenAI(
azure_endpoint=base,
azure_deployment=model,
api_version="2025-04-01-preview",
api_key=gateway.key,
max_retries=0,
)
raise AssertionError(alias)
@pytest.mark.parametrize("alias", ("v1", "unversioned", "azure_deployment"))
def test_gpt_image_generation_forwards_every_sdk_field_once_with_integer_n(gateway: Gateway, alias: str) -> None:
def respond(request: Request) -> Reply:
assert request.method == "POST"
assert request.target == "/images/generations"
assert request.headers["authorization"] == "Bearer synthetic-openai-key"
assert _JSON_OBJECT.validate_json(request.body) == {
"model": "gpt-image-1",
"prompt": _PROMPT,
"n": 2,
"size": "1024x1536",
"quality": "high",
"background": "transparent",
"output_format": "webp",
"moderation": "low",
"partial_images": 0,
}
return Reply(
body=json.dumps(
{
"created": 1700000000,
"data": [{"b64_json": "aW1nLTE="}, {"b64_json": "aW1nLTI="}],
"background": "transparent",
"output_format": "webp",
"quality": "high",
"size": "1024x1536",
"usage": {
"input_tokens": 12,
"input_tokens_details": {"text_tokens": 12, "image_tokens": 0},
"output_tokens": 4160,
"total_tokens": 4172,
},
}
).encode()
)
with wire_server(respond) as wire, gateway.scenario() as scenario:
model: Final = scenario.model(
model="openai/gpt-image-1",
api_base=wire.url,
api_key="synthetic-openai-key",
)
client: Final = _openai_client(gateway, model, alias)
result: Final = client.images.generate(
model=model,
prompt=_PROMPT,
n=2,
size="1024x1536",
quality="high",
background="transparent",
output_format="webp",
moderation="low",
extra_body={"partial_images": 0},
)
assert [image.b64_json for image in result.data] == ["aW1nLTE=", "aW1nLTI="]
assert result.usage is not None
assert (
result.usage.input_tokens,
result.usage.output_tokens,
result.usage.total_tokens,
) == (12, 4160, 4172)
assert [(request.method, request.target) for request in wire.drain()] == [("POST", "/images/generations")]

View file

@ -1,9 +1,15 @@
import json
import struct
import zlib
from collections.abc import Mapping, Sequence
from dataclasses import dataclass
from email.message import Message
from email.parser import BytesParser
from email.policy import HTTP
from typing import Final
from typing import Final, cast
import httpx
import openai
import pytest
from integration._support.client import Gateway
from integration._support.wire import Reply, Request, wire_server
@ -18,6 +24,31 @@ _PROMPT: Final = "turn the red circle green"
_EDITED_IMAGE_B64: Final = "aW50ZWdyYXRpb24tZWRpdGVkLWltYWdl"
def _png_chunk(chunk_type: bytes, chunk_data: bytes) -> bytes:
checksum: Final = zlib.crc32(chunk_type + chunk_data) & 0xFFFFFFFF
return (
struct.pack(">I", len(chunk_data))
+ chunk_type
+ chunk_data
+ struct.pack(">I", checksum)
)
def _png_from_rgba_pixel(pixel: tuple[int, int, int, int]) -> bytes:
header: Final = struct.pack(">IIBBBBB", 1, 1, 8, 6, 0, 0, 0)
compressed_pixel: Final = zlib.compress(b"\x00" + bytes(pixel))
return (
b"\x89PNG\r\n\x1a\n"
+ _png_chunk(b"IHDR", header)
+ _png_chunk(b"IDAT", compressed_pixel)
+ _png_chunk(b"IEND", b"")
)
_SECOND_PNG: Final = _png_from_rgba_pixel((0, 0, 255, 255))
_MASK_PNG: Final = _png_from_rgba_pixel((0, 0, 0, 0))
class _Image(BaseModel):
b64_json: str
@ -33,20 +64,52 @@ def _multipart_parts(request: Request) -> tuple[Message, ...]:
return tuple(parsed.iter_parts())
def _text_fields(parts: tuple[Message, ...]) -> dict[str, str]:
return {
part.get_param("name", header="content-disposition"): part.get_payload(decode=True).decode()
for part in parts
if part.get_filename() is None
}
@dataclass(frozen=True, slots=True)
class _FilePart:
name: str
filename: str
content_type: str
content: bytes
def _file_fields(parts: tuple[Message, ...]) -> dict[str, bytes]:
return {
part.get_param("name", header="content-disposition"): part.get_payload(decode=True)
def _text_parts(parts: tuple[Message, ...]) -> tuple[tuple[str, str], ...]:
return tuple(
sorted(
(
cast(str, part.get_param("name", header="content-disposition")),
cast(bytes, part.get_payload(decode=True)).decode(),
)
for part in parts
if part.get_filename() is None
)
)
def _file_parts(parts: tuple[Message, ...]) -> tuple[_FilePart, ...]:
return tuple(
_FilePart(
name=cast(str, part.get_param("name", header="content-disposition")),
filename=filename,
content_type=part.get_content_type(),
content=cast(bytes, part.get_payload(decode=True)),
)
for part in parts
if part.get_filename() is not None
}
if (filename := part.get_filename()) is not None
)
def _edit(
gateway: Gateway,
fields: Mapping[str, str],
files: Sequence[tuple[str, tuple[str, bytes, str]]],
path: str = "/v1/images/edits",
) -> httpx.Response:
return gateway.client.post(
path,
data=dict(fields),
files=list(files),
headers={"Authorization": f"Bearer {gateway.key}"},
)
@pytest.mark.covers("other.provider_wire.openai.image_edit_forwards_provider_specific_form_fields")
@ -55,9 +118,10 @@ def test_openai_compatible_image_edit_forwards_seed_form_field_to_backend(gatewa
assert request.method == "POST"
assert request.target == "/v1/images/edits"
assert request.headers["authorization"] == "Bearer synthetic-openai-key"
assert request.headers["content-type"].startswith("multipart/form-data; boundary=")
parts: Final = _multipart_parts(request)
assert _text_fields(parts) == {"model": "gpt-image-1", "prompt": _PROMPT, "seed": "42"}
assert _file_fields(parts) == {"image[]": _PNG_BYTES}
assert _text_parts(parts) == (("model", "gpt-image-1"), ("prompt", _PROMPT), ("seed", "42"))
assert _file_parts(parts) == (_FilePart("image[]", "image.png", "image/png", _PNG_BYTES),)
return Reply(body=json.dumps({"created": 1700000000, "data": [{"b64_json": _EDITED_IMAGE_B64}]}).encode())
with wire_server(respond) as wire, gateway.scenario() as scenario:
@ -73,3 +137,299 @@ def test_openai_compatible_image_edit_forwards_seed_form_field_to_backend(gatewa
payload: Final = _ImageResponse.model_validate_json(response.content)
assert [image.b64_json for image in payload.data] == [_EDITED_IMAGE_B64], response.text
assert [(request.method, request.target) for request in wire.drain()] == [("POST", "/v1/images/edits")]
@pytest.mark.parametrize(
"path_template",
("/v1/images/edits", "/images/edits", "/openai/deployments/{model}/images/edits"),
ids=("v1", "unversioned", "azure_deployment"),
)
def test_image_edit_bracketed_image_field_reaches_openai_as_one_file_part(
gateway: Gateway, path_template: str
) -> None:
def respond(request: Request) -> Reply:
assert request.method == "POST"
assert request.target == "/v1/images/edits"
assert request.headers["authorization"] == "Bearer synthetic-openai-key"
assert request.headers["content-type"].startswith("multipart/form-data; boundary=")
parts: Final = _multipart_parts(request)
assert _text_parts(parts) == (("model", "gpt-image-1"), ("prompt", _PROMPT), ("seed", "42"))
assert _file_parts(parts) == (_FilePart("image[]", "image.png", "image/png", _PNG_BYTES),)
return Reply(body=json.dumps({"created": 1700000000, "data": [{"b64_json": _EDITED_IMAGE_B64}]}).encode())
with wire_server(respond) as wire, gateway.scenario() as scenario:
model: Final = scenario.model(
model="openai/gpt-image-1", api_base=f"{wire.url}/v1", api_key="synthetic-openai-key"
)
response: Final = _edit(
gateway,
{"model": model, "prompt": _PROMPT, "seed": "42"},
(("image[]", ("red_circle.png", _PNG_BYTES, "image/png")),),
path_template.format(model=model),
)
assert response.status_code == 200, response.text
payload: Final = _ImageResponse.model_validate_json(response.content)
assert [image.b64_json for image in payload.data] == [_EDITED_IMAGE_B64], response.text
assert [(request.method, request.target) for request in wire.drain()] == [("POST", "/v1/images/edits")]
def test_image_edit_repeated_bracketed_image_parts_reach_openai_in_upload_order(gateway: Gateway) -> None:
def respond(request: Request) -> Reply:
assert request.method == "POST"
assert request.target == "/v1/images/edits"
assert request.headers["authorization"] == "Bearer synthetic-openai-key"
assert request.headers["content-type"].startswith("multipart/form-data; boundary=")
parts: Final = _multipart_parts(request)
assert _text_parts(parts) == (("model", "gpt-image-1"), ("n", "2"), ("prompt", _PROMPT))
assert _file_parts(parts) == (
_FilePart("image[]", "image.png", "image/png", _PNG_BYTES),
_FilePart("image[]", "image.png", "image/png", _SECOND_PNG),
)
return Reply(body=json.dumps({"created": 1700000000, "data": [{"b64_json": _EDITED_IMAGE_B64}]}).encode())
with wire_server(respond) as wire, gateway.scenario() as scenario:
model: Final = scenario.model(
model="openai/gpt-image-1", api_base=f"{wire.url}/v1", api_key="synthetic-openai-key"
)
response: Final = _edit(
gateway,
{"model": model, "prompt": _PROMPT, "n": "2"},
(
("image[]", ("first.png", _PNG_BYTES, "image/png")),
("image[]", ("second.png", _SECOND_PNG, "image/png")),
),
)
assert response.status_code == 200, response.text
payload: Final = _ImageResponse.model_validate_json(response.content)
assert [image.b64_json for image in payload.data] == [_EDITED_IMAGE_B64], response.text
assert [(request.method, request.target) for request in wire.drain()] == [("POST", "/v1/images/edits")]
def test_image_edit_bracketed_mask_reaches_openai_as_mask_file_part(gateway: Gateway) -> None:
def respond(request: Request) -> Reply:
assert request.method == "POST"
assert request.target == "/v1/images/edits"
assert request.headers["authorization"] == "Bearer synthetic-openai-key"
assert request.headers["content-type"].startswith("multipart/form-data; boundary=")
parts: Final = _multipart_parts(request)
assert _text_parts(parts) == (("model", "gpt-image-1"), ("prompt", _PROMPT))
assert _file_parts(parts) == (
_FilePart("image[]", "image.png", "image/png", _PNG_BYTES),
_FilePart("mask", "mask.png", "image/png", _MASK_PNG),
)
return Reply(body=json.dumps({"created": 1700000000, "data": [{"b64_json": _EDITED_IMAGE_B64}]}).encode())
with wire_server(respond) as wire, gateway.scenario() as scenario:
model: Final = scenario.model(
model="openai/gpt-image-1", api_base=f"{wire.url}/v1", api_key="synthetic-openai-key"
)
response: Final = _edit(
gateway,
{"model": model, "prompt": _PROMPT},
(
("image[]", ("red_circle.png", _PNG_BYTES, "image/png")),
("mask[]", ("mask.png", _MASK_PNG, "image/png")),
),
)
assert response.status_code == 200, response.text
payload: Final = _ImageResponse.model_validate_json(response.content)
assert [image.b64_json for image in payload.data] == [_EDITED_IMAGE_B64], response.text
assert [(request.method, request.target) for request in wire.drain()] == [("POST", "/v1/images/edits")]
def test_image_edit_repeated_bracketed_mask_parts_reach_openai_as_first_mask_only(gateway: Gateway) -> None:
def respond(request: Request) -> Reply:
assert request.method == "POST"
assert request.target == "/v1/images/edits"
assert request.headers["authorization"] == "Bearer synthetic-openai-key"
assert request.headers["content-type"].startswith("multipart/form-data; boundary=")
parts: Final = _multipart_parts(request)
assert _text_parts(parts) == (("model", "gpt-image-1"), ("prompt", _PROMPT))
assert _file_parts(parts) == (
_FilePart("image[]", "image.png", "image/png", _PNG_BYTES),
_FilePart("mask", "mask.png", "image/png", _MASK_PNG),
)
return Reply(body=json.dumps({"created": 1700000000, "data": [{"b64_json": _EDITED_IMAGE_B64}]}).encode())
with wire_server(respond) as wire, gateway.scenario() as scenario:
model: Final = scenario.model(
model="openai/gpt-image-1", api_base=f"{wire.url}/v1", api_key="synthetic-openai-key"
)
response: Final = _edit(
gateway,
{"model": model, "prompt": _PROMPT},
(
("image[]", ("red_circle.png", _PNG_BYTES, "image/png")),
("mask[]", ("first-mask.png", _MASK_PNG, "image/png")),
("mask[]", ("second-mask.png", _SECOND_PNG, "image/png")),
),
)
assert response.status_code == 200, response.text
payload: Final = _ImageResponse.model_validate_json(response.content)
assert [image.b64_json for image in payload.data] == [_EDITED_IMAGE_B64], response.text
assert [(request.method, request.target) for request in wire.drain()] == [("POST", "/v1/images/edits")]
def test_image_edit_plain_mask_reaches_openai_as_mask_file_part(gateway: Gateway) -> None:
def respond(request: Request) -> Reply:
assert request.method == "POST"
assert request.target == "/v1/images/edits"
assert request.headers["authorization"] == "Bearer synthetic-openai-key"
assert request.headers["content-type"].startswith("multipart/form-data; boundary=")
parts: Final = _multipart_parts(request)
assert _text_parts(parts) == (("model", "gpt-image-1"), ("prompt", _PROMPT))
assert _file_parts(parts) == (
_FilePart("image[]", "image.png", "image/png", _PNG_BYTES),
_FilePart("mask", "mask.png", "image/png", _MASK_PNG),
)
return Reply(body=json.dumps({"created": 1700000000, "data": [{"b64_json": _EDITED_IMAGE_B64}]}).encode())
with wire_server(respond) as wire, gateway.scenario() as scenario:
model: Final = scenario.model(
model="openai/gpt-image-1", api_base=f"{wire.url}/v1", api_key="synthetic-openai-key"
)
response: Final = _edit(
gateway,
{"model": model, "prompt": _PROMPT},
(
("image", ("red_circle.png", _PNG_BYTES, "image/png")),
("mask", ("mask.png", _MASK_PNG, "image/png")),
),
)
assert response.status_code == 200, response.text
payload: Final = _ImageResponse.model_validate_json(response.content)
assert [image.b64_json for image in payload.data] == [_EDITED_IMAGE_B64], response.text
assert [(request.method, request.target) for request in wire.drain()] == [("POST", "/v1/images/edits")]
def test_image_edit_repeated_plain_image_parts_reach_openai_as_two_file_parts(gateway: Gateway) -> None:
def respond(request: Request) -> Reply:
assert request.method == "POST"
assert request.target == "/v1/images/edits"
assert request.headers["authorization"] == "Bearer synthetic-openai-key"
assert request.headers["content-type"].startswith("multipart/form-data; boundary=")
parts: Final = _multipart_parts(request)
assert _text_parts(parts) == (("model", "gpt-image-1"), ("prompt", _PROMPT))
assert _file_parts(parts) == (
_FilePart("image[]", "image.png", "image/png", _PNG_BYTES),
_FilePart("image[]", "image.png", "image/png", _SECOND_PNG),
)
return Reply(body=json.dumps({"created": 1700000000, "data": [{"b64_json": _EDITED_IMAGE_B64}]}).encode())
with wire_server(respond) as wire, gateway.scenario() as scenario:
model: Final = scenario.model(
model="openai/gpt-image-1", api_base=f"{wire.url}/v1", api_key="synthetic-openai-key"
)
response: Final = _edit(
gateway,
{"model": model, "prompt": _PROMPT},
(
("image", ("first.png", _PNG_BYTES, "image/png")),
("image", ("second.png", _SECOND_PNG, "image/png")),
),
)
assert response.status_code == 200, response.text
payload: Final = _ImageResponse.model_validate_json(response.content)
assert [image.b64_json for image in payload.data] == [_EDITED_IMAGE_B64], response.text
assert [(request.method, request.target) for request in wire.drain()] == [("POST", "/v1/images/edits")]
def _sdk_client(gateway: Gateway, model: str, alias: str) -> openai.OpenAI:
base: Final = str(gateway.client.base_url)
match alias:
case "v1":
return openai.OpenAI(base_url=f"{base}/v1", api_key=gateway.key, max_retries=0)
case "azure_deployment":
return openai.AzureOpenAI(
azure_endpoint=base,
azure_deployment=model,
api_version="2025-04-01-preview",
api_key=gateway.key,
max_retries=0,
)
raise AssertionError(alias)
@pytest.mark.parametrize("alias", ("v1", "azure_deployment"))
def test_openai_sdk_image_edit_with_image_list_and_mask_reaches_openai_as_file_parts(
gateway: Gateway, alias: str
) -> None:
def respond(request: Request) -> Reply:
assert request.method == "POST"
assert request.target == "/v1/images/edits"
assert request.headers["authorization"] == "Bearer synthetic-openai-key"
assert request.headers["content-type"].startswith("multipart/form-data; boundary=")
parts: Final = _multipart_parts(request)
assert _text_parts(parts) == (("model", "gpt-image-1"), ("n", "2"), ("prompt", _PROMPT))
assert _file_parts(parts) == (
_FilePart("image[]", "image.png", "image/png", _PNG_BYTES),
_FilePart("image[]", "image.png", "image/png", _SECOND_PNG),
_FilePart("mask", "mask.png", "image/png", _MASK_PNG),
)
return Reply(body=json.dumps({"created": 1700000000, "data": [{"b64_json": _EDITED_IMAGE_B64}]}).encode())
with wire_server(respond) as wire, gateway.scenario() as scenario:
model: Final = scenario.model(
model="openai/gpt-image-1", api_base=f"{wire.url}/v1", api_key="synthetic-openai-key"
)
client: Final = _sdk_client(gateway, model, alias)
result: Final = client.images.edit(
model=model,
image=[
("first.png", _PNG_BYTES, "image/png"),
("second.png", _SECOND_PNG, "image/png"),
],
mask=("mask.png", _MASK_PNG, "image/png"),
prompt=_PROMPT,
n=2,
)
assert [image.b64_json for image in result.data] == [_EDITED_IMAGE_B64]
assert [(request.method, request.target) for request in wire.drain()] == [("POST", "/v1/images/edits")]
@pytest.mark.parametrize(
("files", "detail"),
(
pytest.param(
(
("image", ("red_circle.png", _PNG_BYTES, "image/png")),
("image[]", ("second.png", _SECOND_PNG, "image/png")),
),
"Cannot specify both 'image' and 'image[]'",
id="image",
),
pytest.param(
(
("image", ("red_circle.png", _PNG_BYTES, "image/png")),
("mask", ("mask.png", _MASK_PNG, "image/png")),
("mask[]", ("mask-array.png", _MASK_PNG, "image/png")),
),
"Cannot specify both 'mask' and 'mask[]'",
id="mask",
),
),
)
def test_image_edit_rejects_plain_and_bracketed_spelling_together_without_calling_upstream(
gateway: Gateway,
files: Sequence[tuple[str, tuple[str, bytes, str]]],
detail: str,
) -> None:
def respond(request: Request) -> Reply:
assert request.method == "POST"
assert request.target == "/v1/images/edits"
assert request.headers["authorization"] == "Bearer synthetic-openai-key"
assert request.headers["content-type"].startswith("multipart/form-data; boundary=")
return Reply(body=json.dumps({"created": 1700000000, "data": [{"b64_json": _EDITED_IMAGE_B64}]}).encode())
with wire_server(respond) as wire, gateway.scenario() as scenario:
model: Final = scenario.model(
model="openai/gpt-image-1", api_base=f"{wire.url}/v1", api_key="synthetic-openai-key"
)
response: Final = _edit(
gateway,
{"model": model, "prompt": _PROMPT},
files,
)
assert response.status_code == 422, response.text
assert response.json() == {"detail": detail}, response.text
assert wire.drain() == ()

View file

@ -0,0 +1,70 @@
import json
from email.message import Message
from email.parser import BytesParser
from email.policy import HTTP
from typing import Final, cast
import pytest
from integration._support.client import Gateway
from integration._support.wire import Reply, Request, wire_server
from pydantic import BaseModel
_PNG_BYTES: Final = (
b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00"
b"\x1f\x15\xc4\x89\x00\x00\x00\rIDAT\x08\xd7c\xf8\xcf\xc0\xf0\x1f\x00\x05\x00\x01\xff"
b"\x89\x99=\x1d\x00\x00\x00\x00IEND\xaeB`\x82"
)
_VARIATION_URLS: Final = ("https://images.example/variation-1.png", "https://images.example/variation-2.png")
class _Image(BaseModel):
url: str
class _ImageResponse(BaseModel):
data: tuple[_Image, ...]
def _multipart_parts(request: Request) -> tuple[Message, ...]:
envelope: Final = f"content-type: {request.headers['content-type']}\r\n\r\n".encode() + request.body
parsed: Final = BytesParser(policy=HTTP).parsebytes(envelope)
assert parsed.is_multipart(), request.headers["content-type"]
return tuple(parsed.iter_parts())
def test_image_variation_multipart_image_and_string_n_reach_openai_as_file_part(gateway: Gateway) -> None:
pytest.skip("BUG: POST /v1/images/variations is not routed by the proxy")
def respond(request: Request) -> Reply:
assert request.method == "POST"
assert request.target == "/v1/images/variations"
assert request.headers["authorization"] == "Bearer synthetic-openai-key"
assert request.headers["content-type"].startswith("multipart/form-data; boundary=")
parts: Final = _multipart_parts(request)
assert sorted(
(
cast(str, part.get_param("name", header="content-disposition")),
cast(bytes, part.get_payload(decode=True)).decode(),
)
for part in parts
if part.get_filename() is None
) == [("model", "dall-e-2"), ("n", "2")]
assert [
(cast(str, part.get_param("name", header="content-disposition")), cast(bytes, part.get_payload(decode=True)))
for part in parts
if part.get_filename()
] == [("image", _PNG_BYTES)]
return Reply(body=json.dumps({"created": 1700000000, "data": [{"url": url} for url in _VARIATION_URLS]}).encode())
with wire_server(respond) as wire, gateway.scenario() as scenario:
model: Final = scenario.model(model="openai/dall-e-2", api_base=f"{wire.url}/v1", api_key="synthetic-openai-key")
response: Final = gateway.client.post(
"/v1/images/variations",
data={"model": model, "n": "2"},
files={"image": ("red_circle.png", _PNG_BYTES, "image/png")},
headers={"Authorization": f"Bearer {gateway.key}"},
)
assert response.status_code == 200, response.text
payload: Final = _ImageResponse.model_validate_json(response.content)
assert [image.url for image in payload.data] == list(_VARIATION_URLS), response.text
assert [(request.method, request.target) for request in wire.drain()] == [("POST", "/v1/images/variations")]