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

172 lines
7.0 KiB
Python

from __future__ import annotations
import requests
from invokeai.app.services.external_generation.errors import (
ExternalProviderCapabilityError,
ExternalProviderRateLimitError,
ExternalProviderRequestError,
)
from invokeai.app.services.external_generation.external_generation_base import ExternalProvider
from invokeai.app.services.external_generation.external_generation_common import (
ExternalGeneratedImage,
ExternalGenerationRequest,
ExternalGenerationResult,
)
from invokeai.app.services.external_generation.image_utils import decode_image_base64, encode_image_base64
_SEEDREAM_BATCH_PREFIXES = (
"seedream-5",
"seedream-4.5",
"seedream-4.0",
"seedream-4-5",
"seedream-4-0",
"seedream-5-0",
)
# Seedream batch endpoint accepts up to 15 total images counting both inputs (reference + init)
# and outputs combined. Hitting this only after the API call wastes a request and produces a
# confusing 400, so we enforce it locally for batch-capable models.
_SEEDREAM_BATCH_MAX_TOTAL_IMAGES = 15
class SeedreamProvider(ExternalProvider):
provider_id = "seedream"
def is_configured(self) -> bool:
return bool(self._app_config.external_seedream_api_key)
def generate(self, request: ExternalGenerationRequest) -> ExternalGenerationResult:
api_key = self._app_config.external_seedream_api_key
if not api_key:
raise ExternalProviderRequestError("Seedream API key is not configured")
base_url = (self._app_config.external_seedream_base_url or "https://ark.ap-southeast.bytepluses.com").rstrip(
"/"
)
endpoint = f"{base_url}/api/v3/images/generations"
headers = {"Authorization": f"Bearer {api_key}"}
model_id = request.model.provider_model_id
is_batch_model = any(model_id.startswith(prefix) for prefix in _SEEDREAM_BATCH_PREFIXES)
if is_batch_model:
input_image_count = len(request.reference_images) + (1 if request.init_image is not None else 0)
total_images = input_image_count + request.num_images
if total_images > _SEEDREAM_BATCH_MAX_TOTAL_IMAGES:
raise ExternalProviderCapabilityError(
f"{request.model.name} supports at most {_SEEDREAM_BATCH_MAX_TOTAL_IMAGES} images total "
f"(reference + init + output), got {total_images}"
)
opts = request.provider_options or {}
payload: dict[str, object] = {
"model": model_id,
"prompt": request.prompt,
"size": f"{request.width}x{request.height}",
"response_format": "b64_json",
"watermark": opts.get("watermark", False),
}
if opts.get("optimize_prompt"):
payload["optimize_prompt_options"] = {"optimize_prompt": True}
# Seed and guidance_scale are only supported on 3.0 models
if not is_batch_model and request.seed is not None and request.seed >= 0:
payload["seed"] = request.seed
if not is_batch_model and opts.get("guidance_scale") is not None:
payload["guidance_scale"] = opts["guidance_scale"]
# Batch generation for 4.x/5.x models
if is_batch_model:
if request.num_images > 1:
payload["sequential_image_generation"] = "auto"
payload["sequential_image_generation_options"] = {"max_images": request.num_images}
else:
payload["sequential_image_generation"] = "disabled"
# Image input: init_image for img2img, reference images for 4.x
images_b64: list[str] = []
if request.init_image is not None:
images_b64.append(f"data:image/png;base64,{encode_image_base64(request.init_image)}")
for reference in request.reference_images:
images_b64.append(f"data:image/png;base64,{encode_image_base64(reference.image)}")
if images_b64:
payload["image"] = images_b64 if len(images_b64) > 1 else images_b64[0]
response = requests.post(endpoint, headers=headers, json=payload, timeout=120)
if not response.ok:
if response.status_code == 429:
retry_after = _parse_retry_after(response.headers.get("retry-after"))
raise ExternalProviderRateLimitError(
f"Seedream rate limit exceeded. {f'Retry after {retry_after:.0f}s.' if retry_after else 'Please try again later.'}",
retry_after=retry_after,
)
raise ExternalProviderRequestError(
f"Seedream request failed with status {response.status_code}: {response.text}"
)
body = response.json()
if not isinstance(body, dict):
raise ExternalProviderRequestError("Seedream response payload was not a JSON object")
generated_images: list[ExternalGeneratedImage] = []
item_errors: list[dict[str, object]] = []
data_items = body.get("data")
if not isinstance(data_items, list):
raise ExternalProviderRequestError("Seedream response payload missing image data")
for item in data_items:
if not isinstance(item, dict):
continue
# Items may be error objects for failed images in batch — collect rather than discard
# so partial-failure causes (e.g., content filter) are visible to the caller.
if "error" in item:
error_payload = item["error"]
item_errors.append(
error_payload if isinstance(error_payload, dict) else {"message": str(error_payload)}
)
continue
encoded = item.get("b64_json")
if not encoded:
continue
image = decode_image_base64(encoded)
generated_images.append(ExternalGeneratedImage(image=image, seed=request.seed))
if not generated_images:
if item_errors:
first = item_errors[0]
message = first.get("message") if isinstance(first, dict) else None
raise ExternalProviderRequestError(
f"Seedream returned no images. Provider reported: {message or item_errors}"
)
raise ExternalProviderRequestError("Seedream response contained no images")
provider_metadata: dict[str, object] = {"model": model_id}
if item_errors:
provider_metadata["partial_failures"] = item_errors
self._logger.warning(
"Seedream returned %d image(s) with %d partial failure(s): %s",
len(generated_images),
len(item_errors),
item_errors,
)
return ExternalGenerationResult(
images=generated_images,
seed_used=request.seed,
provider_metadata=provider_metadata,
)
def _parse_retry_after(value: str | None) -> float | None:
if not value:
return None
try:
return float(value)
except ValueError:
return None