feat(image-gen): Kling Image v3 in the FAL catalog (t2i + singular-key i2i)
Adds fal-ai/kling-image/v3/text-to-image ($0.028/img, native 2K default, 8 aspect ratios) with its image-to-image edit endpoint. The i2i schema takes a SINGULAR `image_url` string instead of the usual `image_urls` list, so the catalog gains an `edit_image_param` knob that _build_fal_edit_payload honors (first source image only); the edit-contract test now validates whichever image key the entry declares.
This commit is contained in:
@@ -87,8 +87,11 @@ class TestFalCatalog:
|
||||
if "edit_endpoint" not in meta:
|
||||
continue
|
||||
assert meta.get("edit_supports"), f"{mid} has edit_endpoint but no edit_supports"
|
||||
assert "image_urls" in meta["edit_supports"], \
|
||||
f"{mid} edit_supports must allow image_urls"
|
||||
# Most edit endpoints take an `image_urls` list; entries with a
|
||||
# singular image key (Kling Image v3) declare edit_image_param.
|
||||
image_param = meta.get("edit_image_param") or "image_urls"
|
||||
assert image_param in meta["edit_supports"], \
|
||||
f"{mid} edit_supports must allow {image_param}"
|
||||
cap = meta.get("max_reference_images")
|
||||
assert isinstance(cap, int) and cap > 0, \
|
||||
f"{mid} needs a positive max_reference_images"
|
||||
|
||||
@@ -67,6 +67,34 @@ class TestFalEditPayload:
|
||||
# while nano-banana-pro is edit-capable
|
||||
assert FAL_MODELS["fal-ai/nano-banana-pro"].get("edit_endpoint")
|
||||
|
||||
def test_singular_edit_image_param_kling_image_v3(self):
|
||||
"""Kling Image v3's i2i endpoint takes a SINGULAR `image_url` string;
|
||||
the catalog opts in via edit_image_param and only the first source
|
||||
image is sent — no `image_urls` list may leak into the payload."""
|
||||
from tools.image_generation_tool import _build_fal_edit_payload
|
||||
|
||||
payload = _build_fal_edit_payload(
|
||||
"fal-ai/kling-image/v3/text-to-image", "make it winter",
|
||||
["https://x/a.png", "https://x/b.png"], "landscape",
|
||||
)
|
||||
assert payload["prompt"] == "make it winter"
|
||||
assert payload["image_url"] == "https://x/a.png"
|
||||
assert "image_urls" not in payload
|
||||
assert payload.get("aspect_ratio") == "16:9"
|
||||
assert payload.get("resolution") == "2K"
|
||||
|
||||
def test_kling_image_v3_text_payload(self):
|
||||
"""t2i payload: whitelist keeps prompt/AR/resolution, defaults 2K."""
|
||||
from tools import image_generation_tool as t
|
||||
|
||||
p = t._build_fal_payload(
|
||||
"fal-ai/kling-image/v3/text-to-image", "a lighthouse", "portrait",
|
||||
)
|
||||
assert p["prompt"] == "a lighthouse"
|
||||
assert p["aspect_ratio"] == "9:16"
|
||||
assert p["resolution"] == "2K"
|
||||
assert p["num_images"] == 1
|
||||
|
||||
|
||||
class TestMandatoryKeysSurviveWhitelist:
|
||||
"""A model whose whitelist forgets the mandatory keys must not produce a
|
||||
|
||||
@@ -19,9 +19,10 @@ def _model(
|
||||
display: str, speed: str, strengths: str, price: str, *, style: str = "image_size_preset",
|
||||
sizes: Optional[Dict[str, Any]] = None, defaults: Dict[str, Any], supports: set,
|
||||
edit_endpoint: Optional[str] = None, edit_supports: Optional[set] = None,
|
||||
max_reference_images: Optional[int] = None,
|
||||
max_reference_images: Optional[int] = None, edit_image_param: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Build one catalog entry; edit keys are present only for edit-capable models."""
|
||||
"""Build one catalog entry; edit keys are present only for edit-capable models. ``edit_image_param``
|
||||
names the source-image key when the edit endpoint takes a singular ``image_url`` instead of ``image_urls``."""
|
||||
entry: Dict[str, Any] = {
|
||||
"display": display, "speed": speed, "strengths": strengths, "price": price,
|
||||
"size_style": style, "sizes": sizes if sizes is not None else _DEFAULT_SIZES[style],
|
||||
@@ -31,6 +32,8 @@ def _model(
|
||||
entry["edit_endpoint"] = edit_endpoint
|
||||
entry["edit_supports"] = edit_supports
|
||||
entry["max_reference_images"] = max_reference_images
|
||||
if edit_image_param:
|
||||
entry["edit_image_param"] = edit_image_param
|
||||
return entry
|
||||
|
||||
|
||||
@@ -347,6 +350,21 @@ FAL_MODELS: Dict[str, Dict[str, Any]] = {
|
||||
},
|
||||
max_reference_images=3,
|
||||
),
|
||||
# 1K and 2K cost the same ($0.028/img) so 2K is the default. The i2i endpoint takes a SINGULAR
|
||||
# `image_url` (one reference image), unlike every other FAL edit endpoint's `image_urls` list.
|
||||
"fal-ai/kling-image/v3/text-to-image": _model(
|
||||
"Kling Image v3", "~10s", "Kuaishou. Realistic detail, cheap native 2K, wide AR set", "$0.028/image",
|
||||
style="aspect_ratio",
|
||||
defaults={"num_images": 1, "output_format": "png", "resolution": "2K"},
|
||||
supports={
|
||||
"prompt", "aspect_ratio", "num_images", "output_format", "resolution", "negative_prompt", "sync_mode",
|
||||
},
|
||||
edit_endpoint="fal-ai/kling-image/v3/image-to-image",
|
||||
edit_supports={
|
||||
"prompt", "image_url", "aspect_ratio", "num_images", "output_format", "resolution", "sync_mode",
|
||||
},
|
||||
max_reference_images=1, edit_image_param="image_url",
|
||||
),
|
||||
"meta/muse-image/text-to-image": _model(
|
||||
"Meta Muse Image", "~5s", "Meta. Realism + typography at commodity price", "$0.01/image",
|
||||
style="aspect_ratio",
|
||||
|
||||
@@ -212,7 +212,7 @@ def _build_payload(model_id, prompt, aspect_ratio, seed, overrides, image_urls=N
|
||||
spec + overrides, filtered to the model whitelist.
|
||||
|
||||
Edit endpoints mostly auto-infer size, so the size key is sent only when ``edit_supports``
|
||||
lists it. ``prompt`` (and ``image_urls`` on edits) survive a whitelist gap: every FAL
|
||||
lists it. ``prompt`` (and the source-image key on edits) survive a whitelist gap: every FAL
|
||||
endpoint requires them, so a catalog mistake can't send a broken request.
|
||||
"""
|
||||
meta = FAL_MODELS[model_id]
|
||||
@@ -225,9 +225,10 @@ def _build_payload(model_id, prompt, aspect_ratio, seed, overrides, image_urls=N
|
||||
payload: Dict[str, Any] = dict(meta.get("defaults", {}))
|
||||
payload["prompt"] = (prompt or "").strip()
|
||||
required = {"prompt"}
|
||||
if edit:
|
||||
payload["image_urls"] = list(image_urls)
|
||||
required.add("image_urls")
|
||||
if edit: # a few edit endpoints (Kling Image v3) take a singular `image_url` string instead of the list
|
||||
image_param = meta.get("edit_image_param") or "image_urls"
|
||||
payload[image_param] = list(image_urls)[0] if image_param != "image_urls" else list(image_urls)
|
||||
required.add(image_param)
|
||||
size_key = _SIZE_KEY_BY_STYLE.get(meta["size_style"])
|
||||
if size_key is None and not edit:
|
||||
raise ValueError(f"Unknown size_style: {meta['size_style']!r}")
|
||||
|
||||
Reference in New Issue
Block a user