5 Commits
Author SHA1 Message Date
Çağan Kiraz 655dda30ac chore(release): bump version to 1.7.0 2026-10-17 15:29:02 +03:00
Çağan Kiraz 773a83703f 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.
2026-10-17 15:29:01 +03:00
Çağan Kiraz ca50e11275 chore(release): bump version to 1.6.0 2026-10-02 00:53:50 +03:00
Çağan Kiraz 057248fa3a feat: add flux 3 image node and poll the returned polling_url
FLUX 3 Image (flux-3-image) generates and edits from one prompt, with
up to ten references and bounding boxes. The endpoint rejects seed,
output_format and webhooks, and 4k renders can take minutes, so the
node polls up to 120 attempts and keeps the result lossless.

BFL requires polling the polling_url returned at submit: it points at
the region holding the task, and FLUX 3 Image returns a regional one.
post_request now returns a TaskId carrying it and get_result polls it
for every node, falling back to get_result?id= only when it is absent.
A failed task reported as HTTP 503 with a terminal status now stops at
once instead of polling until the timeout.

Adds an example workflow and documents the polling behavior.
2026-10-02 00:53:35 +03:00
Çağan Kiraz 56481721a7 ci: send each version's changelog to the comfy registry
The registry's Updates section was empty for every version because the
publish workflow never passed a changelog. comfy-cli reads
COMFY_NODE_CHANGELOG, so the workflow now builds it from the version's
CHANGELOG.md section. The registry shows it as one plain paragraph (no
Markdown), so the script keeps the first column of each table row.

