feat(media): default-on upscaling for sub-2MP image models (FAL + Krea)
Per review: upscaling should be the default behavior (like the original flux-2-pro chain), not agent opt-in. Policy: every image model whose native output is below ~2MP now sets upscale=True in its catalog — users never silently get low-res images. Native hi-res models (Seedream 5 Pro/Lite, Krea 2 Large) stay off to avoid paying to upscale already-large output. - FAL catalog: 16 models flipped to upscale=True (klein, z-image, nano-banana pro/2/2-lite, gpt-image 1.5/2, ideogram v3/v4, recraft v4/v4.1, qwen image/3, krea-2 medium on FAL, MAI 2.5 pro). - Krea plugin: per-model upscale defaults (medium + medium-turbo ON at 1.5K native; large OFF at 2K native), precedence explicit kwarg > image_gen.krea.upscale config > catalog default. - The 'upscale' tool param remains as a per-call override in both directions (false = fast draft, true = force on hi-res/edits). - Video unchanged: opt-in only (default-on would double every video's cost and latency). - Sibling tests updated: routing/payload tests pass upscale=False where the assertion targets the generation submit; catalog test now pins the native-resolution policy instead of the flux-2-pro snapshot.
This commit is contained in:
@@ -57,6 +57,9 @@ _MODELS: Dict[str, Dict[str, Any]] = {
|
|||||||
"strengths": "Illustration, anime, painting, expressive styles. Faster + cheaper.",
|
"strengths": "Illustration, anime, painting, expressive styles. Faster + cheaper.",
|
||||||
"price": "$0.030 (text) / $0.035 (style refs) / $0.040 (moodboards)",
|
"price": "$0.030 (text) / $0.035 (style refs) / $0.040 (moodboards)",
|
||||||
"path": "medium",
|
"path": "medium",
|
||||||
|
# 1.5K native — default the Enhance pass on (mirrors the FAL
|
||||||
|
# catalog policy: sub-2MP models upscale by default).
|
||||||
|
"upscale": True,
|
||||||
},
|
},
|
||||||
"krea-2-large": {
|
"krea-2-large": {
|
||||||
"display": "Krea 2 Large",
|
"display": "Krea 2 Large",
|
||||||
@@ -64,6 +67,8 @@ _MODELS: Dict[str, Dict[str, Any]] = {
|
|||||||
"strengths": "Photorealism, raw textured looks (motion blur, grain), expressive styles.",
|
"strengths": "Photorealism, raw textured looks (motion blur, grain), expressive styles.",
|
||||||
"price": "$0.060 (text) / $0.065 (style refs) / $0.070 (moodboards)",
|
"price": "$0.060 (text) / $0.065 (style refs) / $0.070 (moodboards)",
|
||||||
"path": "large",
|
"path": "large",
|
||||||
|
# 2K native — high-res enough out of the box.
|
||||||
|
"upscale": False,
|
||||||
},
|
},
|
||||||
"krea-2-medium-turbo": {
|
"krea-2-medium-turbo": {
|
||||||
"display": "Krea 2 Medium Turbo",
|
"display": "Krea 2 Medium Turbo",
|
||||||
@@ -71,6 +76,8 @@ _MODELS: Dict[str, Dict[str, Any]] = {
|
|||||||
"strengths": "Fastest Krea 2 — medium quality at lower latency / cost.",
|
"strengths": "Fastest Krea 2 — medium quality at lower latency / cost.",
|
||||||
"price": "$0.015 (text) / $0.0175 (style refs)",
|
"price": "$0.015 (text) / $0.0175 (style refs)",
|
||||||
"path": "medium-turbo",
|
"path": "medium-turbo",
|
||||||
|
# 1.5K native — default the Enhance pass on.
|
||||||
|
"upscale": True,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -821,17 +828,20 @@ class KreaImageGenProvider(ImageGenProvider):
|
|||||||
aspect_ratio=aspect,
|
aspect_ratio=aspect,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Optional high-resolution pass (Krea Enhance). Explicit agent/user
|
# High-resolution pass (Krea Enhance). Precedence: explicit kwarg >
|
||||||
# opt-in via the ``upscale`` kwarg; config default via
|
# ``image_gen.krea.upscale`` config > per-model catalog default
|
||||||
# ``image_gen.krea.upscale``. Best-effort: failure falls back to the
|
# (1.5K-native tiers default on; 2K-native Large stays off). Best-
|
||||||
# original image rather than failing the generation.
|
# effort: failure falls back to the original image rather than
|
||||||
|
# failing the generation.
|
||||||
upscaled = False
|
upscaled = False
|
||||||
upscale_requested = kwargs.get("upscale")
|
upscale_requested = kwargs.get("upscale")
|
||||||
if not isinstance(upscale_requested, bool):
|
if not isinstance(upscale_requested, bool):
|
||||||
cfg_krea = _load_krea_config().get("krea")
|
cfg_krea = _load_krea_config().get("krea")
|
||||||
upscale_requested = bool(
|
cfg_upscale = cfg_krea.get("upscale") if isinstance(cfg_krea, dict) else None
|
||||||
isinstance(cfg_krea, dict) and cfg_krea.get("upscale") is True
|
if isinstance(cfg_upscale, bool):
|
||||||
)
|
upscale_requested = cfg_upscale
|
||||||
|
else:
|
||||||
|
upscale_requested = bool(meta.get("upscale", False))
|
||||||
if upscale_requested:
|
if upscale_requested:
|
||||||
enhanced_url = _enhance_image(
|
enhanced_url = _enhance_image(
|
||||||
base_url,
|
base_url,
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ class TestGenerate:
|
|||||||
return_value=Path("/tmp/krea_krea-2-medium_test.png"),
|
return_value=Path("/tmp/krea_krea-2-medium_test.png"),
|
||||||
) as mock_save, \
|
) as mock_save, \
|
||||||
patch("plugins.image_gen.krea.time.sleep"): # skip real waits
|
patch("plugins.image_gen.krea.time.sleep"): # skip real waits
|
||||||
result = KreaImageGenProvider().generate(prompt="A cinematic lamp")
|
result = KreaImageGenProvider().generate(prompt="A cinematic lamp", upscale=False)
|
||||||
|
|
||||||
assert result["success"] is True
|
assert result["success"] is True
|
||||||
assert result["image"] == "/tmp/krea_krea-2-medium_test.png"
|
assert result["image"] == "/tmp/krea_krea-2-medium_test.png"
|
||||||
@@ -215,7 +215,7 @@ class TestGenerate:
|
|||||||
return_value=Path("/tmp/x.png"),
|
return_value=Path("/tmp/x.png"),
|
||||||
), \
|
), \
|
||||||
patch("plugins.image_gen.krea.time.sleep"):
|
patch("plugins.image_gen.krea.time.sleep"):
|
||||||
KreaImageGenProvider().generate(prompt="test", aspect_ratio="square")
|
KreaImageGenProvider().generate(prompt="test", aspect_ratio="square", upscale=False)
|
||||||
|
|
||||||
payload = mock_post.call_args.kwargs["json"]
|
payload = mock_post.call_args.kwargs["json"]
|
||||||
assert payload["aspect_ratio"] == "1:1"
|
assert payload["aspect_ratio"] == "1:1"
|
||||||
@@ -260,6 +260,7 @@ class TestGenerate:
|
|||||||
moodboards=[{"url": "https://x.com/mood.png"}, {"url": "https://x.com/mood2.png"}],
|
moodboards=[{"url": "https://x.com/mood.png"}, {"url": "https://x.com/mood2.png"}],
|
||||||
image_style_references=[{"url": f"https://x.com/{i}.png"} for i in range(15)],
|
image_style_references=[{"url": f"https://x.com/{i}.png"} for i in range(15)],
|
||||||
creativity="high",
|
creativity="high",
|
||||||
|
upscale=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
payload = mock_post.call_args.kwargs["json"]
|
payload = mock_post.call_args.kwargs["json"]
|
||||||
@@ -290,6 +291,7 @@ class TestGenerate:
|
|||||||
"https://x.com/a.png",
|
"https://x.com/a.png",
|
||||||
{"url": "https://x.com/b.png", "strength": 1.2},
|
{"url": "https://x.com/b.png", "strength": 1.2},
|
||||||
],
|
],
|
||||||
|
upscale=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
payload = mock_post.call_args.kwargs["json"]
|
payload = mock_post.call_args.kwargs["json"]
|
||||||
@@ -513,7 +515,7 @@ class TestManagedGateway:
|
|||||||
return_value=Path("/tmp/x.png"),
|
return_value=Path("/tmp/x.png"),
|
||||||
), \
|
), \
|
||||||
patch("plugins.image_gen.krea.time.sleep"):
|
patch("plugins.image_gen.krea.time.sleep"):
|
||||||
result = KreaImageGenProvider().generate(prompt="A managed lamp")
|
result = KreaImageGenProvider().generate(prompt="A managed lamp", upscale=False)
|
||||||
|
|
||||||
assert result["success"] is True
|
assert result["success"] is True
|
||||||
post_url = mock_post.call_args[0][0]
|
post_url = mock_post.call_args[0][0]
|
||||||
@@ -573,7 +575,7 @@ class TestExplicitModelOverride:
|
|||||||
return_value=Path("/tmp/x.png"),
|
return_value=Path("/tmp/x.png"),
|
||||||
), \
|
), \
|
||||||
patch("plugins.image_gen.krea.time.sleep"):
|
patch("plugins.image_gen.krea.time.sleep"):
|
||||||
result = KreaImageGenProvider().generate(prompt="test", model="krea-2-medium-turbo")
|
result = KreaImageGenProvider().generate(prompt="test", model="krea-2-medium-turbo", upscale=False)
|
||||||
|
|
||||||
assert result["success"] is True
|
assert result["success"] is True
|
||||||
assert result["model"] == "krea-2-medium-turbo"
|
assert result["model"] == "krea-2-medium-turbo"
|
||||||
@@ -587,7 +589,7 @@ class TestExplicitModelOverride:
|
|||||||
|
|
||||||
|
|
||||||
class TestUpscalePass:
|
class TestUpscalePass:
|
||||||
def _run_generate(self, *, upscale, enhance_job):
|
def _run_generate(self, *, upscale, enhance_job, model=None):
|
||||||
"""Drive generate() with sequenced post/get mocks.
|
"""Drive generate() with sequenced post/get mocks.
|
||||||
|
|
||||||
Sequence: generation submit POST → generation poll GET; then (when
|
Sequence: generation submit POST → generation poll GET; then (when
|
||||||
@@ -603,6 +605,10 @@ class TestUpscalePass:
|
|||||||
posts = [gen_submit, enh_submit]
|
posts = [gen_submit, enh_submit]
|
||||||
gets = [gen_poll] + ([enh_poll] if enh_poll else [])
|
gets = [gen_poll] + ([enh_poll] if enh_poll else [])
|
||||||
|
|
||||||
|
kwargs = {"prompt": "a lamp", "upscale": upscale}
|
||||||
|
if model is not None:
|
||||||
|
kwargs["model"] = model
|
||||||
|
|
||||||
with patch("plugins.image_gen.krea.requests.post", side_effect=posts) as mock_post, \
|
with patch("plugins.image_gen.krea.requests.post", side_effect=posts) as mock_post, \
|
||||||
patch("plugins.image_gen.krea.requests.get", side_effect=gets) as mock_get, \
|
patch("plugins.image_gen.krea.requests.get", side_effect=gets) as mock_get, \
|
||||||
patch(
|
patch(
|
||||||
@@ -610,7 +616,7 @@ class TestUpscalePass:
|
|||||||
side_effect=lambda url, prefix: Path(f"/tmp/{url.rsplit('/', 1)[-1]}"),
|
side_effect=lambda url, prefix: Path(f"/tmp/{url.rsplit('/', 1)[-1]}"),
|
||||||
), \
|
), \
|
||||||
patch("plugins.image_gen.krea.time.sleep"):
|
patch("plugins.image_gen.krea.time.sleep"):
|
||||||
result = KreaImageGenProvider().generate(prompt="a lamp", upscale=upscale)
|
result = KreaImageGenProvider().generate(**kwargs)
|
||||||
return result, mock_post, mock_get
|
return result, mock_post, mock_get
|
||||||
|
|
||||||
def test_upscale_routes_through_enhance_endpoint(self):
|
def test_upscale_routes_through_enhance_endpoint(self):
|
||||||
@@ -651,14 +657,42 @@ class TestUpscalePass:
|
|||||||
assert result["image"].endswith("native.png")
|
assert result["image"].endswith("native.png")
|
||||||
assert mock_post.call_count == 2 # enhance attempted, fell back
|
assert mock_post.call_count == 2 # enhance attempted, fell back
|
||||||
|
|
||||||
def test_no_upscale_by_default(self):
|
def test_medium_upscales_by_default(self):
|
||||||
result, mock_post, _ = self._run_generate(upscale=None, enhance_job=None)
|
"""krea-2-medium is 1.5K native — the Enhance pass defaults ON."""
|
||||||
|
enhance_job = {
|
||||||
|
"job_id": "00000000-0000-0000-0000-00000000e0e0",
|
||||||
|
"status": "completed",
|
||||||
|
"created_at": "2026-05-27T00:00:00Z",
|
||||||
|
"completed_at": "2026-05-27T00:01:00Z",
|
||||||
|
"result": {"urls": ["https://krea.cdn/enhanced.png"]},
|
||||||
|
}
|
||||||
|
result, mock_post, _ = self._run_generate(upscale=None, enhance_job=enhance_job)
|
||||||
|
|
||||||
|
assert result["success"] is True
|
||||||
|
assert result["upscaled"] is True
|
||||||
|
assert result["image"].endswith("enhanced.png")
|
||||||
|
assert mock_post.call_count == 2
|
||||||
|
|
||||||
|
def test_large_skips_upscale_by_default(self):
|
||||||
|
"""krea-2-large is 2K native — no automatic Enhance pass."""
|
||||||
|
result, mock_post, _ = self._run_generate(
|
||||||
|
upscale=None, enhance_job=None, model="krea-2-large",
|
||||||
|
)
|
||||||
|
|
||||||
assert result["success"] is True
|
assert result["success"] is True
|
||||||
assert result["upscaled"] is False
|
assert result["upscaled"] is False
|
||||||
assert result["image"].endswith("native.png")
|
assert result["image"].endswith("native.png")
|
||||||
assert mock_post.call_count == 1 # only the generation submit
|
assert mock_post.call_count == 1 # only the generation submit
|
||||||
|
|
||||||
|
def test_explicit_false_disables_default(self):
|
||||||
|
"""Explicit upscale=False wins over medium's default-on."""
|
||||||
|
result, mock_post, _ = self._run_generate(upscale=False, enhance_job=None)
|
||||||
|
|
||||||
|
assert result["success"] is True
|
||||||
|
assert result["upscaled"] is False
|
||||||
|
assert result["image"].endswith("native.png")
|
||||||
|
assert mock_post.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Registration
|
# Registration
|
||||||
|
|||||||
@@ -57,17 +57,23 @@ class TestFalCatalog:
|
|||||||
assert not missing, f"{mid} missing required keys: {missing}"
|
assert not missing, f"{mid} missing required keys: {missing}"
|
||||||
|
|
||||||
|
|
||||||
def test_only_flux2_pro_upscales_by_default(self, image_tool):
|
def test_upscale_defaults_track_native_resolution(self, image_tool):
|
||||||
"""Upscaling should default to False for all new models to preserve
|
"""Default-on upscaling: every model whose native output is below
|
||||||
the <1s / fast-render value prop. Only flux-2-pro stays True for
|
~2MP upscales by default so users never silently get low-res images.
|
||||||
backward-compat with the previous default."""
|
Models that already emit >=2MP natively (Seedream tiers, Krea 2
|
||||||
|
Large on FAL) skip the pass — upscaling them wastes money."""
|
||||||
|
native_hi_res = {
|
||||||
|
"bytedance/seedream/v5/pro/text-to-image", # 1536²-2048² native
|
||||||
|
"bytedance/seedream/v5/lite/text-to-image", # up to 4K native
|
||||||
|
"fal-ai/krea/v2/large/text-to-image", # 2K native
|
||||||
|
}
|
||||||
for mid, meta in image_tool.FAL_MODELS.items():
|
for mid, meta in image_tool.FAL_MODELS.items():
|
||||||
if mid == "fal-ai/flux-2-pro":
|
if mid in native_hi_res:
|
||||||
assert meta["upscale"] is True, \
|
|
||||||
"flux-2-pro should keep upscale=True for backward-compat"
|
|
||||||
else:
|
|
||||||
assert meta["upscale"] is False, \
|
assert meta["upscale"] is False, \
|
||||||
f"{mid} should default to upscale=False"
|
f"{mid} is native hi-res — should not double-upscale"
|
||||||
|
else:
|
||||||
|
assert meta["upscale"] is True, \
|
||||||
|
f"{mid} should default to upscale=True (sub-2MP native)"
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -494,19 +500,22 @@ class TestUpscaleOptIn:
|
|||||||
expected_url = "https://fal/upscaled.png" if upscaler_called else "https://fal/native.png"
|
expected_url = "https://fal/upscaled.png" if upscaler_called else "https://fal/native.png"
|
||||||
assert out["image"] == expected_url
|
assert out["image"] == expected_url
|
||||||
|
|
||||||
def test_explicit_true_upscales_non_default_model(self, image_tool, monkeypatch):
|
def test_explicit_true_upscales_native_hi_res_model(self, image_tool, monkeypatch):
|
||||||
"""Klein has upscale=False in the catalog — explicit True wins."""
|
"""Seedream Lite has upscale=False in the catalog (native 4K) —
|
||||||
|
explicit True still wins."""
|
||||||
self._run(image_tool, monkeypatch,
|
self._run(image_tool, monkeypatch,
|
||||||
model="fal-ai/flux-2/klein/9b", upscale=True, upscaler_called=True)
|
model="bytedance/seedream/v5/lite/text-to-image",
|
||||||
|
upscale=True, upscaler_called=True)
|
||||||
|
|
||||||
def test_explicit_false_disables_flux2_pro_default(self, image_tool, monkeypatch):
|
def test_explicit_false_disables_default_on_model(self, image_tool, monkeypatch):
|
||||||
"""flux-2-pro defaults to upscale=True — explicit False wins."""
|
"""Klein defaults to upscale=True (sub-2MP native) — explicit False wins."""
|
||||||
self._run(image_tool, monkeypatch,
|
self._run(image_tool, monkeypatch,
|
||||||
model="fal-ai/flux-2-pro", upscale=False, upscaler_called=False)
|
model="fal-ai/flux-2/klein/9b", upscale=False, upscaler_called=False)
|
||||||
|
|
||||||
def test_omitted_keeps_catalog_default_off(self, image_tool, monkeypatch):
|
def test_omitted_keeps_catalog_default_off(self, image_tool, monkeypatch):
|
||||||
self._run(image_tool, monkeypatch,
|
self._run(image_tool, monkeypatch,
|
||||||
model="fal-ai/flux-2/klein/9b", upscale=None, upscaler_called=False)
|
model="bytedance/seedream/v5/lite/text-to-image",
|
||||||
|
upscale=None, upscaler_called=False)
|
||||||
|
|
||||||
def test_omitted_keeps_catalog_default_on(self, image_tool, monkeypatch):
|
def test_omitted_keeps_catalog_default_on(self, image_tool, monkeypatch):
|
||||||
self._run(image_tool, monkeypatch,
|
self._run(image_tool, monkeypatch,
|
||||||
|
|||||||
@@ -124,7 +124,11 @@ class TestFalRouting:
|
|||||||
capture: dict = {}
|
capture: dict = {}
|
||||||
self._patch_submit(monkeypatch, image_tool, capture)
|
self._patch_submit(monkeypatch, image_tool, capture)
|
||||||
|
|
||||||
raw = image_tool.image_generate_tool(prompt="a cat", aspect_ratio="square")
|
# Routing test — disable the (default-on) upscale pass so the captured
|
||||||
|
# endpoint is the generation submit, not the upscaler.
|
||||||
|
raw = image_tool.image_generate_tool(
|
||||||
|
prompt="a cat", aspect_ratio="square", upscale=False,
|
||||||
|
)
|
||||||
out = json.loads(raw)
|
out = json.loads(raw)
|
||||||
assert out["success"] is True
|
assert out["success"] is True
|
||||||
assert out["modality"] == "text"
|
assert out["modality"] == "text"
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ FAL_MODELS: Dict[str, Dict[str, Any]] = {
|
|||||||
"prompt", "image_size", "num_inference_steps", "seed",
|
"prompt", "image_size", "num_inference_steps", "seed",
|
||||||
"output_format", "enable_safety_checker",
|
"output_format", "enable_safety_checker",
|
||||||
},
|
},
|
||||||
"upscale": False,
|
"upscale": True,
|
||||||
# Image-to-image / editing: FLUX.2 [klein] 9B edit endpoint takes
|
# Image-to-image / editing: FLUX.2 [klein] 9B edit endpoint takes
|
||||||
# `image_urls` (list). Natural-language edits, multi-ref.
|
# `image_urls` (list). Natural-language edits, multi-ref.
|
||||||
"edit_endpoint": "fal-ai/flux-2/klein/9b/edit",
|
"edit_endpoint": "fal-ai/flux-2/klein/9b/edit",
|
||||||
@@ -183,7 +183,7 @@ FAL_MODELS: Dict[str, Dict[str, Any]] = {
|
|||||||
"seed", "output_format", "enable_safety_checker",
|
"seed", "output_format", "enable_safety_checker",
|
||||||
"enable_prompt_expansion",
|
"enable_prompt_expansion",
|
||||||
},
|
},
|
||||||
"upscale": False,
|
"upscale": True,
|
||||||
},
|
},
|
||||||
"fal-ai/nano-banana-pro": {
|
"fal-ai/nano-banana-pro": {
|
||||||
"display": "Nano Banana Pro (Gemini 3 Pro Image)",
|
"display": "Nano Banana Pro (Gemini 3 Pro Image)",
|
||||||
@@ -209,7 +209,7 @@ FAL_MODELS: Dict[str, Dict[str, Any]] = {
|
|||||||
"safety_tolerance", "seed", "sync_mode", "resolution",
|
"safety_tolerance", "seed", "sync_mode", "resolution",
|
||||||
"enable_web_search", "limit_generations",
|
"enable_web_search", "limit_generations",
|
||||||
},
|
},
|
||||||
"upscale": False,
|
"upscale": True,
|
||||||
# Nano Banana Pro edit (Gemini 3 Pro Image): natural-language edits
|
# Nano Banana Pro edit (Gemini 3 Pro Image): natural-language edits
|
||||||
# with up to 2 reference images via `image_urls`.
|
# with up to 2 reference images via `image_urls`.
|
||||||
"edit_endpoint": "fal-ai/nano-banana-pro/edit",
|
"edit_endpoint": "fal-ai/nano-banana-pro/edit",
|
||||||
@@ -244,7 +244,7 @@ FAL_MODELS: Dict[str, Dict[str, Any]] = {
|
|||||||
"resolution", "enable_web_search", "limit_generations",
|
"resolution", "enable_web_search", "limit_generations",
|
||||||
"thinking_level",
|
"thinking_level",
|
||||||
},
|
},
|
||||||
"upscale": False,
|
"upscale": True,
|
||||||
"edit_endpoint": "fal-ai/nano-banana-2/edit",
|
"edit_endpoint": "fal-ai/nano-banana-2/edit",
|
||||||
"edit_supports": {
|
"edit_supports": {
|
||||||
"prompt", "image_urls", "aspect_ratio", "num_images",
|
"prompt", "image_urls", "aspect_ratio", "num_images",
|
||||||
@@ -276,7 +276,7 @@ FAL_MODELS: Dict[str, Dict[str, Any]] = {
|
|||||||
"prompt", "image_size", "quality", "num_images", "output_format",
|
"prompt", "image_size", "quality", "num_images", "output_format",
|
||||||
"background", "sync_mode",
|
"background", "sync_mode",
|
||||||
},
|
},
|
||||||
"upscale": False,
|
"upscale": True,
|
||||||
# Edit endpoint: high-fidelity edits preserving composition/lighting.
|
# Edit endpoint: high-fidelity edits preserving composition/lighting.
|
||||||
"edit_endpoint": "fal-ai/gpt-image-1.5/edit",
|
"edit_endpoint": "fal-ai/gpt-image-1.5/edit",
|
||||||
"edit_supports": {
|
"edit_supports": {
|
||||||
@@ -315,7 +315,7 @@ FAL_MODELS: Dict[str, Dict[str, Any]] = {
|
|||||||
# openai_api_key (BYOK) intentionally omitted — all users go
|
# openai_api_key (BYOK) intentionally omitted — all users go
|
||||||
# through the shared FAL billing path.
|
# through the shared FAL billing path.
|
||||||
},
|
},
|
||||||
"upscale": False,
|
"upscale": True,
|
||||||
# GPT Image 2 edit endpoint lives under the OpenAI namespace on FAL
|
# GPT Image 2 edit endpoint lives under the OpenAI namespace on FAL
|
||||||
# (NOT fal-ai/). Takes `image_urls` (list) + optional mask. We don't
|
# (NOT fal-ai/). Takes `image_urls` (list) + optional mask. We don't
|
||||||
# send `image_size` on edit so the model auto-infers from input.
|
# send `image_size` on edit so the model auto-infers from input.
|
||||||
@@ -346,7 +346,7 @@ FAL_MODELS: Dict[str, Dict[str, Any]] = {
|
|||||||
"prompt", "image_size", "rendering_speed", "expand_prompt",
|
"prompt", "image_size", "rendering_speed", "expand_prompt",
|
||||||
"style", "seed",
|
"style", "seed",
|
||||||
},
|
},
|
||||||
"upscale": False,
|
"upscale": True,
|
||||||
# Ideogram V3 edit endpoint takes `image_urls` (list).
|
# Ideogram V3 edit endpoint takes `image_urls` (list).
|
||||||
"edit_endpoint": "fal-ai/ideogram/v3/edit",
|
"edit_endpoint": "fal-ai/ideogram/v3/edit",
|
||||||
"edit_supports": {
|
"edit_supports": {
|
||||||
@@ -374,7 +374,7 @@ FAL_MODELS: Dict[str, Dict[str, Any]] = {
|
|||||||
"prompt", "image_size", "enable_safety_checker",
|
"prompt", "image_size", "enable_safety_checker",
|
||||||
"colors", "background_color",
|
"colors", "background_color",
|
||||||
},
|
},
|
||||||
"upscale": False,
|
"upscale": True,
|
||||||
},
|
},
|
||||||
"fal-ai/qwen-image": {
|
"fal-ai/qwen-image": {
|
||||||
"display": "Qwen Image",
|
"display": "Qwen Image",
|
||||||
@@ -398,7 +398,7 @@ FAL_MODELS: Dict[str, Dict[str, Any]] = {
|
|||||||
"prompt", "image_size", "num_inference_steps", "guidance_scale",
|
"prompt", "image_size", "num_inference_steps", "guidance_scale",
|
||||||
"num_images", "output_format", "acceleration", "seed", "sync_mode",
|
"num_images", "output_format", "acceleration", "seed", "sync_mode",
|
||||||
},
|
},
|
||||||
"upscale": False,
|
"upscale": True,
|
||||||
# Qwen edit uses the Qwen Image 2.0 Pro editing endpoint, which takes
|
# Qwen edit uses the Qwen Image 2.0 Pro editing endpoint, which takes
|
||||||
# `image_urls` (list) + natural-language edit instructions.
|
# `image_urls` (list) + natural-language edit instructions.
|
||||||
"edit_endpoint": "fal-ai/qwen-image-2/pro/edit",
|
"edit_endpoint": "fal-ai/qwen-image-2/pro/edit",
|
||||||
@@ -429,7 +429,7 @@ FAL_MODELS: Dict[str, Dict[str, Any]] = {
|
|||||||
"prompt", "aspect_ratio", "creativity", "seed",
|
"prompt", "aspect_ratio", "creativity", "seed",
|
||||||
"image_style_references",
|
"image_style_references",
|
||||||
},
|
},
|
||||||
"upscale": False,
|
"upscale": True,
|
||||||
},
|
},
|
||||||
"fal-ai/krea/v2/large/text-to-image": {
|
"fal-ai/krea/v2/large/text-to-image": {
|
||||||
"display": "Krea 2 Large",
|
"display": "Krea 2 Large",
|
||||||
@@ -524,7 +524,7 @@ FAL_MODELS: Dict[str, Dict[str, Any]] = {
|
|||||||
"prompt", "image_size", "expansion_model", "num_images",
|
"prompt", "image_size", "expansion_model", "num_images",
|
||||||
"seed", "sync_mode", "enable_safety_checker", "output_format",
|
"seed", "sync_mode", "enable_safety_checker", "output_format",
|
||||||
},
|
},
|
||||||
"upscale": False,
|
"upscale": True,
|
||||||
},
|
},
|
||||||
"ideogram/v4/fast": {
|
"ideogram/v4/fast": {
|
||||||
"display": "Ideogram V4 (Fast)",
|
"display": "Ideogram V4 (Fast)",
|
||||||
@@ -545,7 +545,7 @@ FAL_MODELS: Dict[str, Dict[str, Any]] = {
|
|||||||
"prompt", "image_size", "expansion_model", "rendering_speed",
|
"prompt", "image_size", "expansion_model", "rendering_speed",
|
||||||
"num_images", "seed", "sync_mode",
|
"num_images", "seed", "sync_mode",
|
||||||
},
|
},
|
||||||
"upscale": False,
|
"upscale": True,
|
||||||
},
|
},
|
||||||
"alibaba/qwen-image-3/text-to-image": {
|
"alibaba/qwen-image-3/text-to-image": {
|
||||||
"display": "Qwen Image 3",
|
"display": "Qwen Image 3",
|
||||||
@@ -569,7 +569,7 @@ FAL_MODELS: Dict[str, Dict[str, Any]] = {
|
|||||||
"seed", "sync_mode", "output_format",
|
"seed", "sync_mode", "output_format",
|
||||||
"enable_prompt_expansion", "enable_safety_checker",
|
"enable_prompt_expansion", "enable_safety_checker",
|
||||||
},
|
},
|
||||||
"upscale": False,
|
"upscale": True,
|
||||||
# Qwen Image 3 edit: 1-3 reference images, identity-preserving edits.
|
# Qwen Image 3 edit: 1-3 reference images, identity-preserving edits.
|
||||||
"edit_endpoint": "alibaba/qwen-image-3/edit",
|
"edit_endpoint": "alibaba/qwen-image-3/edit",
|
||||||
"edit_supports": {
|
"edit_supports": {
|
||||||
@@ -598,7 +598,7 @@ FAL_MODELS: Dict[str, Dict[str, Any]] = {
|
|||||||
"prompt", "aspect_ratio", "num_images", "output_format",
|
"prompt", "aspect_ratio", "num_images", "output_format",
|
||||||
"sync_mode",
|
"sync_mode",
|
||||||
},
|
},
|
||||||
"upscale": False,
|
"upscale": True,
|
||||||
},
|
},
|
||||||
"google/nano-banana-2-lite": {
|
"google/nano-banana-2-lite": {
|
||||||
"display": "Nano Banana 2 Lite",
|
"display": "Nano Banana 2 Lite",
|
||||||
@@ -621,7 +621,7 @@ FAL_MODELS: Dict[str, Dict[str, Any]] = {
|
|||||||
"output_format", "safety_tolerance", "sync_mode",
|
"output_format", "safety_tolerance", "sync_mode",
|
||||||
"system_prompt", "limit_generations", "thinking_level",
|
"system_prompt", "limit_generations", "thinking_level",
|
||||||
},
|
},
|
||||||
"upscale": False,
|
"upscale": True,
|
||||||
# Fast multi-turn local edits with reference images via `image_urls`.
|
# Fast multi-turn local edits with reference images via `image_urls`.
|
||||||
"edit_endpoint": "google/nano-banana-2-lite/edit",
|
"edit_endpoint": "google/nano-banana-2-lite/edit",
|
||||||
"edit_supports": {
|
"edit_supports": {
|
||||||
@@ -649,7 +649,7 @@ FAL_MODELS: Dict[str, Dict[str, Any]] = {
|
|||||||
"prompt", "image_size", "enable_safety_checker",
|
"prompt", "image_size", "enable_safety_checker",
|
||||||
"colors", "background_color",
|
"colors", "background_color",
|
||||||
},
|
},
|
||||||
"upscale": False,
|
"upscale": True,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1459,11 +1459,12 @@ IMAGE_GENERATE_SCHEMA = {
|
|||||||
"upscale": {
|
"upscale": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": (
|
"description": (
|
||||||
"Optional high-resolution pass: when true, the generated "
|
"Optional override for the high-resolution pass. Models "
|
||||||
"image is run through the active backend's upscaler/"
|
"with sub-2MP native output upscale automatically (~2x, "
|
||||||
"enhancer (extra cost and latency, roughly 2x resolution). "
|
"extra cost/latency); pass false for a faster/cheaper "
|
||||||
"Use when the user asks for high-res / print / wallpaper "
|
"draft at native resolution, or true to force the pass "
|
||||||
"quality output. Omit for the model's native resolution."
|
"on native hi-res models and image edits. Omit to keep "
|
||||||
|
"the per-model default."
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -160,37 +160,33 @@ This translation happens in `_build_fal_payload()` — agent code never has to k
|
|||||||
|
|
||||||
## Upscaling
|
## Upscaling
|
||||||
|
|
||||||
### On-demand (any model)
|
### Automatic (default-on for low-res models)
|
||||||
|
|
||||||
The agent-facing `upscale` parameter requests a high-resolution pass after
|
Every model whose native output is below ~2MP automatically runs a
|
||||||
generation on **any** model — ask for "high-res", "print quality", or
|
high-resolution pass after generation, so you never silently get a low-res
|
||||||
"wallpaper" output and the agent sets `upscale: true`:
|
image:
|
||||||
|
|
||||||
| Backend | Upscaler | Result |
|
| Backend | Models upscaled by default | Upscaler |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| **FAL.ai** (all models) | Clarity Upscaler | ~2× resolution, +$0.03/MP |
|
| **FAL.ai** | all except Seedream 5 Pro/Lite and Krea 2 Large (native ≥2MP) | Clarity Upscaler (2×, +$0.03/MP) |
|
||||||
| **Krea** (Krea 2 family) | Krea Enhance | 2× resolution (up to 8K ceiling) |
|
| **Krea** | Krea 2 Medium + Medium Turbo (1.5K native); Large (2K) skips | Krea Enhance (2×, up to 8K ceiling) |
|
||||||
| Other backends | — | parameter is ignored (native resolution returned) |
|
| Other backends | — | no upscaler; native resolution returned |
|
||||||
|
|
||||||
An explicit `upscale: false` also *disables* the automatic pass on models
|
### The `upscale` parameter (per-call override)
|
||||||
that default to it (currently `flux-2-pro`). Passing `upscale: true` with an
|
|
||||||
image edit runs the pass on the edited output too.
|
|
||||||
|
|
||||||
`video_generate` accepts the same `upscale` parameter on the FAL backend,
|
The agent-facing `upscale` boolean overrides the default in either
|
||||||
chaining ByteDance's **SeedVR2** video upscaler (2×, $0.001/MP of output
|
direction:
|
||||||
video) after generation.
|
|
||||||
|
|
||||||
### Automatic (per-model default)
|
- `upscale: false` — skip the automatic pass (faster/cheaper draft output)
|
||||||
|
- `upscale: true` — force the pass, even on native hi-res models or image
|
||||||
|
edits
|
||||||
|
|
||||||
Upscaling via FAL's **Clarity Upscaler** also runs automatically for models
|
`video_generate` also accepts `upscale: true` on the FAL backend, chaining
|
||||||
whose catalog entry sets `upscale: True`:
|
ByteDance's **SeedVR2** video upscaler (2×, $0.001/MP of output video) after
|
||||||
|
generation. Video stays opt-in — doubling every video's resolution by
|
||||||
|
default would double its cost and latency.
|
||||||
|
|
||||||
| Model | Upscale? | Why |
|
When the FAL image pass runs, it uses these settings:
|
||||||
|---|---|---|
|
|
||||||
| `fal-ai/flux-2-pro` | ✓ | Backward-compat (was the pre-picker default) |
|
|
||||||
| All others | ✗ | Fast models would lose their sub-second value prop; hi-res models don't need it |
|
|
||||||
|
|
||||||
When upscaling runs, it uses these settings:
|
|
||||||
|
|
||||||
| Setting | Value |
|
| Setting | Value |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -207,7 +203,7 @@ If upscaling fails (network issue, rate limit), the original image is returned a
|
|||||||
1. **Model resolution** — `_resolve_fal_model()` reads `image_gen.model` from `config.yaml`, falls back to the `FAL_IMAGE_MODEL` env var, then to `fal-ai/flux-2/klein/9b`.
|
1. **Model resolution** — `_resolve_fal_model()` reads `image_gen.model` from `config.yaml`, falls back to the `FAL_IMAGE_MODEL` env var, then to `fal-ai/flux-2/klein/9b`.
|
||||||
2. **Payload building** — `_build_fal_payload()` translates your `aspect_ratio` into the model's native format (preset enum, aspect-ratio enum, or GPT literal), merges the model's default params, applies any caller overrides, then filters to the model's `supports` whitelist so unsupported keys are never sent.
|
2. **Payload building** — `_build_fal_payload()` translates your `aspect_ratio` into the model's native format (preset enum, aspect-ratio enum, or GPT literal), merges the model's default params, applies any caller overrides, then filters to the model's `supports` whitelist so unsupported keys are never sent.
|
||||||
3. **Submission** — `_submit_fal_request()` routes via direct FAL credentials or the managed Nous gateway.
|
3. **Submission** — `_submit_fal_request()` routes via direct FAL credentials or the managed Nous gateway.
|
||||||
4. **Upscaling** — runs when the agent passed `upscale: true`, or when the model's metadata has `upscale: True` (explicit `upscale: false` wins over the metadata default).
|
4. **Upscaling** — runs when the model's catalog entry has `upscale: True` (the default for sub-2MP models) or the agent passed `upscale: true`; an explicit `upscale: false` always skips it.
|
||||||
5. **Delivery** — final image URL returned to the agent, which emits a `MEDIA:<url>` tag that platform adapters convert to native media.
|
5. **Delivery** — final image URL returned to the agent, which emits a `MEDIA:<url>` tag that platform adapters convert to native media.
|
||||||
|
|
||||||
## Debugging
|
## Debugging
|
||||||
|
|||||||
Reference in New Issue
Block a user