Files
Gökay Aydoğan 647a38f265 Fix schema controls and prevent registry regressions across all models (#85)
* fix: preserve and validate model controls across the fal catalog

* fix: preserve enum constraints through nested normalization
2026-09-17 19:11:50 +03:00

7.7 KiB

Contributing to ComfyUI-fal-API

Thanks for helping! Before you write anything, read this — it will probably save you the PR entirely.

A new fal model does NOT need code

Historically, adding a model to this pack meant hand-writing a node class. That is no longer how it works. Every live public model on fal gets a node automatically, generated at ComfyUI startup from the committed snapshot at data/fal_registry.json. No node class, no mapping entry, no code.

The snapshot stays fresh three ways:

  • A daily GitHub Action (.github/workflows/registry-refresh.yml) builds a candidate, validates it against the committed baseline, and commits safe changes automatically.
  • Anyone can run it locally: python scripts/build_registry.py --out data/fal_registry.json (then python scripts/build_readme.py to regenerate MODELS.md).
  • The fal sidebar can rebuild the local registry; restart ComfyUI afterward so new node classes register.

The automated validator rejects malformed records, duplicate or unsorted endpoint IDs, suspiciously small catalogs, and unexpectedly large additions or removals. A maintainer can override the change-size thresholds when manually dispatching the workflow; all structural checks still apply.

Refresh validation also rejects removed input controls, lost dropdowns, and removed enum choices on existing endpoints. Inspect these changes against the live API before using the separate allow_input_removal workflow option (or validate_registry.py --allow-input-removal). The sidebar builds and validates a candidate before replacing the local snapshot, so a failed refresh keeps the previous registry intact.

The generator handles nested references, composed schemas, nullable types and literal choices for every endpoint, and never caps the number of exposed fields. Short string examples become suggested dropdown choices with a custom value input; they are not treated as exhaustive enums. Prompts, prose and URLs remain text/media controls. Fix schema patterns in the shared generator and widget/argument translators rather than adding model-specific patches. Add upstream schema fixtures and regressions in tests/test_build_registry.py; H3 and the catalog's suggested controls are checked against the shipped registry too.

Missing endpoints are preserved by default with deprecated: true, which keeps their node keys available under FAL/Compatibility for old workflows. Use --prune-missing only for an intentional breaking cleanup. A fresh endpoint record automatically replaces its deprecated copy if it returns to the live catalog.

To check whether a model is already covered:

grep '"endpoint_id": "fal-ai/your/endpoint"' data/fal_registry.json
# or browse MODELS.md, or search the node browser in ComfyUI

If a model is live on fal.ai/models but missing from the snapshot, rerun scripts/build_registry.py — if it's still missing, open an issue with the endpoint id. And if you need a model right now, the Fal Any Endpoint (fal) node calls any endpoint by id without any registry entry at all.

So: please don't open a PR that adds a node class for a new model. It will be redundant the moment the registry refreshes.

Want a model promoted or renamed? Edit data/featured_models.json

When a model deserves curation — a spot in the FAL/Featured menu tier or a friendlier display name — add its endpoint to data/featured_models.json (featured tier + display-name override). That's the whole change: one JSON entry, not a new node class.

When a hand-written node IS justified

A curated node earns its place only when the generated node genuinely can't express the UX:

  • Multi-endpoint orchestration — one node fanning out to several endpoints (e.g. Combined Video Generation).
  • Special input ergonomics — first/last-frame image pairing, unified T2V/I2V dispatch, LoRA slots with per-slot scales.

If you're writing one, the rules are non-negotiable:

  1. Import only from the .fal_utils facade (from .fal_utils import ApiHandler, FalConfig, ImageUtils, ResultProcessor, ...) — never reach into nodes/utils/ internals or call fal_client directly. The facade gives you the result cache, spend guard, session ledger, and error handling for free.
  2. Raise errors — no silent fallbacks. Never return blank images or "Error: ..." strings; let ApiHandler surface fal's actual error message.
  3. Tooltips on every input. Users should never have to guess a parameter.
  4. Never change existing node keys, input names, or output signatures. Existing user workflows reference them forever. tests/legacy_node_keys.json is the snapshot of keys that must never be removed or renamed, and tests/test_mappings.py fails the suite if one disappears. New inputs must be optional with backward-compatible defaults.
  5. Add tests alongside the existing ones in tests/.

Dev setup

pip install -r requirements.txt
python -m pytest tests     # the suite MUST pass
ruff check .               # lint, same as CI
node --experimental-vm-modules --test tests/test_*.mjs

CI runs both on every PR (Python 3.10 and 3.12). The most important test to understand is the compatibility snapshot: tests/test_mappings.py asserts that every node key recorded in tests/legacy_node_keys.json still registers. If your change makes it fail, the fix is to restore the key — not to edit the snapshot.

Architecture map

scripts/build_registry.py     queries fal's platform APIs → writes the snapshot
scripts/validate_registry.py  safety gate for generated registry candidates
data/fal_registry.json        committed model catalog (~1,400 models)
data/featured_models.json     curation: featured tier + display-name overrides
scripts/build_readme.py       renders MODELS.md from the snapshot

nodes/dynamic/                the auto-generated node machinery
  registry_loader.py          reads the snapshot, applies [dynamic_nodes] config;
                              never raises — failures degrade to curated-only
  factory.py                  builds one node class per model, in memory
  schema_to_inputs.py         registry input specs → ComfyUI INPUT_TYPES (+ tooltips)
  arguments.py                widget/socket values → API arguments (uploads media)
  outputs.py                  API result → IMAGE / VIDEO / AUDIO / result_json
  any_endpoint.py             the generic "call any endpoint by id" node

nodes/*.py                    curated hand-written nodes (image, video, llm, vlm,
                              trainer, upscaler, util_*)
nodes/fal_utils.py            import facade — node modules import ONLY from here
nodes/utils/                  the implementations behind the facade: api, config,
                              pricing, result_cache, ledger, billing (spend guard),
                              job_store, media, archive, errors, logger
nodes/platform_node.py        platform nodes (Submit/Collect, costs, request ids)
nodes/inbox_node.py           durable job inbox
nodes/billing_node.py         account balance
nodes/server_routes.py        HTTP endpoints backing the frontend extension
web/                          ComfyUI frontend: cost badges, fal sidebar,
                              endpoint autocomplete
tests/                        pytest suite, incl. the legacy_node_keys.json snapshot

PR checklist

  • Not a hand-written node for a single new model (registry covers it — see above)
  • python -m pytest tests passes locally
  • ruff check . is clean
  • No existing node keys, inputs, or outputs changed
  • New curated node (if truly justified): uses .fal_utils, raises errors, has tooltips and tests
  • No secrets, no config.ini, no generated artifacts in the diff