Releases also date that CHANGELOG section and get an annotated vX.Y.Z
tag; CLAUDE.md and /version-bump describe the steps.
2026-10-02 00:53:27 +03:00
10 changed files with 262 additions and 15 deletions
+10 -7
View File
@@ -1,12 +1,12 @@
---
description: Bump the package version in pyproject.toml (patch | minor | major) and suggest the matching Conventional Commits release commit message.
description: Bump the package version in pyproject.toml (patch | minor | major), date its CHANGELOG section, and suggest the release commit message and tag.
allowed-tools: Read, Edit, Bash(git diff:*)
argument-hint: patch | minor | major
---
# `/version-bump` — bump pyproject.toml version
Edits a single field — `[project] version` in `pyproject.toml` — using semver.
Edits `[project] version` in `pyproject.toml` using semver, and dates the matching `CHANGELOG.md` section (the publish workflow sends that section to the registry as the version's changelog).
## Workflow
@@ -18,18 +18,21 @@ Edits a single field — `[project] version` in `pyproject.toml` — using semve
- If missing or invalid: ask once.
3. Edit `pyproject.toml`:
- `version = "<old>"` → `version = "<new>"`.
4. `git diff pyproject.toml` — confirm exactly one line changed.
5. Propose a commit message (do not run `git commit`):
4. Edit `CHANGELOG.md`:
- `## [Unreleased]` → `## [<new>] — <today, YYYY-MM-DD>`.
- If there is no `## [Unreleased]` section, warn the user: the registry will show no changelog for `<new>`.
5. `git diff pyproject.toml CHANGELOG.md` — confirm exactly one line changed in each.
6. Propose the commit message and the tag (do not run `git commit` or `git tag`):
```
chore(release): bump version to <new>
```
Suggest the user run `/commit` (or `git commit -m '...' pyproject.toml`) when they're ready.
Suggest the user run `/commit` (or `git commit -m '...' pyproject.toml CHANGELOG.md`), then tag that commit with `git tag -a v<new> -m "v<new>"` and push both with `git push --follow-tags`.
## Safety rails
- **Only** modify the `version` field. No description / license / dependency changes.
- Do not touch any other file (no CHANGELOG, no git tag, no push).
- **Only** modify the `version` field and the CHANGELOG heading. No description / license / dependency changes.
- Do not touch any other file (no git tag, no push).
- Do not commit.
- If `version` is missing or unparseable, stop and ask the user.
+54
View File
@@ -0,0 +1,54 @@
"""Print a version's CHANGELOG.md entry as plain text for the Comfy Registry.
The registry's "Updates" section shows the changelog as one plain-text
paragraph (no Markdown, newlines collapse), so each table is reduced to its
first column: "Added: A; B. Fixed: C. Full changelog: <url>".
Usage: registry_changelog.py [version] (defaults to pyproject.toml's version)
"""
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
def plain(cell):
cell = re.sub(r"\[([^\]]*)\]\([^)]*\)", r"\1", cell) # [text](url) -> text
return cell.replace("\\|", "|").replace("`", "").replace("**", "").strip()
def main():
pyproject = (ROOT / "pyproject.toml").read_text(encoding="utf-8")
version = sys.argv[1] if len(sys.argv) > 1 else re.search(r'^version\s*=\s*"([^"]+)"', pyproject, re.M).group(1)
changelog = (ROOT / "CHANGELOG.md").read_text(encoding="utf-8")
section = re.search(rf"^## \[{re.escape(version)}\][^\n]*\n(.*?)(?=^## \[|\Z)", changelog, re.M | re.S)
if not section:
print(
f"::warning::CHANGELOG.md has no '## [{version}]' section; publishing without a changelog", file=sys.stderr
)
return
groups, in_table = [], False # [(heading, [first-column cells])]
for line in section.group(1).splitlines():
if line.startswith("### "):
groups.append((line[4:].strip(), []))
in_table = False
elif line.startswith("|"):
if not in_table:
in_table = True # the first row of every table is its header
elif groups and not re.match(r"^\|[\s:|-]+$", line): # skip the |---| separator
groups[-1][1].append(plain(re.split(r"(?<!\\)\|", line)[1]))
else:
in_table = False
parts = [f"{heading}: {'; '.join(items)}." for heading, items in groups if items]
repo = re.search(r'^Repository\s*=\s*"([^"]+)"', pyproject, re.M)
if repo:
parts.append(f"Full changelog: {repo.group(1)}/blob/master/CHANGELOG.md")
print(" ".join(parts))
if __name__ == "__main__":
main()
+8
View File
@@ -19,6 +19,14 @@ jobs:
steps:
- name: Check out code
uses: actions/checkout@v4
# comfy-cli sends $COMFY_NODE_CHANGELOG as the version's changelog (the registry's "Updates" section)
- name: Build registry changelog from CHANGELOG.md
run: |
{
echo "COMFY_NODE_CHANGELOG<<CHANGELOG_EOF"
python3 .github/scripts/registry_changelog.py
echo "CHANGELOG_EOF"
} >> "$GITHUB_ENV"
- name: Publish Custom Node
uses: Comfy-Org/publish-node-action@v1
with:
+25
View File
@@ -2,6 +2,31 @@
All notable changes to this project from v1.1.0 onward are documented in this file. Earlier history lives in `git log`.
## [1.7.0] — 2026-10-17
### Added
| Node / Feature | Endpoint | Notes |
|---|---|---|
| Flux 3 Image Klein (BFL) | `POST /v1/flux-3-image-klein` | BFL release 2026-10-15: the open-weights 5.6B FLUX 3 Image. Same prompt (bounding boxes as a JSON list at its end), `image_1`–`image_10` references, `resolution` and `aspect_ratio` as Flux 3 Image, and no `grounding`. Adds `seed` (-1 = random), `steps` (1–16), `output_format` (jpeg / png), an exact `width` × `height` (64–6256, sent only when both are set, overriding resolution and aspect ratio) and `safety_tolerance` 0–7. Optional fields are sent only when changed. Shares Flux 3 Image's 120-attempt polling ceiling. |
| FLUX 3 Image Klein example workflow groups | — | `workflows/BFL-API-flux-3-image.json` gets two groups: **Klein — Text to Image** (bounding boxes, fixed seed) and **Klein — Edit an Image** (reference by URL). Appended only, so the existing nodes, links and groups are unchanged. The new groups are bypassed like the rest. Loaded in ComfyUI 0.37.0 (frontend 1.52.7). |
## [1.6.0] — 2026-10-02
### Added
| Node / Feature | Endpoint | Notes |
|---|---|---|
| Flux 3 Image (BFL) | `POST /v1/flux-3-image` | BFL release 2026-10-01. One node for text-to-image and editing: `prompt` (bounding boxes go at its end as a JSON list), up to 10 reference sockets `image_1`–`image_10` sent as `images` and numbered Image 1…N in socket order (a skipped socket logs a warning, since the numbering shifts), `resolution` (`768sq` / `1k` / `1.5k` / `2k` / `4k`), `aspect_ratio` (15 ratios or `auto`), `grounding` (web and image search, on by default), `safety_tolerance` (0–4). The endpoint rejects `seed`, `output_format` and webhooks, so none are exposed. Polls up to 120 × 5 s because `4k` can take several minutes, and keeps the result lossless (png) on its way into the IMAGE tensor. |
| FLUX 3 Image example workflow | — | New `workflows/BFL-API-flux-3-image.json`, built and saved with ComfyUI 0.37.0 (frontend 1.52.7): text-to-image with bounding boxes in the prompt, an edit with a reference given by URL (core **Text** node), and a style transfer that combines a local image (Load Image → Image to Base64, as Image 1) with a URL reference (Image 2). Groups are bypassed like the other examples. |
### Fixed
| Issue | Detail |
|---|---|
| Polling ignored the returned polling_url | BFL requires polling the `polling_url` from the submit response (it points at the region holding the task; FLUX 3 Image returns a regional one). `BaseFlux.post_request` now passes it to `get_result` along with the task id, for every node; the self-built `get_result?id=` is only a fallback when a response has none. |
| Failed tasks reported as HTTP 503 were polled until timeout | FLUX 3 can report a failed task as HTTP 503 with a normal JSON body. A non-200 poll whose body carries `Error`, `Content Moderated` or `Request Moderated` now stops at once and logs the details instead of retrying to `max_attempts`. |
## [1.5.0] — 2026-09-26
### Added
+3 -3
View File
@@ -89,10 +89,10 @@ This is a **ComfyUI custom-nodes package** that wraps the **Black Forest Labs (B
- **Seed handling**: `seed = -1` means *random*. The node parameter defaults to `-1`. Only insert `arguments["seed"] = seed` into the request body when `seed != -1` — never send `-1` to BFL.
- **Optional argument inclusion**: build the request `arguments` dict with required keys, then conditionally add optional ones only when non-default / non-empty (`if webhook_url:`, `if image_prompt:`, …). Matches every node in `nodes/api_node.py`.
- **Graceful failure**: on any unhandled exception, on terminal API status (`Error`, `Content Moderated`, `Request Moderated`), or on polling exhaustion, return `self.create_blank_image()` — a black 512×512 IMAGE tensor. ComfyUI workflows must never crash because of a BFL failure.
- **Polling**: 5-second interval, max 40 attempts (~200 s ceiling). Already implemented in `BaseFlux.get_result` — do not write a second polling loop in a subclass.
- **Polling**: 5-second interval, max 40 attempts (~200 s ceiling). Already implemented in `BaseFlux.get_result` — do not write a second polling loop in a subclass. Slow endpoints pass a larger `max_attempts` instead (Flux 3 Video 240, Flux 3 Image 120).
- **Config resolution**: every generation node accepts an optional `config` input of type `BFL_CONFIG` (the output of `FluxConfig_BFL`). Pass it through to `super().generate_image(url_path, arguments, config)`; the base class hands it to `get_config_loader(config)` from `nodes/config_node.py:57-67`, which decides whether to use the `config.ini`-backed default loader or build one from the override. **Both paths must work** — `config=None` (use file) and `config=<dict>` (override).
- **Conventional Commits 1.0.0**: `<type>[scope][!]: <description>`. Types: `feat`, `fix`, `refactor`, `chore`, `docs`, `style`, `perf`, `test`, `build`, `ci`. Scopes: `api`, `finetune`, `config`, `utils`, `web`, `docs`, `release`, `tooling`. Description: lowercase, imperative ("add", not "added"/"adds"), no trailing period, ≤ 72 chars. Body explains *why*, wrapped at 72. Use `!` after type/scope for breaking changes. No Jira prefix. **No `Co-Authored-By: Claude` / "Generated with…" footer.** Do NOT inspect `git log` to derive style — historical commits used `update:` and similar non-standard types; the new standard starts now.
- **Version bumps**: edit `pyproject.toml` `version` field only. Commit message: `chore(release): bump version to X.Y.Z`.
- **Version bumps**: edit `pyproject.toml` `version` field and rename `CHANGELOG.md`'s `## [Unreleased]` heading to `## [X.Y.Z] — YYYY-MM-DD` — the publish workflow turns that section into the registry's changelog (`.github/scripts/registry_changelog.py` keeps the first column of each table row, so keep those entries short and plain). Commit message: `chore(release): bump version to X.Y.Z`. Then tag that commit `git tag -a vX.Y.Z -m "vX.Y.Z"` and push both with `git push --follow-tags`.
### Never do
@@ -109,7 +109,7 @@ This is a **ComfyUI custom-nodes package** that wraps the **Black Forest Labs (B
- `ConfigLoader` in `nodes/config.py:85-86` constructs a module-level singleton `config_loader` at import time. Some nodes still reference that singleton; new code should use `get_config_loader(config)` from `nodes/config_node.py:57-67` (which respects the per-call override) instead.
- `BASE_URL` in `config.ini` may or may not end with `/`. `ConfigLoader.create_url` uses `urljoin` — pass the `path` argument **without** a leading slash, otherwise `urljoin` will strip the existing prefix path.
- **Finetune endpoints are regional**: `api.us.bfl.ai`, `api.eu.bfl.ai`. The default `api.bfl.ai/v1/` does **not** serve finetune routes. Region selection is via `FluxConfig_BFL`'s `region` field or per-call. See `nodes/finetune.py`.
- BFL's async pattern: POST to `flux-pro-1.1` (etc.) returns `{"id": "<task_id>"}`. Polling GET goes to `get_result?id=<task_id>` — note: **query parameter `id`**, not a path segment. `BaseFlux.get_result` already builds this correctly; if you write a one-off polling call, match it.
- BFL's async pattern: POST to `flux-pro-1.1` (etc.) returns `{"id": "<task_id>", "polling_url": "..."}`. BFL requires polling that **returned `polling_url`** — it points at the region holding the task (FLUX 3 Image returns a regional one), so don't build the URL yourself. `BaseFlux.post_request` returns a `TaskId` (a `str` subclass) that carries it, and `BaseFlux.get_result` polls it; only a plain id with no polling URL falls back to `get_result?id=<task_id>` (query parameter `id`, not a path segment). If you write a one-off POST, pass its `polling_url` through the same way.
- The newer Flux 2 / Klein nodes (`nodes/api_node.py` lower half) sometimes inline `self.post_request` + `self.get_result` instead of calling `super().generate_image(...)`. Both styles exist in the codebase; if you're scaffolding a new node, **prefer `super().generate_image(...)`** — it's shorter and runs the dimension check for you.
- `__init__.py` does dynamic import of every entry in `node_list`. A new node file in `nodes/<name>.py` is invisible to ComfyUI until you append `"<name>"` to that list.
- `WEB_DIRECTORY = "./web"` in `__init__.py:21` exposes `web/js/*` to ComfyUI's frontend. Renaming or moving `web/` breaks the frontend extensions silently — they just won't load.
+11
View File
@@ -36,11 +36,22 @@ Custom nodes for integrating Flux models with the BFL API.
You can either use `config.ini` for a global API key, or connect a **Flux Config (BFL)** node directly to any generation node to override the key, base URL, and region per-node. If no config node is connected, `config.ini` is used automatically.
### How results are polled
Every BFL request is asynchronous: the node submits the job, then polls until the result is ready.
- 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** 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
### Generation
| 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 |
+120
View File
@@ -850,6 +850,122 @@ class Flux2Klein4b(BaseFlux):
return self.create_blank_image()
class Flux3Image(BaseFlux):
CATEGORY = "BFL/Flux3"
# 4k renders can take several minutes (BFL docs); 5s interval -> ~10 min
MAX_ATTEMPTS = 120
IMAGE_SLOTS = [f"image_{i}" for i in range(1, 11)]
@classmethod
def INPUT_TYPES(cls):
image_socket = ("STRING", {
"forceInput": True,
"tooltip": "Reference image (base64 from Image to Base64, or a URL). The prompt calls the "
"connected images Image 1, Image 2, ... in socket order"
})
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True}),
"resolution": (["768sq", "1k", "1.5k", "2k", "4k"], {"default": "1k"}),
"aspect_ratio": (["auto", "21:9", "2:1", "16:9", "3:2", "7:5", "4:3", "5:4", "1:1", "4:5", "3:4",
"5:7", "2:3", "9:16", "1:2", "9:21"], {"default": "auto"}),
"grounding": ("BOOLEAN", {
"default": True,
"tooltip": "Let FLUX 3 research the prompt with web and image search before generating"
}),
"safety_tolerance": ("INT", {"default": 2, "min": 0, "max": 4})
},
"optional": {
**{name: image_socket for name in cls.IMAGE_SLOTS},
"config": ("BFL_CONFIG",)
}
}
def generate_image(self, prompt, resolution, aspect_ratio, grounding, safety_tolerance, 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 resolution != "1k":
arguments["resolution"] = resolution
if aspect_ratio != "auto":
arguments["aspect_ratio"] = aspect_ratio
if safety_tolerance != 2:
arguments["safety_tolerance"] = safety_tolerance
if not grounding:
arguments["grounding"] = False
try:
task_id = self.post_request("flux-3-image", arguments, config)
if task_id:
# png: process_result re-encodes the result into this format, and jpeg would recompress it
return self.get_result(task_id, output_format="png", 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 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"
@@ -887,6 +1003,8 @@ NODE_CLASS_MAPPINGS = {
"Flux2Klein9b_BFL": Flux2Klein9b,
"Flux2Klein9bPreview_BFL": Flux2Klein9bPreview,
"Flux2Klein4b_BFL": Flux2Klein4b,
"Flux3Image_BFL": Flux3Image,
"Flux3ImageKlein_BFL": Flux3ImageKlein,
"FluxCredits_BFL": FluxCredits
}
@@ -905,5 +1023,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"Flux2Klein9b_BFL": "Flux 2 Klein 9B (BFL)",
"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)"
}
+29 -4
View File
@@ -12,6 +12,16 @@ from .status import Status
REQUEST_TIMEOUT = 300 # seconds for connect + read
class TaskId(str):
"""Task id from post_request that also carries the submit response's polling_url.
BFL requires polling the returned polling_url (it points at the region holding the
task); as a str subclass, callers keep passing the id straight to get_result.
"""
polling_url = None
class BaseFlux:
RETURN_TYPES = ("IMAGE",)
FUNCTION = "generate_image"
@@ -66,19 +76,32 @@ class BaseFlux:
print(f"[BFL] POST response: {response.status_code}")
if response.status_code == 200:
task_id = response.json().get("id")
print(f"[BFL] Task ID: {task_id}")
body = response.json()
print(f"[BFL] Task ID: {body.get('id')}")
if not body.get("id"):
return None
task_id = TaskId(body["id"])
task_id.polling_url = body.get("polling_url")
return task_id
else:
print(f"[BFL] Error initiating request: {response.status_code}, {response.text}")
return None
@staticmethod
def _reports_terminal_status(response):
try:
status = response.json().get("status")
except (ValueError, AttributeError):
return False
return status in (Status.ERROR.value, Status.CONTENT_MODERATED.value, Status.REQUEST_MODERATED.value)
def get_result(self, task_id, output_format="jpeg", max_attempts=40, config_override=None):
# Use ConfigLoader with optional config override
config_loader_instance = get_config_loader(config_override)
headers = {"x-key": config_loader_instance.get_x_key()}
get_url = config_loader_instance.create_url(f"get_result?id={task_id}")
# Build get_result?id= only for a plain id without a returned polling_url (e.g. a caller's own POST)
get_url = getattr(task_id, "polling_url", None) or config_loader_instance.create_url(f"get_result?id={task_id}")
attempt = 1
start_time = time.time()
print(f"[BFL] Polling task {task_id} (max {max_attempts} attempts, 5s interval)")
@@ -90,7 +113,9 @@ class BaseFlux:
result_response = requests.get(get_url, headers=headers, timeout=REQUEST_TIMEOUT)
print(f"[BFL] Poll response: {result_response.status_code}")
if result_response.status_code != 200:
# FLUX 3 reports some failed tasks as HTTP 503 with a normal JSON body: let those fall
# through to the terminal-status branch below instead of retrying until max_attempts
if result_response.status_code != 200 and not self._reports_terminal_status(result_response):
print(
f"[BFL] HTTP error on attempt {attempt}/{max_attempts}: "
f"{result_response.status_code}, {result_response.text}"
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "comfyui-flux-bfl-api"
description = "Custom nodes for integrating Flux models with the BFL API."
version = "1.5.0"
version = "1.7.0"
license = {file = "LICENSE"}
dependencies = ["torch"]
File diff suppressed because one or more lines are too long