diff --git a/plugins/image_gen/krea/__init__.py b/plugins/image_gen/krea/__init__.py index 86bae4a986..ae801992e5 100644 --- a/plugins/image_gen/krea/__init__.py +++ b/plugins/image_gen/krea/__init__.py @@ -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, diff --git a/tests/plugins/image_gen/test_krea_provider.py b/tests/plugins/image_gen/test_krea_provider.py index cdb9ebd6cc..1288c442fb 100644 --- a/tests/plugins/image_gen/test_krea_provider.py +++ b/tests/plugins/image_gen/test_krea_provider.py @@ -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 diff --git a/tests/tools/test_image_generation.py b/tests/tools/test_image_generation.py index 6cea8ff9a3..ded1c3e2ba 100644 --- a/tests/tools/test_image_generation.py +++ b/tests/tools/test_image_generation.py @@ -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, diff --git a/tests/tools/test_image_generation_image_to_image.py b/tests/tools/test_image_generation_image_to_image.py index dbc68b7e3b..250a452660 100644 --- a/tests/tools/test_image_generation_image_to_image.py +++ b/tests/tools/test_image_generation_image_to_image.py @@ -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" diff --git a/tools/image_generation_tool.py b/tools/image_generation_tool.py index 238a290e20..be485db9f3 100644 --- a/tools/image_generation_tool.py +++ b/tools/image_generation_tool.py @@ -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." ), }, }, diff --git a/website/docs/user-guide/features/image-generation.md b/website/docs/user-guide/features/image-generation.md index e33544c716..a4504a9441 100644 --- a/website/docs/user-guide/features/image-generation.md +++ b/website/docs/user-guide/features/image-generation.md @@ -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:` tag that platform adapters convert to native media. ## Debugging