From f2a1c5edde7563e2d3242c8da37a9b8cf8af3cdc Mon Sep 17 00:00:00 2001 From: Ken Simpson Date: Sun, 23 Aug 2026 15:36:08 -0700 Subject: [PATCH] Fix Nodes 2.0 provider policy menu --- README.md | 2 ++ docs/LEARNINGS.md | 1 + tests/modal_toggle_frontend.test.mjs | 44 ++++++++++++++++++++++++---- web/modal_toggle.js | 36 +++++++++++------------ 4 files changed, 59 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index d1703bf..6eb561e 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,8 @@ Provider policy is stored under `workflow.extra.remote_execution` and travels wi - **Automatic** — considers ready SSH hosts, the selected Modal GPU, and any workflow-declared Vast pools; rejects hard incompatibilities (insufficient VRAM, provider-specific requirements), then prefers the lowest predicted incremental cost with deterministic tie-breaking. - **Automatically place eligible nodes** — lets the planner select remote nodes itself instead of requiring per-node toggles. Leave it off to keep manual component boundaries. +To change the policy from a node, right-click an eligible node and open **Remote Execution**. ComfyUI Nodes 2.0 lists **Modal only**, **Self-hosted only**, **Vast.ai only**, and **Automatic** directly below the disabled **Provider policy (current choice)** heading because its extension-menu adapter supports only one submenu level. + ### Modal Authentication is user-managed — run Modal's setup once if the current user does not already have working credentials: diff --git a/docs/LEARNINGS.md b/docs/LEARNINGS.md index 87b7a0c..c2e6b47 100644 --- a/docs/LEARNINGS.md +++ b/docs/LEARNINGS.md @@ -491,6 +491,7 @@ - Nodes 2.0 node bodies can cover legacy canvas foreground drawings, so execution-location UI needs a DOM-layer equivalent rather than relying only on `onDrawForeground`. Attach the exact runtime identity to streamed status events as well as numeric progress; otherwise loader and other non-numeric phases know the active node but cannot display its Modal task ID. ## 2026-08-23: Vast instances need a direct-container backend +- ComfyUI Nodes 2.0 converts legacy extension context menus into a Vue menu model whose submenu entries are action-only leaves. A third menu level is silently discarded, so extension-owned provider and GPU selectors must put their choices directly in the first submenu and use a disabled heading to show the current selection. - Vast worker images must be built explicitly for `linux/amd64`. Without Docker's `--platform linux/amd64`, an Apple Silicon host selects arm64 base-image stages and rejects the intentionally x86_64 CUDA/vLLM wheels before the image can be pushed. - Provider-aware DAG cuts are valid only for serializable or safely reproducible edges. For shared `MODEL`, `CLIP`, `VAE`, `NOISE`, and `SAMPLER` outputs around an SSH-only LLM phase, clone a non-sampling loader dependency closure into each downstream component and rewire it before final partitioning. Fall back to hard affinity only when the producer cannot be reproduced safely; otherwise one GGUF node can pull an unrelated 61.73 GiB Comfy model onto a 52.64 GiB-RAM SSH host. - A raw Hugging Face repository ID is valid only for the Transformers-compatible model resolver, which requires `config.json` and safetensors. GGUF repositories must be selected through a curated profile that pins the exact weight, optional projector, tokenizer source, and `llama.cpp` backend; otherwise prompt validation fails before provider placement. diff --git a/tests/modal_toggle_frontend.test.mjs b/tests/modal_toggle_frontend.test.mjs index 303e206..abeda64 100644 --- a/tests/modal_toggle_frontend.test.mjs +++ b/tests/modal_toggle_frontend.test.mjs @@ -1191,16 +1191,48 @@ workflowGraph.nodes.push(menuNode); modalToggle.installModalContextMenu(MenuNode, { name: "KSampler" }); const menuOptions = []; menuNode.getExtraMenuOptions(null, menuOptions); -const modalMenu = menuOptions.find((option) => option?.content === "Modal"); -const gpuMenu = modalMenu.submenu.options.find((option) => option?.content === "GPU"); +const remoteExecutionMenu = menuOptions.find( + (option) => option?.content === "Remote Execution", +); +const providerPolicyHeading = remoteExecutionMenu.submenu.options.find( + (option) => option?.content === "Provider policy (Modal only)", +); +assert.equal(providerPolicyHeading.disabled, true); +assert.equal(providerPolicyHeading.has_submenu, undefined); +const providerPolicyHeadingIndex = remoteExecutionMenu.submenu.options.indexOf( + providerPolicyHeading, +); assert.deepEqual( - gpuMenu.submenu.options.map((option) => option.content), + remoteExecutionMenu.submenu.options + .slice(providerPolicyHeadingIndex + 1, providerPolicyHeadingIndex + 5) + .map((option) => option.content), + ["Modal only", "Self-hosted only", "Vast.ai only", "Automatic (lowest cost compatible)"], +); +const vastPolicyOption = remoteExecutionMenu.submenu.options.find( + (option) => option?.content === "Vast.ai only", +); +assert.equal(vastPolicyOption.checked, false); +assert.equal(vastPolicyOption.submenu, undefined); +vastPolicyOption.callback(); +assert.equal(workflowGraph.extra.remote_execution.policy, "vast"); +assert.equal(workflowGraph.changeCount, 2); + +const modalMenu = menuOptions.find((option) => option?.content === "Modal"); +const gpuHeading = modalMenu.submenu.options.find( + (option) => option?.content === "GPU (B300)", +); +assert.equal(gpuHeading.disabled, true); +assert.equal(gpuHeading.has_submenu, undefined); +assert.deepEqual( + modalMenu.submenu.options + .filter((option) => modalToggle.MODAL_GPU_TYPES.includes(option?.content)) + .map((option) => option.content), modalToggle.MODAL_GPU_TYPES, ); assert.equal( - gpuMenu.submenu.options.find((option) => option.content === "B300")?.checked, + modalMenu.submenu.options.find((option) => option?.content === "B300")?.checked, true, ); -gpuMenu.submenu.options.find((option) => option.content === "L40S").callback(); +modalMenu.submenu.options.find((option) => option?.content === "L40S").callback(); assert.equal(workflowGraph.extra.comfy_modal.gpu, "L40S"); -assert.equal(workflowGraph.changeCount, 2); +assert.equal(workflowGraph.changeCount, 3); diff --git a/web/modal_toggle.js b/web/modal_toggle.js index cd0d1a6..213643d 100644 --- a/web/modal_toggle.js +++ b/web/modal_toggle.js @@ -3401,22 +3401,24 @@ function installModalContextMenu(nodeType, nodeData) { : "Disable on Upstream Nodes"; if (!targetOptions.some((option) => option?.content === "Remote Execution")) { const executionPreferences = selectedExecutionPreferences(); + const executionPolicyLabel = + EXECUTION_POLICIES.find(([value]) => value === executionPreferences.policy)?.[1] + ?? executionPreferences.policy; targetOptions.push(null, { content: "Remote Execution", has_submenu: true, submenu: { options: [ { - content: "Provider policy", - has_submenu: true, - submenu: { - options: EXECUTION_POLICIES.map(([value, label]) => ({ - content: label, - checked: executionPreferences.policy === value, - callback: () => setExecutionPreferences({ policy: value }), - })), - }, + content: `Provider policy (${executionPolicyLabel})`, + disabled: true, }, + ...EXECUTION_POLICIES.map(([value, label]) => ({ + content: label, + checked: executionPreferences.policy === value, + callback: () => setExecutionPreferences({ policy: value }), + })), + null, { content: "Automatically place eligible nodes", checked: executionPreferences.autoPlace, @@ -3446,16 +3448,14 @@ function installModalContextMenu(nodeType, nodeData) { submenu: { options: [ { - content: "GPU", - has_submenu: true, - submenu: { - options: MODAL_GPU_TYPES.map((modalGpu) => ({ - content: modalGpu, - checked: modalGpu === currentModalGpu, - callback: () => setSelectedModalGpu(modalGpu), - })), - }, + content: `GPU (${currentModalGpu})`, + disabled: true, }, + ...MODAL_GPU_TYPES.map((modalGpu) => ({ + content: modalGpu, + checked: modalGpu === currentModalGpu, + callback: () => setSelectedModalGpu(modalGpu), + })), null, { content: enableUpstreamMenuItemLabel,