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.",
|
||||
"price": "$0.030 (text) / $0.035 (style refs) / $0.040 (moodboards)",
|
||||
"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": {
|
||||
"display": "Krea 2 Large",
|
||||
@@ -64,6 +67,8 @@ _MODELS: Dict[str, Dict[str, Any]] = {
|
||||
"strengths": "Photorealism, raw textured looks (motion blur, grain), expressive styles.",
|
||||
"price": "$0.060 (text) / $0.065 (style refs) / $0.070 (moodboards)",
|
||||
"path": "large",
|
||||
# 2K native — high-res enough out of the box.
|
||||
"upscale": False,
|
||||
},
|
||||
"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.",
|
||||
"price": "$0.015 (text) / $0.0175 (style refs)",
|
||||
"path": "medium-turbo",
|
||||
# 1.5K native — default the Enhance pass on.
|
||||
"upscale": True,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -821,17 +828,20 @@ class KreaImageGenProvider(ImageGenProvider):
|
||||
aspect_ratio=aspect,
|
||||
)
|
||||
|
||||
# Optional high-resolution pass (Krea Enhance). Explicit agent/user
|
||||
# opt-in via the ``upscale`` kwarg; config default via
|
||||
# ``image_gen.krea.upscale``. Best-effort: failure falls back to the
|
||||
# original image rather than failing the generation.
|
||||
# High-resolution pass (Krea Enhance). Precedence: explicit kwarg >
|
||||
# ``image_gen.krea.upscale`` config > per-model catalog default
|
||||
# (1.5K-native tiers default on; 2K-native Large stays off). Best-
|
||||
# effort: failure falls back to the original image rather than
|
||||
# failing the generation.
|
||||
upscaled = False
|
||||
upscale_requested = kwargs.get("upscale")
|
||||
if not isinstance(upscale_requested, bool):
|
||||
cfg_krea = _load_krea_config().get("krea")
|
||||
upscale_requested = bool(
|
||||
isinstance(cfg_krea, dict) and cfg_krea.get("upscale") is True
|
||||
)
|
||||
cfg_upscale = cfg_krea.get("upscale") if isinstance(cfg_krea, dict) else None
|
||||
if isinstance(cfg_upscale, bool):
|
||||
upscale_requested = cfg_upscale
|
||||
else:
|
||||
upscale_requested = bool(meta.get("upscale", False))
|
||||
if upscale_requested:
|
||||
enhanced_url = _enhance_image(
|
||||
base_url,
|
||||
|
||||
@@ -163,7 +163,7 @@ class TestGenerate:
|
||||
return_value=Path("/tmp/krea_krea-2-medium_test.png"),
|
||||
) as mock_save, \
|
||||
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["image"] == "/tmp/krea_krea-2-medium_test.png"
|
||||
@@ -215,7 +215,7 @@ class TestGenerate:
|
||||
return_value=Path("/tmp/x.png"),
|
||||
), \
|
||||
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"]
|
||||
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"}],
|
||||
image_style_references=[{"url": f"https://x.com/{i}.png"} for i in range(15)],
|
||||
creativity="high",
|
||||
upscale=False,
|
||||
)
|
||||
|
||||
payload = mock_post.call_args.kwargs["json"]
|
||||
@@ -290,6 +291,7 @@ class TestGenerate:
|
||||
"https://x.com/a.png",
|
||||
{"url": "https://x.com/b.png", "strength": 1.2},
|
||||
],
|
||||
upscale=False,
|
||||
)
|
||||
|
||||
payload = mock_post.call_args.kwargs["json"]
|
||||
@@ -513,7 +515,7 @@ class TestManagedGateway:
|
||||
return_value=Path("/tmp/x.png"),
|
||||
), \
|
||||
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
|
||||
post_url = mock_post.call_args[0][0]
|
||||
@@ -573,7 +575,7 @@ class TestExplicitModelOverride:
|
||||
return_value=Path("/tmp/x.png"),
|
||||
), \
|
||||
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["model"] == "krea-2-medium-turbo"
|
||||
@@ -587,7 +589,7 @@ class TestExplicitModelOverride:
|
||||
|
||||
|
||||
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.
|
||||
|
||||
Sequence: generation submit POST → generation poll GET; then (when
|
||||
@@ -603,6 +605,10 @@ class TestUpscalePass:
|
||||
posts = [gen_submit, enh_submit]
|
||||
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, \
|
||||
patch("plugins.image_gen.krea.requests.get", side_effect=gets) as mock_get, \
|
||||
patch(
|
||||
@@ -610,7 +616,7 @@ class TestUpscalePass:
|
||||
side_effect=lambda url, prefix: Path(f"/tmp/{url.rsplit('/', 1)[-1]}"),
|
||||
), \
|
||||
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
|
||||
|
||||
def test_upscale_routes_through_enhance_endpoint(self):
|
||||
@@ -651,14 +657,42 @@ class TestUpscalePass:
|
||||
assert result["image"].endswith("native.png")
|
||||
assert mock_post.call_count == 2 # enhance attempted, fell back
|
||||
|
||||
def test_no_upscale_by_default(self):
|
||||
result, mock_post, _ = self._run_generate(upscale=None, enhance_job=None)
|
||||
def test_medium_upscales_by_default(self):
|
||||
"""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["upscaled"] is False
|
||||
assert result["image"].endswith("native.png")
|
||||
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
|
||||
|
||||
@@ -57,17 +57,23 @@ class TestFalCatalog:
|
||||
assert not missing, f"{mid} missing required keys: {missing}"
|
||||
|
||||
|
||||
def test_only_flux2_pro_upscales_by_default(self, image_tool):
|
||||
"""Upscaling should default to False for all new models to preserve
|
||||
the <1s / fast-render value prop. Only flux-2-pro stays True for
|
||||
backward-compat with the previous default."""
|
||||
def test_upscale_defaults_track_native_resolution(self, image_tool):
|
||||
"""Default-on upscaling: every model whose native output is below
|
||||
~2MP upscales by default so users never silently get low-res images.
|
||||
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():
|
||||
if mid == "fal-ai/flux-2-pro":
|
||||
assert meta["upscale"] is True, \
|
||||
"flux-2-pro should keep upscale=True for backward-compat"
|
||||
else:
|
||||
if mid in native_hi_res:
|
||||
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"
|
||||
assert out["image"] == expected_url
|
||||
|
||||
def test_explicit_true_upscales_non_default_model(self, image_tool, monkeypatch):
|
||||
"""Klein has upscale=False in the catalog — explicit True wins."""
|
||||
def test_explicit_true_upscales_native_hi_res_model(self, image_tool, monkeypatch):
|
||||
"""Seedream Lite has upscale=False in the catalog (native 4K) —
|
||||
explicit True still wins."""
|
||||
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):
|
||||
"""flux-2-pro defaults to upscale=True — explicit False wins."""
|
||||
def test_explicit_false_disables_default_on_model(self, image_tool, monkeypatch):
|
||||
"""Klein defaults to upscale=True (sub-2MP native) — explicit False wins."""
|
||||
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):
|
||||
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):
|
||||
self._run(image_tool, monkeypatch,
|
||||
|
||||
@@ -124,7 +124,11 @@ class TestFalRouting:
|
||||
capture: dict = {}
|
||||
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)
|
||||
assert out["success"] is True
|
||||
assert out["modality"] == "text"
|
||||
|
||||
@@ -115,7 +115,7 @@ FAL_MODELS: Dict[str, Dict[str, Any]] = {
|
||||
"prompt", "image_size", "num_inference_steps", "seed",
|
||||
"output_format", "enable_safety_checker",
|
||||
},
|
||||
"upscale": False,
|
||||
"upscale": True,
|
||||
# Image-to-image / editing: FLUX.2 [klein] 9B edit endpoint takes
|
||||
# `image_urls` (list). Natural-language edits, multi-ref.
|
||||
"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",
|
||||
"enable_prompt_expansion",
|
||||
},
|
||||
"upscale": False,
|
||||
"upscale": True,
|
||||
},
|
||||
"fal-ai/nano-banana-pro": {
|
||||
"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",
|
||||
"enable_web_search", "limit_generations",
|
||||
},
|
||||
"upscale": False,
|
||||
"upscale": True,
|
||||
# Nano Banana Pro edit (Gemini 3 Pro Image): natural-language edits
|
||||
# with up to 2 reference images via `image_urls`.
|
||||
"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",
|
||||
"thinking_level",
|
||||
},
|
||||
"upscale": False,
|
||||
"upscale": True,
|
||||
"edit_endpoint": "fal-ai/nano-banana-2/edit",
|
||||
"edit_supports": {
|
||||
"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",
|
||||
"background", "sync_mode",
|
||||
},
|
||||
"upscale": False,
|
||||
"upscale": True,
|
||||
# Edit endpoint: high-fidelity edits preserving composition/lighting.
|
||||
"edit_endpoint": "fal-ai/gpt-image-1.5/edit",
|
||||
"edit_supports": {
|
||||
@@ -315,7 +315,7 @@ FAL_MODELS: Dict[str, Dict[str, Any]] = {
|
||||
# openai_api_key (BYOK) intentionally omitted — all users go
|
||||
# through the shared FAL billing path.
|
||||
},
|
||||
"upscale": False,
|
||||
"upscale": True,
|
||||
# GPT Image 2 edit endpoint lives under the OpenAI namespace on FAL
|
||||
# (NOT fal-ai/). Takes `image_urls` (list) + optional mask. We don't
|
||||
# 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",
|
||||
"style", "seed",
|
||||
},
|
||||
"upscale": False,
|
||||
"upscale": True,
|
||||
# Ideogram V3 edit endpoint takes `image_urls` (list).
|
||||
"edit_endpoint": "fal-ai/ideogram/v3/edit",
|
||||
"edit_supports": {
|
||||
@@ -374,7 +374,7 @@ FAL_MODELS: Dict[str, Dict[str, Any]] = {
|
||||
"prompt", "image_size", "enable_safety_checker",
|
||||
"colors", "background_color",
|
||||
},
|
||||
"upscale": False,
|
||||
"upscale": True,
|
||||
},
|
||||
"fal-ai/qwen-image": {
|
||||
"display": "Qwen Image",
|
||||
@@ -398,7 +398,7 @@ FAL_MODELS: Dict[str, Dict[str, Any]] = {
|
||||
"prompt", "image_size", "num_inference_steps", "guidance_scale",
|
||||
"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
|
||||
# `image_urls` (list) + natural-language edit instructions.
|
||||
"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",
|
||||
"image_style_references",
|
||||
},
|
||||
"upscale": False,
|
||||
"upscale": True,
|
||||
},
|
||||
"fal-ai/krea/v2/large/text-to-image": {
|
||||
"display": "Krea 2 Large",
|
||||
@@ -524,7 +524,7 @@ FAL_MODELS: Dict[str, Dict[str, Any]] = {
|
||||
"prompt", "image_size", "expansion_model", "num_images",
|
||||
"seed", "sync_mode", "enable_safety_checker", "output_format",
|
||||
},
|
||||
"upscale": False,
|
||||
"upscale": True,
|
||||
},
|
||||
"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",
|
||||
"num_images", "seed", "sync_mode",
|
||||
},
|
||||
"upscale": False,
|
||||
"upscale": True,
|
||||
},
|
||||
"alibaba/qwen-image-3/text-to-image": {
|
||||
"display": "Qwen Image 3",
|
||||
@@ -569,7 +569,7 @@ FAL_MODELS: Dict[str, Dict[str, Any]] = {
|
||||
"seed", "sync_mode", "output_format",
|
||||
"enable_prompt_expansion", "enable_safety_checker",
|
||||
},
|
||||
"upscale": False,
|
||||
"upscale": True,
|
||||
# Qwen Image 3 edit: 1-3 reference images, identity-preserving edits.
|
||||
"edit_endpoint": "alibaba/qwen-image-3/edit",
|
||||
"edit_supports": {
|
||||
@@ -598,7 +598,7 @@ FAL_MODELS: Dict[str, Dict[str, Any]] = {
|
||||
"prompt", "aspect_ratio", "num_images", "output_format",
|
||||
"sync_mode",
|
||||
},
|
||||
"upscale": False,
|
||||
"upscale": True,
|
||||
},
|
||||
"google/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",
|
||||
"system_prompt", "limit_generations", "thinking_level",
|
||||
},
|
||||
"upscale": False,
|
||||
"upscale": True,
|
||||
# Fast multi-turn local edits with reference images via `image_urls`.
|
||||
"edit_endpoint": "google/nano-banana-2-lite/edit",
|
||||
"edit_supports": {
|
||||
@@ -649,7 +649,7 @@ FAL_MODELS: Dict[str, Dict[str, Any]] = {
|
||||
"prompt", "image_size", "enable_safety_checker",
|
||||
"colors", "background_color",
|
||||
},
|
||||
"upscale": False,
|
||||
"upscale": True,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1459,11 +1459,12 @@ IMAGE_GENERATE_SCHEMA = {
|
||||
"upscale": {
|
||||
"type": "boolean",
|
||||
"description": (
|
||||
"Optional high-resolution pass: when true, the generated "
|
||||
"image is run through the active backend's upscaler/"
|
||||
"enhancer (extra cost and latency, roughly 2x resolution). "
|
||||
"Use when the user asks for high-res / print / wallpaper "
|
||||
"quality output. Omit for the model's native resolution."
|
||||
"Optional override for the high-resolution pass. Models "
|
||||
"with sub-2MP native output upscale automatically (~2x, "
|
||||
"extra cost/latency); pass false for a faster/cheaper "
|
||||
"draft at native resolution, or true to force the pass "
|
||||
"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
|
||||
|
||||
### On-demand (any model)
|
||||
### Automatic (default-on for low-res models)
|
||||
|
||||
The agent-facing `upscale` parameter requests a high-resolution pass after
|
||||
generation on **any** model — ask for "high-res", "print quality", or
|
||||
"wallpaper" output and the agent sets `upscale: true`:
|
||||
Every model whose native output is below ~2MP automatically runs a
|
||||
high-resolution pass after generation, so you never silently get a low-res
|
||||
image:
|
||||
|
||||
| Backend | Upscaler | Result |
|
||||
| Backend | Models upscaled by default | Upscaler |
|
||||
|---|---|---|
|
||||
| **FAL.ai** (all models) | Clarity Upscaler | ~2× resolution, +$0.03/MP |
|
||||
| **Krea** (Krea 2 family) | Krea Enhance | 2× resolution (up to 8K ceiling) |
|
||||
| Other backends | — | parameter is ignored (native resolution returned) |
|
||||
| **FAL.ai** | all except Seedream 5 Pro/Lite and Krea 2 Large (native ≥2MP) | Clarity Upscaler (2×, +$0.03/MP) |
|
||||
| **Krea** | Krea 2 Medium + Medium Turbo (1.5K native); Large (2K) skips | Krea Enhance (2×, up to 8K ceiling) |
|
||||
| Other backends | — | no upscaler; native resolution returned |
|
||||
|
||||
An explicit `upscale: false` also *disables* the automatic pass on models
|
||||
that default to it (currently `flux-2-pro`). Passing `upscale: true` with an
|
||||
image edit runs the pass on the edited output too.
|
||||
### The `upscale` parameter (per-call override)
|
||||
|
||||
`video_generate` accepts the same `upscale` parameter on the FAL backend,
|
||||
chaining ByteDance's **SeedVR2** video upscaler (2×, $0.001/MP of output
|
||||
video) after generation.
|
||||
The agent-facing `upscale` boolean overrides the default in either
|
||||
direction:
|
||||
|
||||
### 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
|
||||
whose catalog entry sets `upscale: True`:
|
||||
`video_generate` also accepts `upscale: true` on the FAL backend, chaining
|
||||
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 |
|
||||
|---|---|---|
|
||||
| `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:
|
||||
When the FAL image pass runs, it uses these settings:
|
||||
|
||||
| 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`.
|
||||
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.
|
||||
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.
|
||||
|
||||
## Debugging
|
||||
|
||||
Reference in New Issue
Block a user