feat(media): opt-in upscale pass for image_generate and video_generate across FAL and Krea

The generated-media surface previously had almost no upscaler coverage:
only fal-ai/flux-2-pro chained Clarity Upscaler (hardcoded catalog
default), every other image model returned ~1MP output with no high-res
path, and video had no upscaler at all. Krea's API treats the enhancer
as a standard second pass; this brings the same shape to Hermes.

- image_generate: new optional 'upscale' boolean in the tool schema.
  Explicit true chains the backend upscaler on ANY model (including
  edits); explicit false disables flux-2-pro's automatic default;
  omitted keeps per-model catalog behavior. Response now reports
  'upscaled' so the agent knows which resolution it got.
- FAL image path: explicit flag overrides the catalog 'upscale' default
  (Clarity Upscaler, 2x). Failure falls back to the native image.
- Krea plugin: upscale=true chains Krea Enhance
  (/generate/enhance/krea/enhance, 2x, prompt-guided) through the same
  BYO/managed base URL + auth as generation, with a best-effort poll
  loop that never fails a successful generation.
- video_generate: new optional 'upscale' boolean; FAL video plugin
  chains ByteDance SeedVR2 (fal-ai/seedvr/upscale/video, 2x factor
  mode). Providers without upscalers ignore the kwarg per the ABC
  contract (documented in both ABCs).

