Conversation
Mirrors the architecture established by the web (#25182), browser (#25214), and video_gen (#25126) plugin migrations: * `tools/fal_common.py` — stateless atoms shared by both FAL-backed plugins (image_gen + video_gen). Holds the lazy `fal_client` import helper, `_ManagedFalSyncClient`, `_normalize_fal_queue_url_format`, `_extract_http_status`. Stateful pieces (`fal_client` module global, `_managed_fal_client*` cache, `_submit_fal_request`, `_resolve_managed_fal_gateway`, `_get_managed_fal_client`) intentionally stay on `tools.image_generation_tool` so the existing `monkeypatch.setattr(image_tool, ...)` patch sites keep working unchanged. * `plugins/video_gen/fal/__init__.py` — drops its inline `_load_fal_client` duplicate; consumes `tools.fal_common.import_fal_client`. * `plugins/image_gen/fal/{plugin.yaml,__init__.py}` — new plugin. `FalImageGenProvider` is a thin registration adapter that resolves the legacy module via `import tools.image_generation_tool as _it` and calls `_it.image_generate_tool` + `_it._resolve_fal_model` at call time. The 18-model catalog, `_build_fal_payload`, managed- gateway selection, and Clarity Upscaler chaining all remain in `tools.image_generation_tool` as the single source of truth — the plugin is a registration adapter, not a parallel implementation. * `tools/image_generation_tool.py::_dispatch_to_plugin_provider` — drops the `configured == "fal"` skip. Setting `image_gen.provider: fal` now routes through the registry like any other provider; the plugin re-enters this module's pipeline so behavior is identical. Unset `image_gen.provider` still falls through to the in-tree pipeline (preserves no-config-with-FAL_KEY UX from #15696). * `hermes_cli/tools_config.py` — drops the hardcoded "FAL.ai" row from `TOOL_CATEGORIES["image_gen"]["providers"]` (now injected by `_plugin_image_gen_providers` like every other backend) and the `getattr(provider, "name") == "fal"` skip that protected against duplication with the hardcoded row. The "Nous Subscription" row stays as a setup-flow entry — same shape browser kept "Nous Subscription (Browser Use cloud)" after #25214. * `tests/plugins/image_gen/test_fal_provider.py` — 14 cases covering the ABC surface, call-time indirection (verifying `monkeypatch.setattr(image_tool, "image_generate_tool", ...)` takes effect through the plugin), response-shape stamping, exception handling, and registry wiring. * `tests/plugins/image_gen/check_parity_vs_main.py` — subprocess harness mirroring `tests/plugins/browser/check_parity_vs_main.py`. Pins one path to origin/main, one to the worktree; runs six scenarios (unset, explicit-fal-no-creds, explicit-fal-with-creds, explicit-fal-with-model, typo provider, managed-gateway-only) and diffs the reduced shape `{dispatch_kind, provider_name, model}` per scenario. The only acceptable diff is "legacy_fal → plugin (fal)" for explicit-FAL paths — every other delta is flagged as a regression. * `tests/hermes_cli/test_image_gen_picker.py::test_fal_surfaced_alongside_other_plugins` — flips the previous `test_fal_skipped_to_avoid_duplicate` to match the new shape (FAL is a plugin now, no dedup needed). Verified: 195/195 tests across `tests/{tools/test_image_generation*,tools/test_managed_media_gateways,plugins/image_gen,plugins/video_gen,hermes_cli/test_image_gen_picker}.py` pass on this branch with no test patches modified outside the picker test that asserted the old skip behaviour. Fixes #26241
Contributor
🔎 Lint report:
|
| Rule | Count |
|---|---|
unsupported-operator |
1 |
unresolved-import |
1 |
call-non-callable |
1 |
First entries
hermes_cli/tools_config.py:2654: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `dict[Unknown, Unknown]` and `str | list[dict[str, str | list[Unknown] | bool | list[str]] | dict[str, str | list[Unknown]] | dict[str, str | list[dict[str, str]]]] | list[dict[str, str | list[Unknown] | bool | list[str]] | dict[str, str | list[dict[str, str]]]] | ... omitted 5 union elements`
tests/plugins/image_gen/test_fal_provider.py:19: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tools/fal_common.py:157: [call-non-callable] call-non-callable: Object of type `None` is not callable
✅ Fixed issues (2):
| Rule | Count |
|---|---|
unsupported-operator |
1 |
call-non-callable |
1 |
First entries
hermes_cli/tools_config.py:2659: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `dict[Unknown, Unknown]` and `str | list[dict[str, str | list[Unknown] | bool | list[str]] | dict[str, str | list[Unknown]] | dict[str, str | list[dict[str, str]]]] | list[dict[str, str | list[Unknown] | bool | list[str]] | dict[str, str | list[dict[str, str]]]] | ... omitted 4 union elements`
tools/image_generation_tool.py:443: [call-non-callable] call-non-callable: Object of type `None` is not callable
Unchanged: 4761 pre-existing issues carried over.
Diagnostics are surfaced as warnings — this check never fails the build.
7 tasks
1 task
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Salvages PR #27966 onto current
main. Ports the FAL.ai image-gen backend out oftools/image_generation_tool.pyintoplugins/image_gen/fal/, completing the four-way architectural parity established by web (#25182), browser (#25214), and video_gen (#25126).TOOL_CATEGORIES["image_gen"]["providers"]now carries only the "Nous Subscription" UX row — same shape browser/video_gen ship today.Original PR was 475 commits behind main; cherry-pick was clean with one trivial auto-merge in
hermes_cli/tools_config.py. Author@0xDevNinjapreserved via rebase-merge.Closes #26241.
What changed
New
tools/fal_common.py— stateless atoms shared by both FAL-backed plugins (lazyfal_clientimport,_ManagedFalSyncClient,_normalize_fal_queue_url_format,_extract_http_status).plugins/image_gen/fal/{__init__.py,plugin.yaml}—FalImageGenProviderthin registration adapter. Resolvestools.image_generation_toolat call time via_it = import tools.image_generation_toolso existingmonkeypatch.setattr(image_tool, ...)patch sites intests/tools/test_image_generation*.pyandtests/tools/test_managed_media_gateways.pykeep working unchanged.tests/plugins/image_gen/test_fal_provider.py— 14 cases covering ABC surface, call-time indirection, response-shape stamping, exception handling, registry wiring.tests/plugins/image_gen/check_parity_vs_main.py— subprocess parity harness mirroringtests/plugins/browser/check_parity_vs_main.py. Six scenarios; only "legacy_fal → plugin (fal)" deltas are acceptable.Changed
tools/image_generation_tool.py— drops theconfigured == "fal"skip in_dispatch_to_plugin_provider. Settingimage_gen.provider: falnow routes through the registry like any other provider; the plugin re-enters this module's pipeline so behaviour is identical. Unsetimage_gen.providerstill falls through (preserves no-config-with-FAL_KEYUX from refactor(memory): remove flush_memories entirely #15696). Stateful pieces (fal_clientmodule global,_managed_fal_client*cache,_submit_fal_request,_resolve_managed_fal_gateway,_get_managed_fal_client) intentionally stay here so the existingmonkeypatch.setattr(image_tool, ...)test surface is preserved.hermes_cli/tools_config.py— drops the hardcoded "FAL.ai" row and theprovider.name == "fal"skip in_plugin_image_gen_providers. The "Nous Subscription" setup-flow row stays.plugins/video_gen/fal/__init__.py— drops its inline_load_fal_clientduplicate; consumestools.fal_common.import_fal_client.Validation
TOOL_CATEGORIES["image_gen"]["providers"]_dispatch_to_plugin_providerforprovider: fal_plugin_image_gen_providersimage_tool.<name>195/195 tests pass on the salvaged commit:
E2E sanity (smoke import + plugin ABC surface + registry wiring + dispatcher patch verification) confirms the
configured == "fal"skip is gone, the fal_common imports are wired, andregister()correctly registersFalImageGenProviderthrough the plugin context.Credit
@0xDevNinja did all the substantive work — design, implementation, tests, and the parity harness. This salvage just rebases their commit onto current
main(475 commits ahead of their branch base).Infographic