fix(image_gen): disable default-on upscaling everywhere — opt-in only
The Aug 8 default-on upscaling policy (66ea4e686) chained the Clarity
Upscaler after every sub-2MP generation. Clarity is an SD1.5 creative
tile-diffusion enhancer (creativity 0.35, "masterpiece" prompt prefix) —
it redraws content, which degraded output on 100% of generations for
models like GPT Image 2 and Ideogram whose value is precise text
rendering, CJK, and photorealistic detail.
Policy now: no model upscales by default, on FAL or Krea. The `upscale`
tool param remains as a per-call opt-in (`upscale: true`); explicit
requests still chain Clarity (FAL) / Krea Enhance as before.
- FAL catalog: all 17 default-on entries flipped to upscale=False
- Krea plugin: medium + medium-turbo per-model defaults flipped off
- Tool schema: upscale param described as opt-in with a fidelity warning
- Tests updated: catalog invariant now pins all-off; default-on cases
now assert no upscaler call
- Docs (en + zh) updated to the opt-in policy
This commit is contained in:
@@ -57,9 +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,
|
||||
# Upscaling is opt-in everywhere (Aug 2026 policy: default-on
|
||||
# enhance passes degraded output quality).
|
||||
"upscale": False,
|
||||
},
|
||||
"krea-2-large": {
|
||||
"display": "Krea 2 Large",
|
||||
@@ -76,8 +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,
|
||||
# Opt-in only (Aug 2026 policy).
|
||||
"upscale": False,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -657,24 +657,18 @@ class TestUpscalePass:
|
||||
assert result["image"].endswith("native.png")
|
||||
assert mock_post.call_count == 2 # enhance attempted, fell back
|
||||
|
||||
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)
|
||||
def test_medium_skips_upscale_by_default(self):
|
||||
"""Upscaling is opt-in only (Aug 2026 policy) — even for
|
||||
krea-2-medium's 1.5K native output, no automatic Enhance pass."""
|
||||
result, mock_post, _ = self._run_generate(upscale=None, enhance_job=None)
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["upscaled"] is True
|
||||
assert result["image"].endswith("enhanced.png")
|
||||
assert mock_post.call_count == 2
|
||||
assert result["upscaled"] is False
|
||||
assert result["image"].endswith("native.png")
|
||||
assert mock_post.call_count == 1 # only the generation submit
|
||||
|
||||
def test_large_skips_upscale_by_default(self):
|
||||
"""krea-2-large is 2K native — no automatic Enhance pass."""
|
||||
"""krea-2-large: no automatic Enhance pass either."""
|
||||
result, mock_post, _ = self._run_generate(
|
||||
upscale=None, enhance_job=None, model="krea-2-large",
|
||||
)
|
||||
@@ -685,7 +679,7 @@ class TestUpscalePass:
|
||||
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."""
|
||||
"""Explicit upscale=False matches the off default."""
|
||||
result, mock_post, _ = self._run_generate(upscale=False, enhance_job=None)
|
||||
|
||||
assert result["success"] is True
|
||||
|
||||
@@ -57,23 +57,15 @@ class TestFalCatalog:
|
||||
assert not missing, f"{mid} missing required keys: {missing}"
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
def test_upscale_defaults_are_all_off(self, image_tool):
|
||||
"""Upscaling is opt-in only (Aug 2026 policy). The default-on
|
||||
experiment chained the Clarity Upscaler — a creative SD1.5
|
||||
tile-diffusion enhancer — after every sub-2MP generation, which
|
||||
degraded output quality (mangled GPT Image 2 / Ideogram text
|
||||
rendering, CJK, faces). No catalog entry may default upscale on."""
|
||||
for mid, meta in image_tool.FAL_MODELS.items():
|
||||
if mid in native_hi_res:
|
||||
assert meta["upscale"] is 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)"
|
||||
assert meta["upscale"] is False, \
|
||||
f"{mid} must not default upscale on — opt-in per call only"
|
||||
|
||||
|
||||
def test_edit_capable_entries_declare_a_full_edit_contract(self, image_tool):
|
||||
@@ -594,8 +586,8 @@ class TestUpscaleOptIn:
|
||||
model="bytedance/seedream/v5/lite/text-to-image",
|
||||
upscale=True, upscaler_called=True)
|
||||
|
||||
def test_explicit_false_disables_default_on_model(self, image_tool, monkeypatch):
|
||||
"""Klein defaults to upscale=True (sub-2MP native) — explicit False wins."""
|
||||
def test_explicit_false_stays_off(self, image_tool, monkeypatch):
|
||||
"""Explicit False and the catalog default agree: no upscale."""
|
||||
self._run(image_tool, monkeypatch,
|
||||
model="fal-ai/flux-2/klein/9b", upscale=False, upscaler_called=False)
|
||||
|
||||
@@ -604,9 +596,10 @@ class TestUpscaleOptIn:
|
||||
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_is_off_for_previously_default_on_model(self, image_tool, monkeypatch):
|
||||
"""flux-2-pro was the old default-on model — now off like the rest."""
|
||||
self._run(image_tool, monkeypatch,
|
||||
model="fal-ai/flux-2-pro", upscale=None, upscaler_called=True)
|
||||
model="fal-ai/flux-2-pro", upscale=None, upscaler_called=False)
|
||||
|
||||
def test_upscale_failure_falls_back_to_native(self, image_tool, monkeypatch):
|
||||
monkeypatch.setenv("FAL_IMAGE_MODEL", "fal-ai/flux-2/klein/9b")
|
||||
|
||||
@@ -13,8 +13,10 @@ Architecture:
|
||||
aspect_ratio) into the model-specific payload and filters to the
|
||||
``supports`` whitelist so models never receive rejected keys.
|
||||
- Upscaling via FAL's Clarity Upscaler is gated per-model via the ``upscale``
|
||||
flag — on for FLUX 2 Pro (backward-compat), off for all faster/newer models
|
||||
where upscaling would either hurt latency or add marginal quality.
|
||||
flag — OFF by default for every model. Clarity is an SD1.5 creative
|
||||
tile-diffusion enhancer (creativity 0.35 redraws content); chained by
|
||||
default it mangled GPT Image 2 / Ideogram text rendering, CJK, and faces
|
||||
(Aug 2026 quality regression). Upscaling is strictly per-call opt-in.
|
||||
|
||||
Pricing shown in UI strings is as-of the initial commit; we accept drift and
|
||||
update when it's noticed.
|
||||
@@ -93,6 +95,8 @@ logger = logging.getLogger(__name__)
|
||||
# rejected parameters (each FAL model rejects unknown keys differently).
|
||||
#
|
||||
# ``upscale`` controls whether to chain Clarity Upscaler after generation.
|
||||
# Policy (Aug 2026): False everywhere — the default-on experiment degraded
|
||||
# output quality (Clarity redraws content). Opt-in per call only.
|
||||
|
||||
FAL_MODELS: Dict[str, Dict[str, Any]] = {
|
||||
"fal-ai/flux-2/klein/9b": {
|
||||
@@ -115,7 +119,7 @@ FAL_MODELS: Dict[str, Dict[str, Any]] = {
|
||||
"prompt", "image_size", "num_inference_steps", "seed",
|
||||
"output_format", "enable_safety_checker",
|
||||
},
|
||||
"upscale": True,
|
||||
"upscale": False,
|
||||
# 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",
|
||||
@@ -150,7 +154,7 @@ FAL_MODELS: Dict[str, Dict[str, Any]] = {
|
||||
"num_images", "output_format", "enable_safety_checker",
|
||||
"safety_tolerance", "sync_mode", "seed",
|
||||
},
|
||||
"upscale": True, # Backward-compat: current default behavior.
|
||||
"upscale": False, # opt-in only (was default-on pre-Aug 2026)
|
||||
# Edit endpoint accepts up to 9 reference images.
|
||||
"edit_endpoint": "fal-ai/flux-2-pro/edit",
|
||||
"edit_supports": {
|
||||
@@ -183,7 +187,7 @@ FAL_MODELS: Dict[str, Dict[str, Any]] = {
|
||||
"seed", "output_format", "enable_safety_checker",
|
||||
"enable_prompt_expansion",
|
||||
},
|
||||
"upscale": True,
|
||||
"upscale": False,
|
||||
},
|
||||
"fal-ai/nano-banana-pro": {
|
||||
"display": "Nano Banana Pro (Gemini 3 Pro Image)",
|
||||
@@ -209,7 +213,7 @@ FAL_MODELS: Dict[str, Dict[str, Any]] = {
|
||||
"safety_tolerance", "seed", "sync_mode", "resolution",
|
||||
"enable_web_search", "limit_generations",
|
||||
},
|
||||
"upscale": True,
|
||||
"upscale": False,
|
||||
# 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 +248,7 @@ FAL_MODELS: Dict[str, Dict[str, Any]] = {
|
||||
"resolution", "enable_web_search", "limit_generations",
|
||||
"thinking_level",
|
||||
},
|
||||
"upscale": True,
|
||||
"upscale": False,
|
||||
"edit_endpoint": "fal-ai/nano-banana-2/edit",
|
||||
"edit_supports": {
|
||||
"prompt", "image_urls", "aspect_ratio", "num_images",
|
||||
@@ -276,7 +280,7 @@ FAL_MODELS: Dict[str, Dict[str, Any]] = {
|
||||
"prompt", "image_size", "quality", "num_images", "output_format",
|
||||
"background", "sync_mode",
|
||||
},
|
||||
"upscale": True,
|
||||
"upscale": False,
|
||||
# Edit endpoint: high-fidelity edits preserving composition/lighting.
|
||||
"edit_endpoint": "fal-ai/gpt-image-1.5/edit",
|
||||
"edit_supports": {
|
||||
@@ -315,7 +319,7 @@ FAL_MODELS: Dict[str, Dict[str, Any]] = {
|
||||
# openai_api_key (BYOK) intentionally omitted — all users go
|
||||
# through the shared FAL billing path.
|
||||
},
|
||||
"upscale": True,
|
||||
"upscale": False,
|
||||
# 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 +350,7 @@ FAL_MODELS: Dict[str, Dict[str, Any]] = {
|
||||
"prompt", "image_size", "rendering_speed", "expand_prompt",
|
||||
"style", "seed",
|
||||
},
|
||||
"upscale": True,
|
||||
"upscale": False,
|
||||
# Ideogram V3 edit endpoint takes `image_urls` (list).
|
||||
"edit_endpoint": "fal-ai/ideogram/v3/edit",
|
||||
"edit_supports": {
|
||||
@@ -374,7 +378,7 @@ FAL_MODELS: Dict[str, Dict[str, Any]] = {
|
||||
"prompt", "image_size", "enable_safety_checker",
|
||||
"colors", "background_color",
|
||||
},
|
||||
"upscale": True,
|
||||
"upscale": False,
|
||||
},
|
||||
"fal-ai/qwen-image": {
|
||||
"display": "Qwen Image",
|
||||
@@ -398,7 +402,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": True,
|
||||
"upscale": False,
|
||||
# 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 +433,7 @@ FAL_MODELS: Dict[str, Dict[str, Any]] = {
|
||||
"prompt", "aspect_ratio", "creativity", "seed",
|
||||
"image_style_references",
|
||||
},
|
||||
"upscale": True,
|
||||
"upscale": False,
|
||||
},
|
||||
"fal-ai/krea/v2/large/text-to-image": {
|
||||
"display": "Krea 2 Large",
|
||||
@@ -531,7 +535,7 @@ FAL_MODELS: Dict[str, Dict[str, Any]] = {
|
||||
"prompt", "image_size", "expansion_model", "num_images",
|
||||
"seed", "sync_mode", "enable_safety_checker", "output_format",
|
||||
},
|
||||
"upscale": True,
|
||||
"upscale": False,
|
||||
},
|
||||
"ideogram/v4/fast": {
|
||||
"display": "Ideogram V4 (Fast)",
|
||||
@@ -552,7 +556,7 @@ FAL_MODELS: Dict[str, Dict[str, Any]] = {
|
||||
"prompt", "image_size", "expansion_model", "rendering_speed",
|
||||
"num_images", "seed", "sync_mode",
|
||||
},
|
||||
"upscale": True,
|
||||
"upscale": False,
|
||||
},
|
||||
"alibaba/qwen-image-3/text-to-image": {
|
||||
"display": "Qwen Image 3",
|
||||
@@ -576,7 +580,7 @@ FAL_MODELS: Dict[str, Dict[str, Any]] = {
|
||||
"seed", "sync_mode", "output_format",
|
||||
"enable_prompt_expansion", "enable_safety_checker",
|
||||
},
|
||||
"upscale": True,
|
||||
"upscale": False,
|
||||
# Qwen Image 3 edit: 1-3 reference images, identity-preserving edits.
|
||||
"edit_endpoint": "alibaba/qwen-image-3/edit",
|
||||
"edit_supports": {
|
||||
@@ -605,7 +609,7 @@ FAL_MODELS: Dict[str, Dict[str, Any]] = {
|
||||
"prompt", "aspect_ratio", "num_images", "output_format",
|
||||
"sync_mode",
|
||||
},
|
||||
"upscale": True,
|
||||
"upscale": False,
|
||||
},
|
||||
"google/nano-banana-2-lite": {
|
||||
"display": "Nano Banana 2 Lite",
|
||||
@@ -628,7 +632,7 @@ FAL_MODELS: Dict[str, Dict[str, Any]] = {
|
||||
"output_format", "safety_tolerance", "sync_mode",
|
||||
"system_prompt", "limit_generations", "thinking_level",
|
||||
},
|
||||
"upscale": True,
|
||||
"upscale": False,
|
||||
# Fast multi-turn local edits with reference images via `image_urls`.
|
||||
"edit_endpoint": "google/nano-banana-2-lite/edit",
|
||||
"edit_supports": {
|
||||
@@ -656,7 +660,7 @@ FAL_MODELS: Dict[str, Dict[str, Any]] = {
|
||||
"prompt", "image_size", "enable_safety_checker",
|
||||
"colors", "background_color",
|
||||
},
|
||||
"upscale": True,
|
||||
"upscale": False,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1466,12 +1470,12 @@ IMAGE_GENERATE_SCHEMA = {
|
||||
"upscale": {
|
||||
"type": "boolean",
|
||||
"description": (
|
||||
"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."
|
||||
"Optional post-generation high-resolution pass (~2x, "
|
||||
"extra cost/latency). Off by default for every model — "
|
||||
"pass true to opt in. The upscaler is a creative "
|
||||
"enhancer and can alter fine detail (rendered text, "
|
||||
"faces), so only use it when resolution matters more "
|
||||
"than fidelity."
|
||||
),
|
||||
},
|
||||
},
|
||||
|
||||
@@ -160,31 +160,28 @@ This translation happens in `_build_fal_payload()` — agent code never has to k
|
||||
|
||||
## Upscaling
|
||||
|
||||
### Automatic (default-on for low-res models)
|
||||
### Opt-in only
|
||||
|
||||
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:
|
||||
No model upscales by default. Modern image models emit their best quality
|
||||
natively, and the available upscalers are *creative* enhancers (diffusion
|
||||
passes) that can subtly redraw content — degrading rendered text, faces, and
|
||||
fine detail. Upscaling only runs when the agent explicitly requests it.
|
||||
|
||||
| Backend | Models upscaled by default | Upscaler |
|
||||
|---|---|---|
|
||||
| **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 |
|
||||
### The `upscale` parameter (per-call opt-in)
|
||||
|
||||
### The `upscale` parameter (per-call override)
|
||||
- `upscale: true` — chain a high-resolution pass after generation:
|
||||
|
||||
The agent-facing `upscale` boolean overrides the default in either
|
||||
direction:
|
||||
| Backend | Upscaler |
|
||||
|---|---|
|
||||
| **FAL.ai** | Clarity Upscaler (2×, +$0.03/MP) |
|
||||
| **Krea** | Krea Enhance (2×, up to 8K ceiling) |
|
||||
| Other backends | no upscaler; native resolution returned |
|
||||
|
||||
- `upscale: false` — skip the automatic pass (faster/cheaper draft output)
|
||||
- `upscale: true` — force the pass, even on native hi-res models or image
|
||||
edits
|
||||
- `upscale: false` / omitted — native resolution (the default)
|
||||
|
||||
`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.
|
||||
generation.
|
||||
|
||||
When the FAL image pass runs, it uses these settings:
|
||||
|
||||
@@ -203,7 +200,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 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.
|
||||
4. **Upscaling** — runs only when the agent passed `upscale: true`; every model's catalog default is off.
|
||||
5. **Delivery** — final image URL returned to the agent, which emits a `MEDIA:<url>` tag that platform adapters convert to native media.
|
||||
|
||||
## Debugging
|
||||
|
||||
@@ -97,12 +97,7 @@ Make me a futuristic cityscape, landscape orientation
|
||||
|
||||
## 自动超分(Upscale)
|
||||
|
||||
是否启用 FAL **Clarity Upscaler** 按模型区分:
|
||||
|
||||
| 模型 | 超分? | 原因 |
|
||||
|---|---|---|
|
||||
| `fal-ai/flux-2-pro` | ✓ | 历史兼容(选择器出现前的默认) |
|
||||
| 其他 | ✗ | 亚秒级模型若再超分会失去速度优势;高分辨率模型本身已足够清晰 |
|
||||
超分**默认关闭**(所有模型)。现代图像模型原生输出即为最佳质量,而可用的超分器属于*创意增强器*(扩散重绘),会细微改动内容——损伤文字渲染、人脸与细节。仅当智能体显式传入 `upscale: true` 时才会执行 FAL **Clarity Upscaler** 超分。
|
||||
|
||||
超分启用时的主要参数:
|
||||
|
||||
@@ -121,7 +116,7 @@ Make me a futuristic cityscape, landscape orientation
|
||||
1. **模型解析** — `_resolve_fal_model()` 读取 `config.yaml` 的 `image_gen.model`,否则看 `FAL_IMAGE_MODEL` 环境变量,再否则默认 `fal-ai/flux-2/klein/9b`。
|
||||
2. **构造请求体** — `_build_fal_payload()` 将 `aspect_ratio` 转为各模型枚举或字面量,合并默认参数与调用方覆盖,并按 `supports` 白名单过滤非法字段。
|
||||
3. **提交** — `_submit_fal_request()` 根据凭据走直连 FAL 或 Nous 托管网关。
|
||||
4. **超分** — 仅当模型元数据标记 `upscale: True` 时执行。
|
||||
4. **超分** — 仅当调用显式传入 `upscale: true` 时执行;所有模型目录默认关闭。
|
||||
5. **交付** — 最终图像 URL 返回给智能体,并发出 `MEDIA:<url>`,由各平台适配器转为原生媒体消息。
|
||||
|
||||
## 调试
|
||||
|
||||
Reference in New Issue
Block a user