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:
Teknium
2026-08-08 12:12:54 -07:00
parent 137960c9aa
commit 66ea4e686d
6 changed files with 131 additions and 77 deletions

View File

@@ -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,

View File

@@ -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

View File

@@ -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,

View File

@@ -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"

View File

@@ -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."
), ),
}, },
}, },

View File

@@ -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