Files
wehub-resource-sync cddb07a176
docs / deploy (push) Has been cancelled
docs / changes (push) Has been cancelled
docs / check-and-build (push) Has been cancelled
build container image / cpu (push) Has been cancelled
build container image / cuda (push) Has been cancelled
build container image / rocm (push) Has been cancelled
frontend checks / frontend-checks (push) Has been cancelled
frontend tests / frontend-tests (push) Has been cancelled
lfs checks / lfs-check (push) Has been cancelled
python checks / python-checks (push) Has been cancelled
python tests / py3.12: macos-default (push) Has been cancelled
python tests / py3.11: windows-cpu (push) Has been cancelled
python tests / py3.12: windows-cpu (push) Has been cancelled
python tests / py3.11: linux-cpu (push) Has been cancelled
typegen checks / typegen-checks (push) Has been cancelled
uv lock checks / uv-lock-checks (push) Has been cancelled
openapi checks / openapi-checks (push) Has been cancelled
python tests / py3.11: macos-default (push) Has been cancelled
python tests / py3.12: linux-cpu (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:22:06 +08:00

355 lines
13 KiB
Python

import logging
import pytest
from PIL import Image
from invokeai.app.services.config.config_default import InvokeAIAppConfig
from invokeai.app.services.external_generation.errors import (
ExternalProviderCapabilityError,
ExternalProviderRequestError,
)
from invokeai.app.services.external_generation.external_generation_common import (
ExternalGenerationRequest,
ExternalReferenceImage,
)
from invokeai.app.services.external_generation.image_utils import encode_image_base64
from invokeai.app.services.external_generation.providers.seedream import SeedreamProvider
from invokeai.backend.model_manager.configs.external_api import ExternalApiModelConfig, ExternalModelCapabilities
class DummyResponse:
def __init__(self, ok: bool, status_code: int = 200, json_data: dict | None = None, text: str = "") -> None:
self.ok = ok
self.status_code = status_code
self._json_data = json_data or {}
self.text = text
self.headers: dict[str, str] = {}
def json(self) -> dict:
return self._json_data
def _make_image(color: str = "black") -> Image.Image:
return Image.new("RGB", (32, 32), color=color)
def _build_model(provider_model_id: str, modes: list[str] | None = None) -> ExternalApiModelConfig:
return ExternalApiModelConfig(
key="seedream_test",
name="Seedream Test",
provider_id="seedream",
provider_model_id=provider_model_id,
capabilities=ExternalModelCapabilities(
modes=modes or ["txt2img", "img2img"],
supports_reference_images=True,
supports_seed=True,
),
)
def _build_request(
model: ExternalApiModelConfig,
mode: str = "txt2img",
init_image: Image.Image | None = None,
reference_images: list[ExternalReferenceImage] | None = None,
num_images: int = 1,
seed: int | None = 123,
provider_options: dict | None = None,
) -> ExternalGenerationRequest:
return ExternalGenerationRequest(
model=model,
mode=mode, # type: ignore[arg-type]
prompt="A test prompt",
seed=seed,
num_images=num_images,
width=2048,
height=2048,
image_size=None,
init_image=init_image,
mask_image=None,
reference_images=reference_images or [],
metadata=None,
provider_options=provider_options,
)
def test_seedream_is_configured() -> None:
config = InvokeAIAppConfig(external_seedream_api_key="test-key")
provider = SeedreamProvider(config, logging.getLogger("test"))
assert provider.is_configured() is True
def test_seedream_not_configured() -> None:
config = InvokeAIAppConfig()
provider = SeedreamProvider(config, logging.getLogger("test"))
assert provider.is_configured() is False
def test_seedream_txt2img_success(monkeypatch: pytest.MonkeyPatch) -> None:
api_key = "seedream-key"
config = InvokeAIAppConfig(external_seedream_api_key=api_key)
provider = SeedreamProvider(config, logging.getLogger("test"))
model = _build_model("seedream-4-5-251128")
request = _build_request(model)
encoded = encode_image_base64(_make_image("green"))
captured: dict[str, object] = {}
def fake_post(url: str, headers: dict, json: dict, timeout: int) -> DummyResponse:
captured["url"] = url
captured["headers"] = headers
captured["json"] = json
return DummyResponse(ok=True, json_data={"data": [{"b64_json": encoded}]})
monkeypatch.setattr("requests.post", fake_post)
result = provider.generate(request)
assert captured["url"] == "https://ark.ap-southeast.bytepluses.com/api/v3/images/generations"
headers = captured["headers"]
assert isinstance(headers, dict)
assert headers["Authorization"] == f"Bearer {api_key}"
json_payload = captured["json"]
assert isinstance(json_payload, dict)
assert json_payload["model"] == "seedream-4-5-251128"
assert json_payload["prompt"] == "A test prompt"
assert json_payload["size"] == "2048x2048"
assert json_payload["response_format"] == "b64_json"
assert json_payload["watermark"] is False
assert json_payload["sequential_image_generation"] == "disabled"
# Seed should not be sent for 4.x models
assert "seed" not in json_payload
# Guidance should not be sent for 4.x models
assert "guidance_scale" not in json_payload
assert len(result.images) == 1
assert result.images[0].seed == 123
def test_seedream_3_0_t2i_sends_seed_and_guidance(monkeypatch: pytest.MonkeyPatch) -> None:
config = InvokeAIAppConfig(external_seedream_api_key="seedream-key")
provider = SeedreamProvider(config, logging.getLogger("test"))
model = _build_model("seedream-3-0-t2i-250415", modes=["txt2img"])
request = _build_request(model, seed=42, provider_options={"guidance_scale": 2.5})
encoded = encode_image_base64(_make_image("green"))
captured: dict[str, object] = {}
def fake_post(url: str, headers: dict, json: dict, timeout: int) -> DummyResponse:
captured["json"] = json
return DummyResponse(ok=True, json_data={"data": [{"b64_json": encoded}]})
monkeypatch.setattr("requests.post", fake_post)
provider.generate(request)
json_payload = captured["json"]
assert isinstance(json_payload, dict)
assert json_payload["seed"] == 42
assert json_payload["guidance_scale"] == 2.5
# 3.0 models should not have sequential_image_generation
assert "sequential_image_generation" not in json_payload
def test_seedream_batch_generation(monkeypatch: pytest.MonkeyPatch) -> None:
config = InvokeAIAppConfig(external_seedream_api_key="seedream-key")
provider = SeedreamProvider(config, logging.getLogger("test"))
model = _build_model("seedream-4-5-251128")
request = _build_request(model, num_images=3)
encoded = encode_image_base64(_make_image("green"))
captured: dict[str, object] = {}
def fake_post(url: str, headers: dict, json: dict, timeout: int) -> DummyResponse:
captured["json"] = json
return DummyResponse(
ok=True,
json_data={"data": [{"b64_json": encoded}, {"b64_json": encoded}, {"b64_json": encoded}]},
)
monkeypatch.setattr("requests.post", fake_post)
result = provider.generate(request)
json_payload = captured["json"]
assert isinstance(json_payload, dict)
assert json_payload["sequential_image_generation"] == "auto"
assert json_payload["sequential_image_generation_options"] == {"max_images": 3}
assert len(result.images) == 3
def test_seedream_img2img_with_reference_images(monkeypatch: pytest.MonkeyPatch) -> None:
config = InvokeAIAppConfig(external_seedream_api_key="seedream-key")
provider = SeedreamProvider(config, logging.getLogger("test"))
model = _build_model("seedream-4-5-251128")
init_image = _make_image("blue")
ref_image = _make_image("red")
request = _build_request(
model,
mode="img2img",
init_image=init_image,
reference_images=[ExternalReferenceImage(image=ref_image)],
)
encoded = encode_image_base64(_make_image("green"))
captured: dict[str, object] = {}
def fake_post(url: str, headers: dict, json: dict, timeout: int) -> DummyResponse:
captured["json"] = json
return DummyResponse(ok=True, json_data={"data": [{"b64_json": encoded}]})
monkeypatch.setattr("requests.post", fake_post)
result = provider.generate(request)
json_payload = captured["json"]
assert isinstance(json_payload, dict)
images = json_payload["image"]
assert isinstance(images, list)
assert len(images) == 2 # init_image + reference
assert images[0].startswith("data:image/png;base64,")
assert images[1].startswith("data:image/png;base64,")
assert len(result.images) == 1
def test_seedream_single_image_not_array(monkeypatch: pytest.MonkeyPatch) -> None:
config = InvokeAIAppConfig(external_seedream_api_key="seedream-key")
provider = SeedreamProvider(config, logging.getLogger("test"))
model = _build_model("seedream-3-0-t2i-250415", modes=["txt2img"])
init_image = _make_image("blue")
request = _build_request(model, mode="txt2img", init_image=init_image, provider_options={"guidance_scale": 5.5})
encoded = encode_image_base64(_make_image("green"))
captured: dict[str, object] = {}
def fake_post(url: str, headers: dict, json: dict, timeout: int) -> DummyResponse:
captured["json"] = json
return DummyResponse(ok=True, json_data={"data": [{"b64_json": encoded}]})
monkeypatch.setattr("requests.post", fake_post)
provider.generate(request)
json_payload = captured["json"]
assert isinstance(json_payload, dict)
# Single image should be a string, not an array
image = json_payload["image"]
assert isinstance(image, str)
assert image.startswith("data:image/png;base64,")
def test_seedream_error_response(monkeypatch: pytest.MonkeyPatch) -> None:
config = InvokeAIAppConfig(external_seedream_api_key="seedream-key")
provider = SeedreamProvider(config, logging.getLogger("test"))
model = _build_model("seedream-4-5-251128")
request = _build_request(model)
def fake_post(url: str, headers: dict, json: dict, timeout: int) -> DummyResponse:
return DummyResponse(ok=False, status_code=400, text="bad request")
monkeypatch.setattr("requests.post", fake_post)
with pytest.raises(ExternalProviderRequestError, match="Seedream request failed"):
provider.generate(request)
def test_seedream_no_api_key_raises(monkeypatch: pytest.MonkeyPatch) -> None:
config = InvokeAIAppConfig()
provider = SeedreamProvider(config, logging.getLogger("test"))
model = _build_model("seedream-4-5-251128")
request = _build_request(model)
with pytest.raises(ExternalProviderRequestError, match="API key is not configured"):
provider.generate(request)
def test_seedream_uses_base_url(monkeypatch: pytest.MonkeyPatch) -> None:
config = InvokeAIAppConfig(
external_seedream_api_key="seedream-key",
external_seedream_base_url="https://proxy.seedream/",
)
provider = SeedreamProvider(config, logging.getLogger("test"))
model = _build_model("seedream-4-5-251128")
request = _build_request(model)
encoded = encode_image_base64(_make_image("green"))
captured: dict[str, object] = {}
def fake_post(url: str, headers: dict, json: dict, timeout: int) -> DummyResponse:
captured["url"] = url
return DummyResponse(ok=True, json_data={"data": [{"b64_json": encoded}]})
monkeypatch.setattr("requests.post", fake_post)
provider.generate(request)
assert captured["url"] == "https://proxy.seedream/api/v3/images/generations"
def test_seedream_batch_surfaces_partial_failures(monkeypatch: pytest.MonkeyPatch) -> None:
config = InvokeAIAppConfig(external_seedream_api_key="seedream-key")
provider = SeedreamProvider(config, logging.getLogger("test"))
model = _build_model("seedream-4-5-251128")
request = _build_request(model, num_images=3)
encoded = encode_image_base64(_make_image("green"))
def fake_post(url: str, headers: dict, json: dict, timeout: int) -> DummyResponse:
return DummyResponse(
ok=True,
json_data={
"data": [
{"b64_json": encoded},
{"error": {"code": "content_filter", "message": "filtered"}},
{"b64_json": encoded},
]
},
)
monkeypatch.setattr("requests.post", fake_post)
result = provider.generate(request)
assert len(result.images) == 2
assert result.provider_metadata is not None
partial_failures = result.provider_metadata.get("partial_failures")
assert isinstance(partial_failures, list) and len(partial_failures) == 1
assert partial_failures[0] == {"code": "content_filter", "message": "filtered"}
def test_seedream_batch_all_items_failed_raises(monkeypatch: pytest.MonkeyPatch) -> None:
config = InvokeAIAppConfig(external_seedream_api_key="seedream-key")
provider = SeedreamProvider(config, logging.getLogger("test"))
model = _build_model("seedream-4-5-251128")
request = _build_request(model, num_images=2)
def fake_post(url: str, headers: dict, json: dict, timeout: int) -> DummyResponse:
return DummyResponse(
ok=True,
json_data={
"data": [
{"error": {"code": "content_filter", "message": "filtered"}},
{"error": {"code": "content_filter", "message": "filtered"}},
]
},
)
monkeypatch.setattr("requests.post", fake_post)
with pytest.raises(ExternalProviderRequestError, match="filtered"):
provider.generate(request)
def test_seedream_rejects_combined_reference_and_output_count(monkeypatch: pytest.MonkeyPatch) -> None:
config = InvokeAIAppConfig(external_seedream_api_key="seedream-key")
provider = SeedreamProvider(config, logging.getLogger("test"))
model = _build_model("seedream-4-5-251128")
references = [ExternalReferenceImage(image=_make_image("red")) for _ in range(14)]
request = _build_request(model, num_images=15, reference_images=references)
posted = False
def fake_post(url: str, headers: dict, json: dict, timeout: int) -> DummyResponse:
nonlocal posted
posted = True
return DummyResponse(ok=True, json_data={"data": []})
monkeypatch.setattr("requests.post", fake_post)
with pytest.raises(ExternalProviderCapabilityError, match="15 images total"):
provider.generate(request)
assert posted is False