feat(api): add flux 3 image klein node

BFL released FLUX 3 Image Klein (POST /v1/flux-3-image-klein) on
2026-10-15. It takes the same prompt, references, resolution and
aspect ratio as Flux 3 Image, so the node subclasses Flux3Image and
adds what Klein accepts on top: seed, steps, output_format, exact
width x height and a 0-7 safety_tolerance, without grounding.

The Flux 3 Image example workflow gets two bypassed Klein groups
(text to image, edit by URL); existing groups are untouched.
This commit is contained in:
Çağan Kiraz
2026-10-17 15:29:01 +03:00
parent ca50e11275
commit 773a83703f
3 changed files with 62 additions and 2 deletions
+2 -1
View File
@@ -42,7 +42,7 @@ Every BFL request is asynchronous: the node submits the job, then polls until th
- The submit response contains a `polling_url`, and the node polls **that URL**. BFL requires this because it points at the region that holds your task (Flux 3 Image, for example, returns a regional one).
- Only when a response has no `polling_url` does the node fall back to building `<base_url>get_result?id=<task_id>` itself, with the base URL from `config.ini` or the **Flux Config (BFL)** node.
- Polls run every 5 s: up to 40 attempts (~200 s) for image nodes, 120 for **Flux 3 Image** (`4k` can take several minutes) and 240 for the video nodes. A terminal status (`Error`, `Request Moderated`, `Content Moderated`) stops polling at once, even when BFL reports it as HTTP 503, and the node returns a black placeholder so the workflow keeps running.
- Polls run every 5 s: up to 40 attempts (~200 s) for image nodes, 120 for **Flux 3 Image** and **Flux 3 Image Klein** (`4k` can take several minutes) and 240 for the video nodes. A terminal status (`Error`, `Request Moderated`, `Content Moderated`) stops polling at once, even when BFL reports it as HTTP 503, and the node returns a black placeholder so the workflow keeps running.
- Every poll is logged in the ComfyUI console as `[BFL] Poll attempt … | GET <url>`, so you can see which URL was used.
## Nodes
@@ -51,6 +51,7 @@ Every BFL request is asynchronous: the node submits the job, then polls until th
| Node | Description |
|---|---|
| Flux 3 Image (BFL) | Text-to-image and editing with FLUX 3 in one node — up to 10 references (`image_1`…`image_10` are "Image 1"…"Image 10" in the prompt), bounding boxes as a JSON list at the end of the prompt, `768sq` to `4k` output (`flux-3-image`) |
| Flux 3 Image Klein (BFL) | Open-weights 5.6B FLUX 3 — same prompt, 10 references and bounding boxes as Flux 3 Image, plus `seed`, `steps` (1–16), `output_format` and exact `width` × `height`; no grounding (`flux-3-image-klein`) |
| Flux 3 Video T2V (BFL) | Text-to-video with FLUX 3 Video (up to 20 s, with audio) |
| Flux 3 Video I2V (BFL) | Image-to-video — feed keyframes from the Flux 3 Keyframes node or a single image |
| Flux 3 Video V2V (BFL) | Video continuation from an existing MP4 (URL or base64), up to 15 s |
+59
View File
@@ -909,6 +909,63 @@ class Flux3Image(BaseFlux):
return self.create_blank_image()
class Flux3ImageKlein(Flux3Image):
@classmethod
def INPUT_TYPES(cls):
types = super().INPUT_TYPES()
del types["required"]["grounding"]
types["required"]["safety_tolerance"] = ("INT", {"default": 2, "min": 0, "max": 7})
types["required"]["output_format"] = (["jpeg", "png"], {"default": "jpeg"})
types["required"]["steps"] = ("INT", {"default": 16, "min": 1, "max": 16})
size = ("INT", {
"default": 0, "min": 0, "max": 6256,
"tooltip": "0 = not sent. Set width and height together (64-6256) to override resolution and aspect_ratio"
})
types["optional"] = {
"width": size,
"height": size,
"seed": ("INT", {"default": -1}),
**types["optional"]
}
return types
def generate_image(self, prompt, resolution, aspect_ratio, safety_tolerance, output_format, steps,
width=0, height=0, seed=-1, config=None, **images):
arguments = {"prompt": prompt}
connected = [name for name in self.IMAGE_SLOTS if images.get(name)]
if connected:
arguments["images"] = [images[name] for name in connected]
if connected != self.IMAGE_SLOTS[:len(connected)]:
print(f"[BFL] Warning: references {connected} skip a socket — BFL numbers them Image 1.."
f"{len(connected)} in this order, so 'Image N' in the prompt shifts")
if width > 0 and height > 0:
arguments["width"] = width
arguments["height"] = height
elif width > 0 or height > 0:
print("[BFL] Warning: width and height must be set together — sending neither")
if resolution != "1k":
arguments["resolution"] = resolution
if aspect_ratio != "auto":
arguments["aspect_ratio"] = aspect_ratio
if safety_tolerance != 2:
arguments["safety_tolerance"] = safety_tolerance
if output_format != "jpeg":
arguments["output_format"] = output_format
if steps != 16:
arguments["steps"] = steps
if seed != -1:
arguments["seed"] = seed
try:
task_id = self.post_request("flux-3-image-klein", arguments, config)
if task_id:
return self.get_result(task_id, output_format=output_format, max_attempts=self.MAX_ATTEMPTS,
config_override=config)
return self.create_blank_image()
except Exception as e:
print(f"[BFL] Error generating image: {str(e)}")
return self.create_blank_image()
class FluxCredits:
RETURN_TYPES = ("STRING",)
FUNCTION = "get_credits"
@@ -947,6 +1004,7 @@ NODE_CLASS_MAPPINGS = {
"Flux2Klein9bPreview_BFL": Flux2Klein9bPreview,
"Flux2Klein4b_BFL": Flux2Klein4b,
"Flux3Image_BFL": Flux3Image,
"Flux3ImageKlein_BFL": Flux3ImageKlein,
"FluxCredits_BFL": FluxCredits
}
@@ -966,5 +1024,6 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"Flux2Klein9bPreview_BFL": "Flux 2 Klein 9B Preview (BFL)",
"Flux2Klein4b_BFL": "Flux 2 Klein 4B (BFL)",
"Flux3Image_BFL": "Flux 3 Image (BFL)",
"Flux3ImageKlein_BFL": "Flux 3 Image Klein (BFL)",
"FluxCredits_BFL": "Flux Credits (BFL)"
}
File diff suppressed because one or more lines are too long