From e2d1879cc0e9394257857dd0425b83dd8713dedd Mon Sep 17 00:00:00 2001 From: Ni-zav Date: Fri, 27 Feb 2026 02:41:50 +0700 Subject: [PATCH 1/5] fix(api-io): serialize MultiCombo multi_select as object config --- comfy_api/latest/_io.py | 11 +++--- .../multicombo_serialization_test.py | 35 +++++++++++++++++++ 2 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 tests-unit/comfy_api_test/multicombo_serialization_test.py diff --git a/comfy_api/latest/_io.py b/comfy_api/latest/_io.py index fdeffea2d..823874bde 100644 --- a/comfy_api/latest/_io.py +++ b/comfy_api/latest/_io.py @@ -395,7 +395,6 @@ class Combo(ComfyTypeIO): @comfytype(io_type="COMBO") class MultiCombo(ComfyTypeI): '''Multiselect Combo input (dropdown for selecting potentially more than one value).''' - # TODO: something is wrong with the serialization, frontend does not recognize it as multiselect Type = list[str] class Input(Combo.Input): def __init__(self, id: str, options: list[str], display_name: str=None, optional=False, tooltip: str=None, lazy: bool=None, @@ -408,12 +407,16 @@ class MultiCombo(ComfyTypeI): self.default: list[str] def as_dict(self): - to_return = super().as_dict() | prune_dict({ - "multi_select": self.multiselect, + # Frontend expects `multi_select` to be an object config (not a boolean). + # Keep top-level `multiselect` from Combo.Input for backwards compatibility. + return super().as_dict() | prune_dict({ + "multi_select": prune_dict({ + "placeholder": self.placeholder, + "chip": self.chip, + }) if self.multiselect else None, "placeholder": self.placeholder, "chip": self.chip, }) - return to_return @comfytype(io_type="IMAGE") class Image(ComfyTypeIO): diff --git a/tests-unit/comfy_api_test/multicombo_serialization_test.py b/tests-unit/comfy_api_test/multicombo_serialization_test.py new file mode 100644 index 000000000..9e4fecdd0 --- /dev/null +++ b/tests-unit/comfy_api_test/multicombo_serialization_test.py @@ -0,0 +1,35 @@ +from comfy_api.latest._io import MultiCombo + + +def test_multicombo_serializes_multi_select_as_object(): + multi_combo = MultiCombo.Input( + id="providers", + options=["a", "b", "c"], + default=["a"], + ) + + serialized = multi_combo.as_dict() + + assert serialized["multiselect"] is True + assert "multi_select" in serialized + assert serialized["multi_select"] == {} + + +def test_multicombo_serializes_multi_select_with_placeholder_and_chip(): + multi_combo = MultiCombo.Input( + id="providers", + options=["a", "b", "c"], + default=["a"], + placeholder="Select providers", + chip=True, + ) + + serialized = multi_combo.as_dict() + + assert serialized["multiselect"] is True + assert serialized["multi_select"] == { + "placeholder": "Select providers", + "chip": True, + } + assert serialized["placeholder"] == "Select providers" + assert serialized["chip"] is True From 3db5aa001a27b48ce8011197ccec3f09131ae0e5 Mon Sep 17 00:00:00 2001 From: Jedrzej Kosinski Date: Mon, 20 Apr 2026 15:41:57 -0700 Subject: [PATCH 2/5] fix: remove dead code and redundant top-level keys from MultiCombo serialization Amp-Thread-ID: https://ampcode.com/threads/T-019dad04-a07a-724b-af4d-fbfe98654fdd Co-authored-by: Amp --- comfy_api/latest/_io.py | 4 +--- tests-unit/comfy_api_test/multicombo_serialization_test.py | 2 -- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/comfy_api/latest/_io.py b/comfy_api/latest/_io.py index 823874bde..4fd43008d 100644 --- a/comfy_api/latest/_io.py +++ b/comfy_api/latest/_io.py @@ -413,9 +413,7 @@ class MultiCombo(ComfyTypeI): "multi_select": prune_dict({ "placeholder": self.placeholder, "chip": self.chip, - }) if self.multiselect else None, - "placeholder": self.placeholder, - "chip": self.chip, + }), }) @comfytype(io_type="IMAGE") diff --git a/tests-unit/comfy_api_test/multicombo_serialization_test.py b/tests-unit/comfy_api_test/multicombo_serialization_test.py index 9e4fecdd0..df925ad19 100644 --- a/tests-unit/comfy_api_test/multicombo_serialization_test.py +++ b/tests-unit/comfy_api_test/multicombo_serialization_test.py @@ -31,5 +31,3 @@ def test_multicombo_serializes_multi_select_with_placeholder_and_chip(): "placeholder": "Select providers", "chip": True, } - assert serialized["placeholder"] == "Select providers" - assert serialized["chip"] is True From 15e097411b66d533f9c34f0b4cc71544f8962761 Mon Sep 17 00:00:00 2001 From: Jedrzej Kosinski Date: Mon, 20 Apr 2026 15:54:16 -0700 Subject: [PATCH 3/5] fix: correct skip warning to mention comfy_entrypoint, remove nonexistent NODES_LIST Amp-Thread-ID: https://ampcode.com/threads/T-019dad04-a07a-724b-af4d-fbfe98654fdd Co-authored-by: Amp --- nodes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nodes.py b/nodes.py index 299b3d758..e86545d94 100644 --- a/nodes.py +++ b/nodes.py @@ -2293,7 +2293,7 @@ async def load_custom_node(module_path: str, ignore=set(), module_parent="custom logging.warning(f"Error while calling comfy_entrypoint in {module_path}: {e}") return False else: - logging.warning(f"Skip {module_path} module for custom nodes due to the lack of NODE_CLASS_MAPPINGS or NODES_LIST (need one).") + logging.warning(f"Skip {module_path} module for custom nodes due to the lack of NODE_CLASS_MAPPINGS or comfy_entrypoint (need one).") return False except Exception as e: logging.warning(traceback.format_exc()) From fc51d13bebdc11b64c0d377b2a54ba7d0ac77585 Mon Sep 17 00:00:00 2001 From: Jedrzej Kosinski Date: Mon, 20 Apr 2026 16:04:07 -0700 Subject: [PATCH 4/5] fix: validate MultiCombo list values against options individually Amp-Thread-ID: https://ampcode.com/threads/T-019dad04-a07a-724b-af4d-fbfe98654fdd Co-authored-by: Amp --- execution.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/execution.py b/execution.py index 5e02dffb2..8bce9920c 100644 --- a/execution.py +++ b/execution.py @@ -994,7 +994,12 @@ async def validate_inputs(prompt_id, prompt, item, validated): combo_options = extra_info.get("options", []) else: combo_options = input_type - if val not in combo_options: + # MultiCombo sends a list of selected values + if isinstance(val, list): + invalid_vals = [v for v in val if v not in combo_options] + else: + invalid_vals = [val] if val not in combo_options else [] + if invalid_vals: input_config = info list_info = "" @@ -1009,7 +1014,7 @@ async def validate_inputs(prompt_id, prompt, item, validated): error = { "type": "value_not_in_list", "message": "Value not in list", - "details": f"{x}: '{val}' not in {list_info}", + "details": f"{x}: '{invalid_vals}' not in {list_info}", "extra_info": { "input_name": x, "input_config": input_config, From 711907e6ec3db219bd2bebb3272ca3382c01467f Mon Sep 17 00:00:00 2001 From: Jedrzej Kosinski Date: Mon, 20 Apr 2026 19:53:32 -0700 Subject: [PATCH 5/5] fix: gate multiselect validation on schema config, improve error message, add tests Amp-Thread-ID: https://ampcode.com/threads/T-019dad04-a07a-724b-af4d-fbfe98654fdd Co-authored-by: Amp --- execution.py | 6 +-- .../multicombo_serialization_test.py | 47 ++++++++++++++++++- 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/execution.py b/execution.py index 8bce9920c..a520c009d 100644 --- a/execution.py +++ b/execution.py @@ -994,8 +994,8 @@ async def validate_inputs(prompt_id, prompt, item, validated): combo_options = extra_info.get("options", []) else: combo_options = input_type - # MultiCombo sends a list of selected values - if isinstance(val, list): + is_multiselect = extra_info.get("multiselect", False) + if is_multiselect and isinstance(val, list): invalid_vals = [v for v in val if v not in combo_options] else: invalid_vals = [val] if val not in combo_options else [] @@ -1014,7 +1014,7 @@ async def validate_inputs(prompt_id, prompt, item, validated): error = { "type": "value_not_in_list", "message": "Value not in list", - "details": f"{x}: '{invalid_vals}' not in {list_info}", + "details": f"{x}: {', '.join(repr(v) for v in invalid_vals)} not in {list_info}", "extra_info": { "input_name": x, "input_config": input_config, diff --git a/tests-unit/comfy_api_test/multicombo_serialization_test.py b/tests-unit/comfy_api_test/multicombo_serialization_test.py index df925ad19..421c65a0d 100644 --- a/tests-unit/comfy_api_test/multicombo_serialization_test.py +++ b/tests-unit/comfy_api_test/multicombo_serialization_test.py @@ -1,4 +1,4 @@ -from comfy_api.latest._io import MultiCombo +from comfy_api.latest._io import Combo, MultiCombo def test_multicombo_serializes_multi_select_as_object(): @@ -31,3 +31,48 @@ def test_multicombo_serializes_multi_select_with_placeholder_and_chip(): "placeholder": "Select providers", "chip": True, } + + +def test_combo_does_not_serialize_multiselect(): + """Regular Combo should not have multiselect in its serialized output.""" + combo = Combo.Input( + id="choice", + options=["a", "b", "c"], + ) + + serialized = combo.as_dict() + + # Combo sets multiselect=False, but prune_dict keeps False (not None), + # so it should be present but False + assert serialized.get("multiselect") is False + assert "multi_select" not in serialized + + +def _validate_combo_values(val, combo_options, is_multiselect): + """Reproduce the validation logic from execution.py for testing.""" + if is_multiselect and isinstance(val, list): + return [v for v in val if v not in combo_options] + else: + return [val] if val not in combo_options else [] + + +def test_multicombo_validation_accepts_valid_list(): + options = ["a", "b", "c"] + assert _validate_combo_values(["a", "b"], options, True) == [] + + +def test_multicombo_validation_rejects_invalid_values(): + options = ["a", "b", "c"] + assert _validate_combo_values(["a", "x"], options, True) == ["x"] + + +def test_multicombo_validation_accepts_empty_list(): + options = ["a", "b", "c"] + assert _validate_combo_values([], options, True) == [] + + +def test_combo_validation_rejects_list_even_with_valid_items(): + """A regular Combo should not accept a list value.""" + options = ["a", "b", "c"] + invalid = _validate_combo_values(["a", "b"], options, False) + assert len(invalid) > 0