Validation: targeted suites green (123 tests across 6 files, including
new coverage for override-wins/default-kept/failure-fallback on all
three paths); live E2E on direct FAL verified both chains end-to-end
(klein 9b + Clarity upscaled image; pixverse-v6 1s 360p + SeedVR2
upscaled video).
This commit is contained in:
Teknium
2026-08-08 11:39:49 -07:00
parent 71326399d3
commit 137960c9aa
12 changed files with 594 additions and 13 deletions
+6
View File
@@ -185,6 +185,12 @@ class ImageGenProvider(abc.ABC):
or :func:`error_response`. ``kwargs`` may contain forward-compat
parameters future versions of the schema will expose —
implementations MUST ignore unknown keys (no TypeError).
Known optional kwarg: ``upscale`` (bool) — when true, the caller
requests a post-generation high-resolution pass through the
backend's upscaler/enhancer. Providers without an upscaler simply
ignore it; providers that honor it should report ``upscaled: True``
in the response ``extra``.
"""
+6
View File
@@ -193,6 +193,12 @@ class VideoGenProvider(abc.ABC):
or :func:`error_response`. ``kwargs`` may contain forward-compat
parameters future versions of the schema will expose —
implementations MUST ignore unknown keys (no TypeError).
Known optional kwarg: ``upscale`` (bool) — when true, the caller
requests a post-generation high-resolution pass through the
backend's video upscaler. Providers without an upscaler simply
ignore it; providers that honor it should report ``upscaled: True``
in the response ``extra``.
"""
+1
View File
@@ -143,6 +143,7 @@ class FalImageGenProvider(ImageGenProvider):
"num_images",
"output_format",
"seed",
"upscale",
)
if key in kwargs and kwargs[key] is not None
}
+151
View File
@@ -111,6 +111,12 @@ _RETRYABLE_POLL_STATUSES = frozenset({408, 409, 425, 429, 500, 502, 503, 504})
_TERMINAL_STATES = {"completed", "failed", "cancelled"}
# Krea Enhance — the upscale/enhancer endpoint used for the optional
# ``upscale`` pass after generation ("1.5K native, 4K via Enhancer" is
# Krea's own pipeline shape). Cheap creative enhancer, max 8K.
_ENHANCE_PATH = "/generate/enhance/krea/enhance"
_ENHANCE_SCALE_FACTOR = 2
# ---------------------------------------------------------------------------
# Config
@@ -214,6 +220,121 @@ def _resolve_creativity(value: Optional[str]) -> str:
return "medium"
def _poll_krea_job(
base_url: str,
auth_token: str,
job_id: str,
*,
timeout_seconds: float = _POLL_TIMEOUT_SECONDS,
) -> Optional[Dict[str, Any]]:
"""Poll ``/jobs/{job_id}`` until terminal; return the job dict or None.
Best-effort variant of the main generate() poll loop used for secondary
jobs (the Enhance upscale pass): any failure returns ``None`` so the
caller can fall back instead of failing the whole generation.
"""
job_url = f"{base_url}/jobs/{job_id}"
headers = {
"Authorization": f"Bearer {auth_token}",
"User-Agent": "Hermes-Agent/1.0 (krea-image-gen)",
}
interval = _POLL_INITIAL_INTERVAL
deadline = time.monotonic() + timeout_seconds
while True:
time.sleep(interval)
interval = min(interval * _POLL_BACKOFF, _POLL_MAX_INTERVAL)
try:
resp = requests.get(job_url, headers=headers, timeout=30)
resp.raise_for_status()
job = resp.json()
except requests.HTTPError as exc:
status = exc.response.status_code if exc.response is not None else 0
if status not in _RETRYABLE_POLL_STATUSES or time.monotonic() >= deadline:
logger.warning("Krea enhance poll failed (%d) for job %s", status, job_id)
return None
continue
except Exception as exc: # noqa: BLE001 — timeout/connection/JSON
if time.monotonic() >= deadline:
logger.warning("Krea enhance poll gave up for job %s: %s", job_id, exc)
return None
continue
if isinstance(job, dict):
status_str = job.get("status")
if status_str in _TERMINAL_STATES or job.get("completed_at"):
return job
if time.monotonic() >= deadline:
logger.warning("Krea enhance job %s did not finish in %ds", job_id, int(timeout_seconds))
return None
def _extract_result_url(job: Optional[Dict[str, Any]]) -> Optional[str]:
"""Pull the first result URL out of a terminal Krea job dict."""
if not isinstance(job, dict):
return None
result = job.get("result")
if not isinstance(result, dict):
return None
urls = result.get("urls")
if isinstance(urls, list):
for candidate in urls:
if isinstance(candidate, str) and candidate.strip():
return candidate.strip()
single = result.get("url")
if isinstance(single, str) and single.strip():
return single.strip()
return None
def _enhance_image(
base_url: str,
auth_token: str,
image_url: str,
prompt: str,
*,
managed: bool,
) -> Optional[str]:
"""Run Krea Enhance on ``image_url``; return the enhanced URL or None.
Best-effort: any submit/poll/result failure logs and returns ``None`` so
the caller falls back to the original (un-upscaled) image — an upscale
failure must never destroy an already-successful generation.
"""
headers = {
"Authorization": f"Bearer {auth_token}",
"Content-Type": "application/json",
"User-Agent": "Hermes-Agent/1.0 (krea-image-gen)",
}
if managed:
headers["x-idempotency-key"] = str(uuid.uuid4())
payload: Dict[str, Any] = {
"image_url": image_url,
"image_scaling_factor": _ENHANCE_SCALE_FACTOR,
# Keep the enhancer faithful to the generated composition: the
# original prompt guides detail, and default ai_strength stays
# conservative (Krea default 0.4 adds detail without redrawing).
"prompt": prompt,
}
try:
resp = requests.post(
f"{base_url}{_ENHANCE_PATH}", headers=headers, json=payload, timeout=30,
)
resp.raise_for_status()
job_id = (resp.json() or {}).get("job_id")
except Exception as exc: # noqa: BLE001
logger.warning("Krea Enhance submit failed: %s", exc)
return None
if not isinstance(job_id, str) or not job_id:
logger.warning("Krea Enhance submit response missing job_id")
return None
job = _poll_krea_job(base_url, auth_token, job_id)
if not isinstance(job, dict) or job.get("status") in {"failed", "cancelled"}:
logger.warning("Krea Enhance job %s did not complete successfully", job_id)
return None
return _extract_result_url(job)
# ---------------------------------------------------------------------------
# Provider
# ---------------------------------------------------------------------------
@@ -700,6 +821,33 @@ 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.
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
)
if upscale_requested:
enhanced_url = _enhance_image(
base_url,
auth_token,
result_image_url,
prompt,
managed=managed is not None,
)
if enhanced_url:
result_image_url = enhanced_url
upscaled = True
else:
logger.warning(
"Krea Enhance pass failed — returning native-resolution image"
)
# Materialise locally — Krea result URLs may expire, mirroring
# what we do for xAI / OpenAI URL responses (#26942).
try:
@@ -719,7 +867,10 @@ class KreaImageGenProvider(ImageGenProvider):
"resolution": DEFAULT_RESOLUTION,
"creativity": creativity,
"job_id": job_id,
"upscaled": upscaled,
}
if upscaled:
extra["upscale_factor"] = _ENHANCE_SCALE_FACTOR
if isinstance(job.get("completed_at"), str):
extra["completed_at"] = job["completed_at"]
+58 -2
View File
@@ -531,6 +531,45 @@ def _check_fal_video_available() -> bool:
return _resolve_managed_fal_video_gateway() is not None
# ---------------------------------------------------------------------------
# Upscaler (SeedVR2 — video upscale pass)
# ---------------------------------------------------------------------------
# ByteDance SeedVR2 on FAL: $0.001/megapixel of output video. A 5s 720p→1440p
# 2x pass is roughly $0.44. Faithful restoration-style upscaler (the same
# model family Krea exposes as its "SeedVR2" video enhancer).
UPSCALER_ENDPOINT = "fal-ai/seedvr/upscale/video"
UPSCALER_FACTOR = 2
def _upscale_video(video_url: str) -> Optional[str]:
"""Upscale a generated video via SeedVR2; return the new URL or None.
Best-effort: any failure logs and returns ``None`` so the caller falls
back to the native-resolution video — an upscale failure must never
destroy an already-successful generation.
"""
try:
logger.info("Upscaling video with SeedVR2 (%dx)...", UPSCALER_FACTOR)
handle = _submit_fal_video_request(UPSCALER_ENDPOINT, {
"video_url": video_url,
"upscale_mode": "factor",
"upscale_factor": UPSCALER_FACTOR,
})
result = handle.get()
except Exception as exc: # noqa: BLE001
logger.warning("Video upscale failed: %s", exc)
return None
video = (result or {}).get("video") if isinstance(result, dict) else None
if isinstance(video, dict) and video.get("url"):
return video["url"]
if isinstance(video, str) and video:
return video
logger.warning("Video upscaler returned no URL")
return None
# ---------------------------------------------------------------------------
# Provider
# ---------------------------------------------------------------------------
@@ -618,6 +657,7 @@ class FALVideoGenProvider(VideoGenProvider):
negative_prompt: Optional[str] = None,
audio: Optional[bool] = None,
seed: Optional[int] = None,
upscale: Optional[bool] = None,
**kwargs: Any,
) -> Dict[str, Any]:
if not _check_fal_video_available():
@@ -722,9 +762,25 @@ class FALVideoGenProvider(VideoGenProvider):
provider="fal", model=family_id, prompt=prompt,
)
extra: Dict[str, Any] = {"endpoint": endpoint}
# Optional high-resolution pass (SeedVR2). Explicit agent/user opt-in
# only; best-effort — failure falls back to the native-resolution
# video rather than failing the generation.
upscaled = False
if upscale:
upscaled_url = _upscale_video(url)
if upscaled_url:
url = upscaled_url
upscaled = True
else:
logger.warning(
"Video upscale pass failed — returning native-resolution video"
)
extra: Dict[str, Any] = {"endpoint": endpoint, "upscaled": upscaled}
if upscaled:
extra["upscale_factor"] = UPSCALER_FACTOR
if isinstance(video, dict):
if video.get("file_size"):
if video.get("file_size") and not upscaled:
extra["file_size"] = video["file_size"]
if video.get("content_type"):
extra["content_type"] = video["content_type"]
@@ -581,6 +581,85 @@ class TestExplicitModelOverride:
assert post_url.endswith("/generate/image/krea/krea-2/medium-turbo")
# ---------------------------------------------------------------------------
# Upscale pass (Krea Enhance)
# ---------------------------------------------------------------------------
class TestUpscalePass:
def _run_generate(self, *, upscale, enhance_job):
"""Drive generate() with sequenced post/get mocks.
Sequence: generation submit POST → generation poll GET; then (when
upscale fires) enhance submit POST → enhance poll GET.
"""
from plugins.image_gen.krea import KreaImageGenProvider
gen_submit = _submit_response()
gen_poll = _poll_response(_completed_job("https://krea.cdn/native.png"))
enh_submit = _submit_response("00000000-0000-0000-0000-00000000e0e0")
enh_poll = _poll_response(enhance_job) if enhance_job else None
posts = [gen_submit, enh_submit]
gets = [gen_poll] + ([enh_poll] if enh_poll else [])
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.save_url_image",
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)
return result, mock_post, mock_get
def test_upscale_routes_through_enhance_endpoint(self):
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=True, enhance_job=enhance_job)
assert result["success"] is True
assert result["upscaled"] is True
assert result["upscale_factor"] == 2
assert result["image"].endswith("enhanced.png")
# Second POST hit the Enhance endpoint with the native image + factor.
assert mock_post.call_count == 2
enh_url = mock_post.call_args_list[1][0][0]
assert enh_url.endswith("/generate/enhance/krea/enhance")
enh_payload = mock_post.call_args_list[1].kwargs["json"]
assert enh_payload["image_url"] == "https://krea.cdn/native.png"
assert enh_payload["image_scaling_factor"] == 2
assert enh_payload["prompt"] == "a lamp"
def test_upscale_failure_falls_back_to_native(self):
failed_job = {
"job_id": "00000000-0000-0000-0000-00000000e0e0",
"status": "failed",
"created_at": "2026-05-27T00:00:00Z",
"completed_at": "2026-05-27T00:01:00Z",
"result": None,
}
result, mock_post, _ = self._run_generate(upscale=True, enhance_job=failed_job)
assert result["success"] is True
assert result["upscaled"] is False
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)
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
# ---------------------------------------------------------------------------
# Registration
# ---------------------------------------------------------------------------
@@ -341,3 +341,78 @@ class TestPayloadBuilder:
)
# Only prompt — no payload bloat for fields we can't verify
assert p == {"prompt": "a horse galloping"}
class TestUpscalePass:
"""Opt-in SeedVR2 upscale chain after generation."""
@pytest.fixture
def with_fake_fal(self, monkeypatch):
"""Stub fal_client.submit, capturing every endpoint hit in order."""
import sys
import types
captured = {"calls": []}
class FakeHandle:
def __init__(self, endpoint):
self._endpoint = endpoint
def get(self):
if self._endpoint.endswith("upscale/video"):
return {"video": {"url": "https://fake/upscaled.mp4"}}
return {"video": {"url": "https://fake/native.mp4"}}
fake = types.ModuleType("fal_client")
def _submit(endpoint, arguments=None, headers=None):
captured["calls"].append((endpoint, arguments))
return FakeHandle(endpoint)
fake.submit = _submit # type: ignore
monkeypatch.setitem(sys.modules, "fal_client", fake)
from plugins.video_gen import fal as fal_plugin
fal_plugin._fal_client = None
fal_plugin._managed_fal_video_client = None
fal_plugin._managed_fal_video_client_config = None
monkeypatch.setenv("FAL_KEY", "test")
monkeypatch.setattr(fal_plugin, "_resolve_managed_fal_video_gateway", lambda: None)
return captured
def test_upscale_chains_seedvr(self, with_fake_fal):
from plugins.video_gen.fal import FALVideoGenProvider, UPSCALER_ENDPOINT
result = FALVideoGenProvider().generate(
"a dog", model="pixverse-v6", upscale=True,
)
assert result["success"] is True
assert result["video"] == "https://fake/upscaled.mp4"
assert result["upscaled"] is True
assert result["upscale_factor"] == 2
endpoints = [c[0] for c in with_fake_fal["calls"]]
assert endpoints == ["fal-ai/pixverse/v6/text-to-video", UPSCALER_ENDPOINT]
# Upscale request carries the native URL + factor mode.
upscale_args = with_fake_fal["calls"][1][1]
assert upscale_args["video_url"] == "https://fake/native.mp4"
assert upscale_args["upscale_mode"] == "factor"
def test_no_upscale_by_default(self, with_fake_fal):
from plugins.video_gen.fal import FALVideoGenProvider
result = FALVideoGenProvider().generate("a dog", model="pixverse-v6")
assert result["success"] is True
assert result["video"] == "https://fake/native.mp4"
assert result["upscaled"] is False
assert len(with_fake_fal["calls"]) == 1
def test_upscale_failure_falls_back_to_native(self, with_fake_fal, monkeypatch):
from plugins.video_gen import fal as fal_plugin
from plugins.video_gen.fal import FALVideoGenProvider
monkeypatch.setattr(fal_plugin, "_upscale_video", lambda url: None)
result = FALVideoGenProvider().generate(
"a dog", model="pixverse-v6", upscale=True,
)
assert result["success"] is True
assert result["video"] == "https://fake/native.mp4"
assert result["upscaled"] is False
+125 -3
View File
@@ -274,12 +274,13 @@ class TestRegistryIntegration:
def test_schema_exposes_expected_agent_params(self, image_tool):
"""The agent-facing schema exposes the unified text+image surface:
prompt (required), aspect_ratio, and the image-to-image inputs
image_url + reference_image_urls. Model selection stays a user-level
config choice, never an agent-level arg."""
prompt (required), aspect_ratio, the image-to-image inputs
image_url + reference_image_urls, and the opt-in upscale pass. Model
selection stays a user-level config choice, never an agent-level arg."""
props = image_tool.IMAGE_GENERATE_SCHEMA["parameters"]["properties"]
assert set(props.keys()) == {
"prompt", "aspect_ratio", "image_url", "reference_image_urls",
"upscale",
}
assert image_tool.IMAGE_GENERATE_SCHEMA["parameters"]["required"] == ["prompt"]
@@ -447,3 +448,124 @@ class TestFalKreaCatalog:
def test_fal_krea_models_in_fal_catalog(self, image_tool):
assert "fal-ai/krea/v2/medium/text-to-image" in image_tool.FAL_MODELS
assert "fal-ai/krea/v2/large/text-to-image" in image_tool.FAL_MODELS
# ---------------------------------------------------------------------------
# Opt-in upscale pass
# ---------------------------------------------------------------------------
class _FakeHandle:
def __init__(self, result):
self._result = result
def get(self):
return self._result
class TestUpscaleOptIn:
"""Explicit ``upscale`` overrides the per-model catalog default."""
def _run(self, image_tool, monkeypatch, *, model, upscale, upscaler_called):
monkeypatch.setenv("FAL_IMAGE_MODEL", model)
monkeypatch.setattr(image_tool, "fal_key_is_configured", lambda: True)
monkeypatch.setattr(image_tool, "_resolve_managed_fal_gateway", lambda: None)
monkeypatch.setattr(
image_tool, "_submit_fal_request",
lambda endpoint, arguments=None: _FakeHandle(
{"images": [{"url": "https://fal/native.png", "width": 1024, "height": 768}]}
),
)
calls = []
def _fake_upscale(url, prompt):
calls.append(url)
return {
"url": "https://fal/upscaled.png", "width": 2048, "height": 1536,
"upscaled": True, "upscale_factor": 2,
}
monkeypatch.setattr(image_tool, "_upscale_image", _fake_upscale)
import json as _json
out = _json.loads(image_tool.image_generate_tool("a cat", upscale=upscale))
assert out["success"] is True
assert bool(calls) is upscaler_called
assert out["upscaled"] is upscaler_called
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."""
self._run(image_tool, monkeypatch,
model="fal-ai/flux-2/klein/9b", 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."""
self._run(image_tool, monkeypatch,
model="fal-ai/flux-2-pro", 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)
def test_omitted_keeps_catalog_default_on(self, image_tool, monkeypatch):
self._run(image_tool, monkeypatch,
model="fal-ai/flux-2-pro", upscale=None, upscaler_called=True)
def test_upscale_failure_falls_back_to_native(self, image_tool, monkeypatch):
monkeypatch.setenv("FAL_IMAGE_MODEL", "fal-ai/flux-2/klein/9b")
monkeypatch.setattr(image_tool, "fal_key_is_configured", lambda: True)
monkeypatch.setattr(image_tool, "_resolve_managed_fal_gateway", lambda: None)
monkeypatch.setattr(
image_tool, "_submit_fal_request",
lambda endpoint, arguments=None: _FakeHandle(
{"images": [{"url": "https://fal/native.png"}]}
),
)
monkeypatch.setattr(image_tool, "_upscale_image", lambda url, prompt: None)
import json as _json
out = _json.loads(image_tool.image_generate_tool("a cat", upscale=True))
assert out["success"] is True
assert out["image"] == "https://fal/native.png"
assert out["upscaled"] is False
class TestUpscaleDispatchForwarding:
"""The tool handler forwards explicit upscale to plugin providers."""
def test_dispatch_forwards_upscale(self, image_tool, monkeypatch):
from unittest.mock import MagicMock
import json as _json
monkeypatch.setattr(image_tool, "_read_configured_image_provider", lambda: "krea")
monkeypatch.setattr(image_tool, "_read_configured_image_model", lambda: None)
fake_provider = MagicMock()
fake_provider.generate.return_value = {"success": True, "image": "/tmp/x.png"}
monkeypatch.setattr(
"agent.image_gen_registry.get_provider", lambda name: fake_provider
)
monkeypatch.setattr(
"hermes_cli.plugins._ensure_plugins_discovered", lambda *a, **k: None
)
out = image_tool._dispatch_to_plugin_provider("a cat", "square", upscale=True)
assert _json.loads(out)["success"] is True
assert fake_provider.generate.call_args.kwargs["upscale"] is True
def test_dispatch_omits_upscale_when_unset(self, image_tool, monkeypatch):
from unittest.mock import MagicMock
monkeypatch.setattr(image_tool, "_read_configured_image_provider", lambda: "krea")
monkeypatch.setattr(image_tool, "_read_configured_image_model", lambda: None)
fake_provider = MagicMock()
fake_provider.generate.return_value = {"success": True, "image": "/tmp/x.png"}
monkeypatch.setattr(
"agent.image_gen_registry.get_provider", lambda name: fake_provider
)
monkeypatch.setattr(
"hermes_cli.plugins._ensure_plugins_discovered", lambda *a, **k: None
)
image_tool._dispatch_to_plugin_provider("a cat", "square")
assert "upscale" not in fake_provider.generate.call_args.kwargs
@@ -94,3 +94,20 @@ class TestUnifiedDispatch:
props = VIDEO_GENERATE_SCHEMA["parameters"]["properties"]
assert "operation" not in props
assert "video_url" not in props
def test_upscale_in_schema_and_forwarded(self):
"""`upscale` is an agent-facing param, forwarded to providers when
set and omitted (not None) when unset."""
from tools.video_generation_tool import VIDEO_GENERATE_SCHEMA
props = VIDEO_GENERATE_SCHEMA["parameters"]["properties"]
assert props["upscale"]["type"] == "boolean"
provider = _RecordingProvider()
video_gen_registry.register_provider(provider)
result = self._run({"prompt": "a dog", "upscale": True}, configured="fake")
assert result["success"] is True
assert provider.last_kwargs["upscale"] is True
self._run({"prompt": "a dog"}, configured="fake")
assert "upscale" not in provider.last_kwargs
+36 -4
View File
@@ -1079,6 +1079,7 @@ def image_generate_tool(
seed: Optional[int] = None,
image_url: Optional[str] = None,
reference_image_urls: Optional[list] = None,
upscale: Optional[bool] = None,
) -> str:
"""Generate an image from a text prompt, or edit a source image, via FAL.
@@ -1203,9 +1204,15 @@ def image_generate_tool(
if not images:
raise ValueError("No images were generated")
# Edit endpoints already return the final composition; the Clarity
# upscaler is a text-to-image quality pass, so skip it for edits.
should_upscale = bool(meta.get("upscale", False)) and not use_edit
# Explicit ``upscale`` (agent/user opt-in via the tool schema) wins
# over the per-model catalog default — including for edits, where an
# explicit request is intentional. The catalog default keeps skipping
# edits (Clarity is a text-to-image quality pass and must not alter
# edit compositions silently).
if upscale is not None:
should_upscale = bool(upscale)
else:
should_upscale = bool(meta.get("upscale", False)) and not use_edit
formatted_images = []
for img in images:
@@ -1241,6 +1248,7 @@ def image_generate_tool(
"success": True,
"image": formatted_images[0]["url"] if formatted_images else None,
"modality": modality,
"upscaled": bool(formatted_images and formatted_images[0].get("upscaled")),
}
debug_call_data["success"] = True
@@ -1448,6 +1456,16 @@ IMAGE_GENERATE_SCHEMA = {
"capped per-model; the description above indicates the max."
),
},
"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."
),
},
},
"required": ["prompt"],
},
@@ -1498,6 +1516,7 @@ def _dispatch_to_plugin_provider(
aspect_ratio: str,
image_url: Optional[str] = None,
reference_image_urls: Optional[list] = None,
upscale: Optional[bool] = None,
):
"""Route the call to a plugin-registered provider when one is selected.
@@ -1512,7 +1531,9 @@ def _dispatch_to_plugin_provider(
``image_url`` / ``reference_image_urls`` enable image-to-image / editing:
they are forwarded to the provider's ``generate()`` so the backend can
route to its edit endpoint.
route to its edit endpoint. ``upscale`` (when explicitly set) requests a
post-generation high-resolution pass; providers without upscale support
ignore it via their ``**kwargs`` (the ABC contract).
"""
configured = _read_configured_image_provider()
if not configured or configured == "fal":
@@ -1568,6 +1589,8 @@ def _dispatch_to_plugin_provider(
norm_refs = normalize_reference_images(reference_image_urls)
if norm_refs:
kwargs["reference_image_urls"] = norm_refs
if upscale is not None:
kwargs["upscale"] = bool(upscale)
result = provider.generate(**kwargs)
except TypeError as exc:
# A provider whose generate() signature predates image_url support
@@ -1655,6 +1678,7 @@ def _maybe_route_managed_krea(
aspect_ratio: str,
image_url: Optional[str] = None,
reference_image_urls: Optional[list] = None,
upscale: Optional[bool] = None,
) -> Optional[str]:
"""Route a native ``krea-2-*`` model to the managed Krea gateway, in managed mode.
@@ -1713,6 +1737,8 @@ def _maybe_route_managed_krea(
norm_refs = normalize_reference_images(reference_image_urls)
if norm_refs:
kwargs["reference_image_urls"] = norm_refs
if upscale is not None:
kwargs["upscale"] = bool(upscale)
result = provider.generate(**kwargs)
except Exception as exc: # noqa: BLE001
logger.warning("Managed Krea routing failed: %s", exc)
@@ -1781,6 +1807,9 @@ def _handle_image_generate(args, **kw):
aspect_ratio = args.get("aspect_ratio", DEFAULT_ASPECT_RATIO)
image_url = args.get("image_url")
reference_image_urls = args.get("reference_image_urls")
upscale = args.get("upscale")
if not isinstance(upscale, bool):
upscale = None
task_id = kw.get("task_id")
# Terminal-backend confinement chokepoint: convert path-like sources to
@@ -1799,6 +1828,7 @@ def _handle_image_generate(args, **kw):
prompt, aspect_ratio,
image_url=image_url,
reference_image_urls=reference_image_urls,
upscale=upscale,
)
if dispatched is not None:
return _postprocess_image_generate_result(dispatched, task_id=task_id)
@@ -1812,6 +1842,7 @@ def _handle_image_generate(args, **kw):
prompt, aspect_ratio,
image_url=image_url,
reference_image_urls=reference_image_urls,
upscale=upscale,
)
if krea_routed is not None:
return _postprocess_image_generate_result(krea_routed, task_id=task_id)
@@ -1821,6 +1852,7 @@ def _handle_image_generate(args, **kw):
aspect_ratio=aspect_ratio,
image_url=image_url,
reference_image_urls=reference_image_urls,
upscale=upscale,
)
return _postprocess_image_generate_result(raw, task_id=task_id)
+13
View File
@@ -145,6 +145,17 @@ VIDEO_GENERATE_SCHEMA: Dict[str, Any] = {
"dependent)."
),
},
"upscale": {
"type": "boolean",
"description": (
"Optional high-resolution pass: when true, the generated "
"video is run through the active backend's video upscaler "
"(extra cost and latency, roughly 2x resolution). Use when "
"the user asks for high-res / 4K output. Omit for the "
"model's native resolution. Ignored by backends without "
"an upscaler."
),
},
"model": {
"type": "string",
"description": (
@@ -328,6 +339,7 @@ def _handle_video_generate(args: Dict[str, Any], **_kw: Any) -> str:
negative_prompt = (args.get("negative_prompt") or "").strip() or None
audio = _coerce_bool(args.get("audio"))
seed = _coerce_int(args.get("seed"))
upscale = _coerce_bool(args.get("upscale"))
model_override = (args.get("model") or "").strip() or None
# Soft validation — providers do their own. Prompt is required by the
@@ -361,6 +373,7 @@ def _handle_video_generate(args: Dict[str, Any], **_kw: Any) -> str:
"negative_prompt": negative_prompt,
"audio": audio,
"seed": seed,
"upscale": upscale,
}
# Drop None entries so providers see clean defaults.
kwargs = {k: v for k, v in kwargs.items() if v is not None}
@@ -158,9 +158,32 @@ GPT Image 2 maps to 4:3 presets rather than 16:9 because its minimum pixel count
This translation happens in `_build_fal_payload()` — agent code never has to know about per-model schema differences.
## Automatic Upscaling
## Upscaling
Upscaling via FAL's **Clarity Upscaler** is gated per-model:
### On-demand (any model)
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`:
| Backend | Upscaler | Result |
|---|---|---|
| **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) |
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.
`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.
### Automatic (per-model default)
Upscaling via FAL's **Clarity Upscaler** also runs automatically for models
whose catalog entry sets `upscale: True`:
| Model | Upscale? | Why |
|---|---|---|
@@ -177,14 +200,14 @@ When upscaling runs, it uses these settings:
| Guidance scale | 4 |
| Inference steps | 18 |
If upscaling fails (network issue, rate limit), the original image is returned automatically.
If upscaling fails (network issue, rate limit), the original image is returned automatically. The response reports `upscaled: true/false` so the agent knows which resolution it got.
## How It Works Internally
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 only if the model's metadata has `upscale: True`.
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).
5. **Delivery** — final image URL returned to the agent, which emits a `MEDIA:<url>` tag that platform adapters convert to native media.
## Debugging