Files
ttulttul-ComfyUI-Modal/api_intercept.py
T

860 lines
33 KiB
Python

"""Prompt interception and graph rewriting for Modal-backed execution."""
from __future__ import annotations
import copy
import logging
import time
import uuid
from collections import defaultdict
from dataclasses import dataclass, field
from typing import Any
from aiohttp import web
from .modal_executor_node import ensure_modal_component_proxy_node_registered
from .settings import ModalSyncSettings, get_settings
from .sync_engine import ModalAssetSyncEngine, SyncedAsset
logger = logging.getLogger(__name__)
_ROUTE_REGISTERED = False
_TRANSPORTABLE_OUTPUT_TYPES = frozenset(
{
"*",
"BOOLEAN",
"FLOAT",
"IMAGE",
"INT",
"LATENT",
"MASK",
"NOISE",
"SIGMAS",
"STRING",
}
)
@dataclass(frozen=True)
class LinkedOutputRef:
"""Reference a node output slot within a prompt graph."""
node_id: str
output_index: int
@dataclass(frozen=True)
class InputTarget:
"""Describe a target input inside a remote component."""
node_id: str
input_name: str
@dataclass
class BoundaryInputSpec:
"""Describe one local-to-remote boundary value for a component."""
proxy_input_name: str
source: LinkedOutputRef
targets: list[InputTarget] = field(default_factory=list)
@dataclass
class BoundaryOutputSpec:
"""Describe one remote-to-local boundary value for a component."""
proxy_output_name: str
source: LinkedOutputRef
io_type: str
is_list: bool
@dataclass
class RemoteComponentPlan:
"""Execution and rewrite plan for one connected remote component."""
node_ids: list[str]
representative_node_id: str
boundary_inputs: list[BoundaryInputSpec]
boundary_outputs: list[BoundaryOutputSpec]
execute_node_ids: list[str]
contains_output_node: bool
@dataclass
class RewriteSummary:
"""Summary of the prompt rewrite performed for a queue request."""
remote_node_ids: list[str] = field(default_factory=list)
remote_component_ids: list[str] = field(default_factory=list)
component_node_ids_by_representative: dict[str, list[str]] = field(default_factory=dict)
rewritten_node_id_map: dict[str, str] = field(default_factory=dict)
synced_assets: list[SyncedAsset] = field(default_factory=list)
custom_nodes_bundle: SyncedAsset | None = None
class ModalPromptValidationError(ValueError):
"""Raised when a prompt cannot be executed with the current Modal transport."""
def _emit_modal_status(
prompt_server: Any,
phase: str,
*,
client_id: str | None,
prompt_id: str | None,
node_ids: list[str],
component_node_ids_by_representative: dict[str, list[str]] | None = None,
error_message: str | None = None,
) -> None:
"""Send a Modal execution status event to the active websocket client."""
if client_id is None:
logger.debug("Skipping Modal status event %s because no client id is available.", phase)
return
payload: dict[str, Any] = {
"phase": phase,
"prompt_id": prompt_id,
"node_ids": list(node_ids),
}
if component_node_ids_by_representative:
payload["components"] = [
{
"representative_node_id": representative_node_id,
"node_ids": list(component_node_ids),
}
for representative_node_id, component_node_ids in sorted(
component_node_ids_by_representative.items()
)
]
if error_message is not None:
payload["error_message"] = error_message
prompt_server.send_sync("modal_status", payload, client_id)
def _get_nodes_module() -> Any:
"""Import the ComfyUI nodes module lazily."""
import nodes
return nodes
def _get_server_module() -> Any:
"""Import the ComfyUI server module lazily."""
import server
return server
def _get_execution_module() -> Any:
"""Import the ComfyUI execution module lazily."""
import execution
return execution
def _is_link(value: Any) -> bool:
"""Return whether a prompt input value is a ComfyUI link."""
return (
isinstance(value, list)
and len(value) == 2
and all(not isinstance(item, dict) for item in value)
)
def extract_remote_node_ids(
workflow: dict[str, Any] | None,
settings: ModalSyncSettings | None = None,
) -> set[str]:
"""Return the node ids marked for remote execution in the workflow metadata."""
if workflow is None:
return set()
marker = (settings or get_settings()).marker_property
remote_node_ids: set[str] = set()
for node in workflow.get("nodes", []):
properties = node.get("properties") or {}
if properties.get(marker):
remote_node_ids.add(str(node.get("id")))
return remote_node_ids
def _normalize_output_metadata(node_class: type[Any]) -> tuple[tuple[str, ...], tuple[str, ...], tuple[bool, ...]]:
"""Return normalized output metadata for a node class."""
if hasattr(node_class, "GET_SCHEMA"):
node_class.GET_SCHEMA()
output_types = tuple(getattr(node_class, "RETURN_TYPES", ("*",))) or ("*",)
default_names = tuple(f"output_{index}" for index, _ in enumerate(output_types))
output_names = tuple(getattr(node_class, "RETURN_NAMES", default_names))
output_is_list = tuple(getattr(node_class, "OUTPUT_IS_LIST", (False,) * len(output_types)))
if len(output_names) < len(output_types):
output_names = output_names + default_names[len(output_names) :]
if len(output_is_list) < len(output_types):
output_is_list = output_is_list + (False,) * (len(output_types) - len(output_is_list))
return output_types, output_names[: len(output_types)], output_is_list[: len(output_types)]
def _is_transportable_output_type(io_type: str) -> bool:
"""Return whether a ComfyUI output type can cross the current transport."""
normalized_parts = [part.strip() for part in io_type.split(",") if part.strip()]
return bool(normalized_parts) and all(part in _TRANSPORTABLE_OUTPUT_TYPES for part in normalized_parts)
def _build_consumer_map(prompt: dict[str, Any]) -> dict[LinkedOutputRef, list[InputTarget]]:
"""Build a reverse map from node outputs to downstream prompt inputs."""
consumers: dict[LinkedOutputRef, list[InputTarget]] = defaultdict(list)
for node_id, prompt_node in prompt.items():
for input_name, input_value in (prompt_node.get("inputs") or {}).items():
if not _is_link(input_value):
continue
source = LinkedOutputRef(node_id=str(input_value[0]), output_index=int(input_value[1]))
consumers[source].append(InputTarget(node_id=str(node_id), input_name=str(input_name)))
return consumers
def _build_remote_components(prompt: dict[str, Any], remote_node_ids: set[str]) -> list[list[str]]:
"""Partition remote-marked nodes into connected components."""
adjacency: dict[str, set[str]] = {node_id: set() for node_id in remote_node_ids}
for node_id in remote_node_ids:
prompt_node = prompt.get(node_id)
if prompt_node is None:
continue
for input_value in (prompt_node.get("inputs") or {}).values():
if not _is_link(input_value):
continue
upstream_node_id = str(input_value[0])
if upstream_node_id not in remote_node_ids:
continue
adjacency[node_id].add(upstream_node_id)
adjacency[upstream_node_id].add(node_id)
components: list[list[str]] = []
visited: set[str] = set()
for node_id in sorted(remote_node_ids):
if node_id in visited:
continue
pending = [node_id]
component: list[str] = []
while pending:
current = pending.pop()
if current in visited:
continue
visited.add(current)
component.append(current)
pending.extend(sorted(adjacency[current] - visited))
components.append(sorted(component))
logger.info(
"Partitioned %d remote nodes into %d remote components: %s",
len(remote_node_ids),
len(components),
components,
)
return components
def _build_component_plan(
component_node_ids: list[str],
prompt: dict[str, Any],
consumers: dict[LinkedOutputRef, list[InputTarget]],
nodes_module: Any,
) -> RemoteComponentPlan:
"""Build rewrite metadata for a connected remote component."""
component_node_id_set = set(component_node_ids)
representative_node_id = component_node_ids[0]
boundary_inputs_by_source: dict[LinkedOutputRef, BoundaryInputSpec] = {}
boundary_outputs_by_source: dict[LinkedOutputRef, BoundaryOutputSpec] = {}
output_execution_targets: set[str] = set()
contains_output_node = False
for node_id in component_node_ids:
prompt_node = prompt[node_id]
class_type = str(prompt_node["class_type"])
node_class = nodes_module.NODE_CLASS_MAPPINGS[class_type]
output_types, output_names, output_is_list = _normalize_output_metadata(node_class)
if getattr(node_class, "OUTPUT_NODE", False):
contains_output_node = True
output_execution_targets.add(node_id)
for input_name, input_value in (prompt_node.get("inputs") or {}).items():
if not _is_link(input_value):
continue
upstream_node_id = str(input_value[0])
if upstream_node_id in component_node_id_set:
continue
source = LinkedOutputRef(node_id=upstream_node_id, output_index=int(input_value[1]))
spec = boundary_inputs_by_source.get(source)
if spec is None:
spec = BoundaryInputSpec(
proxy_input_name=f"remote_input_{len(boundary_inputs_by_source)}",
source=source,
)
boundary_inputs_by_source[source] = spec
spec.targets.append(InputTarget(node_id=node_id, input_name=str(input_name)))
for output_index, io_type in enumerate(output_types):
source = LinkedOutputRef(node_id=node_id, output_index=output_index)
local_consumers = [
consumer for consumer in consumers.get(source, []) if consumer.node_id not in component_node_id_set
]
if not local_consumers:
continue
output_execution_targets.add(node_id)
if source in boundary_outputs_by_source:
continue
output_name = output_names[output_index]
boundary_outputs_by_source[source] = BoundaryOutputSpec(
proxy_output_name=f"{node_id}_{output_name}",
source=source,
io_type=str(io_type),
is_list=bool(output_is_list[output_index]),
)
component = RemoteComponentPlan(
node_ids=component_node_ids,
representative_node_id=representative_node_id,
boundary_inputs=sorted(
boundary_inputs_by_source.values(),
key=lambda spec: (spec.source.node_id, spec.source.output_index),
),
boundary_outputs=sorted(
boundary_outputs_by_source.values(),
key=lambda spec: (spec.source.node_id, spec.source.output_index),
),
execute_node_ids=sorted(output_execution_targets),
contains_output_node=contains_output_node,
)
logger.info(
"Planned remote component %s: nodes=%s boundary_inputs=%d boundary_outputs=%d execute_nodes=%s output_node=%s",
component.representative_node_id,
component.node_ids,
len(component.boundary_inputs),
len(component.boundary_outputs),
component.execute_node_ids,
component.contains_output_node,
)
return component
def _build_component_plans(
prompt: dict[str, Any],
remote_node_ids: set[str],
nodes_module: Any,
) -> list[RemoteComponentPlan]:
"""Build plans for every connected remote component."""
consumers = _build_consumer_map(prompt)
components = _build_remote_components(prompt, remote_node_ids)
return [
_build_component_plan(component, prompt, consumers, nodes_module)
for component in components
]
def _describe_output_boundary_error(
component: RemoteComponentPlan,
source: LinkedOutputRef,
source_class_type: str,
io_type: str,
local_consumer: InputTarget,
local_consumer_class_type: str,
) -> str:
"""Format a human-readable remote-to-local transport validation error."""
return (
"Remote component rooted at node "
f"{component.representative_node_id} exports node {source.node_id} "
f"({source_class_type}) output index {source.output_index} of type '{io_type}' "
f"to local node {local_consumer.node_id} ({local_consumer_class_type}) input "
f"'{local_consumer.input_name}', which cannot cross the current local/remote boundary. "
"Current ComfyUI-Modal transport only supports JSON-compatible values, bytes, "
"and tensor-like outputs such as IMAGE, MASK, LATENT, SIGMAS, NOISE, INT, "
"FLOAT, BOOLEAN, and STRING."
)
def _describe_input_boundary_error(
component: RemoteComponentPlan,
target: InputTarget,
target_class_type: str,
source: LinkedOutputRef,
source_class_type: str,
io_type: str,
) -> str:
"""Format a human-readable local-to-remote transport validation error."""
return (
"Remote node "
f"{target.node_id} ({target_class_type}) input '{target.input_name}' "
f"depends on upstream node {source.node_id} ({source_class_type}) output index "
f"{source.output_index} of type '{io_type}', which cannot cross the current "
"local/remote boundary. Current ComfyUI-Modal transport only supports "
"JSON-compatible values, bytes, and tensor-like outputs such as IMAGE, MASK, "
"LATENT, SIGMAS, NOISE, INT, FLOAT, BOOLEAN, and STRING."
)
def validate_remote_component_transport_compatibility(
prompt: dict[str, Any],
components: list[RemoteComponentPlan],
nodes_module: Any,
) -> None:
"""Reject remote components whose true graph boundaries require unsupported transport."""
validation_errors: list[str] = []
consumers = _build_consumer_map(prompt)
logger.info("Validating %d remote components for transport compatibility.", len(components))
for component in components:
for boundary_input in component.boundary_inputs:
source_prompt_node = prompt.get(boundary_input.source.node_id)
if source_prompt_node is None:
continue
source_class_type = str(source_prompt_node["class_type"])
source_class = nodes_module.NODE_CLASS_MAPPINGS.get(source_class_type)
if source_class is None:
continue
source_output_types, _, _ = _normalize_output_metadata(source_class)
if boundary_input.source.output_index >= len(source_output_types):
continue
io_type = str(source_output_types[boundary_input.source.output_index])
if _is_transportable_output_type(io_type):
continue
for target in boundary_input.targets:
target_class_type = str(prompt[target.node_id]["class_type"])
validation_errors.append(
_describe_input_boundary_error(
component=component,
target=target,
target_class_type=target_class_type,
source=boundary_input.source,
source_class_type=source_class_type,
io_type=io_type,
)
)
for boundary_output in component.boundary_outputs:
if _is_transportable_output_type(boundary_output.io_type):
continue
source_class_type = str(prompt[boundary_output.source.node_id]["class_type"])
for local_consumer in consumers.get(boundary_output.source, []):
if local_consumer.node_id in component.node_ids:
continue
local_consumer_class_type = str(prompt[local_consumer.node_id]["class_type"])
validation_errors.append(
_describe_output_boundary_error(
component=component,
source=boundary_output.source,
source_class_type=source_class_type,
io_type=boundary_output.io_type,
local_consumer=local_consumer,
local_consumer_class_type=local_consumer_class_type,
)
)
if validation_errors:
raise ModalPromptValidationError("\n".join(validation_errors))
logger.info("Remote component transport validation passed.")
def _sync_component_prompt_inputs(
component: RemoteComponentPlan,
rewritten_prompt: dict[str, Any],
sync_engine: ModalAssetSyncEngine,
) -> tuple[dict[str, Any], list[SyncedAsset]]:
"""Build a synced prompt payload for one remote component."""
component_prompt: dict[str, Any] = {}
synced_assets: list[SyncedAsset] = []
logger.info(
"Syncing prompt inputs for remote component %s with %d nodes.",
component.representative_node_id,
len(component.node_ids),
)
for node_id in component.node_ids:
prompt_node = rewritten_prompt[node_id]
synced_inputs, node_assets = sync_engine.sync_prompt_inputs(copy.deepcopy(prompt_node.get("inputs", {})))
synced_assets.extend(node_assets)
component_prompt[node_id] = {
"class_type": str(prompt_node["class_type"]),
"inputs": synced_inputs,
"_meta": copy.deepcopy(prompt_node.get("_meta", {})),
}
logger.info(
"Prepared remote node %s (%s) with %d synced assets.",
node_id,
component_prompt[node_id]["class_type"],
len(node_assets),
)
logger.info(
"Finished syncing remote component %s with %d total synced assets.",
component.representative_node_id,
len(synced_assets),
)
return component_prompt, synced_assets
def _build_component_payload(
component: RemoteComponentPlan,
component_prompt: dict[str, Any],
extra_data: dict[str, Any] | None,
custom_nodes_bundle: SyncedAsset | None,
) -> dict[str, Any]:
"""Build the serialized execution payload for one remote component."""
payload = {
"payload_kind": "subgraph",
"component_id": component.representative_node_id,
"subgraph_prompt": component_prompt,
"boundary_inputs": [
{
"proxy_input_name": boundary_input.proxy_input_name,
"targets": [
{"node_id": target.node_id, "input_name": target.input_name}
for target in boundary_input.targets
],
}
for boundary_input in component.boundary_inputs
],
"boundary_outputs": [
{
"proxy_output_name": boundary_output.proxy_output_name,
"node_id": boundary_output.source.node_id,
"output_index": boundary_output.source.output_index,
"io_type": boundary_output.io_type,
"is_list": boundary_output.is_list,
}
for boundary_output in component.boundary_outputs
],
"execute_node_ids": list(component.execute_node_ids),
"extra_data": copy.deepcopy(extra_data or {}),
"custom_nodes_bundle": (
custom_nodes_bundle.remote_path if custom_nodes_bundle is not None else None
),
}
logger.info(
"Built remote payload for component %s: boundary_inputs=%d boundary_outputs=%d execute_nodes=%s",
component.representative_node_id,
len(payload["boundary_inputs"]),
len(payload["boundary_outputs"]),
payload["execute_node_ids"],
)
return payload
def _rewrite_component_into_proxy(
component: RemoteComponentPlan,
rewritten_prompt: dict[str, Any],
payload: dict[str, Any],
nodes_module: Any,
) -> str:
"""Replace a remote component with a single proxy node in the prompt."""
output_types = tuple(spec.io_type for spec in component.boundary_outputs)
output_names = tuple(spec.proxy_output_name for spec in component.boundary_outputs)
output_is_list = tuple(spec.is_list for spec in component.boundary_outputs)
proxy_node_id = ensure_modal_component_proxy_node_registered(
output_types=output_types,
output_names=output_names,
output_is_list=output_is_list,
nodes_module=nodes_module,
is_output_node=component.contains_output_node,
)
proxy_inputs: dict[str, Any] = {
boundary_input.proxy_input_name: [boundary_input.source.node_id, boundary_input.source.output_index]
for boundary_input in component.boundary_inputs
}
proxy_inputs["original_node_data"] = payload
representative_node_id = component.representative_node_id
representative_meta = copy.deepcopy(rewritten_prompt[representative_node_id].get("_meta", {}))
rewritten_prompt[representative_node_id] = {
"class_type": proxy_node_id,
"inputs": proxy_inputs,
"_meta": representative_meta,
}
boundary_output_indices = {
spec.source: index for index, spec in enumerate(component.boundary_outputs)
}
component_node_id_set = set(component.node_ids)
for node_id, prompt_node in list(rewritten_prompt.items()):
if node_id in component_node_id_set and node_id != representative_node_id:
del rewritten_prompt[node_id]
continue
if node_id == representative_node_id:
continue
for input_name, input_value in list((prompt_node.get("inputs") or {}).items()):
if not _is_link(input_value):
continue
source = LinkedOutputRef(node_id=str(input_value[0]), output_index=int(input_value[1]))
if source not in boundary_output_indices:
continue
prompt_node["inputs"][input_name] = [representative_node_id, boundary_output_indices[source]]
logger.info(
"Rewrote remote component %s with %d nodes to Modal proxy %s.",
representative_node_id,
len(component.node_ids),
proxy_node_id,
)
return representative_node_id
def rewrite_prompt_for_modal(
prompt: dict[str, Any],
workflow: dict[str, Any] | None,
sync_engine: ModalAssetSyncEngine | None = None,
settings: ModalSyncSettings | None = None,
nodes_module: Any | None = None,
extra_data: dict[str, Any] | None = None,
) -> tuple[dict[str, Any], RewriteSummary]:
"""Rewrite connected remote components into Modal proxy nodes."""
resolved_settings = settings or get_settings()
remote_node_ids = extract_remote_node_ids(workflow, resolved_settings)
summary = RewriteSummary(remote_node_ids=sorted(remote_node_ids))
logger.info("Found %d workflow nodes marked for Modal execution.", len(remote_node_ids))
if not remote_node_ids:
return copy.deepcopy(prompt), summary
resolved_nodes_module = nodes_module or _get_nodes_module()
resolved_sync_engine = sync_engine or ModalAssetSyncEngine.from_environment(resolved_settings)
rewritten_prompt = copy.deepcopy(prompt)
components = _build_component_plans(rewritten_prompt, remote_node_ids, resolved_nodes_module)
validate_remote_component_transport_compatibility(
prompt=rewritten_prompt,
components=components,
nodes_module=resolved_nodes_module,
)
if resolved_settings.sync_custom_nodes:
summary.custom_nodes_bundle = resolved_sync_engine.sync_custom_nodes_directory()
else:
logger.info(
"Skipping custom_nodes bundle sync because sync is disabled for execution_mode=%s.",
resolved_settings.execution_mode,
)
for component in components:
summary.remote_component_ids.append(component.representative_node_id)
summary.component_node_ids_by_representative[component.representative_node_id] = list(
component.node_ids
)
for node_id in component.node_ids:
summary.rewritten_node_id_map[node_id] = component.representative_node_id
logger.info(
"Rewriting remote component %s covering nodes %s.",
component.representative_node_id,
component.node_ids,
)
component_prompt, synced_assets = _sync_component_prompt_inputs(
component=component,
rewritten_prompt=rewritten_prompt,
sync_engine=resolved_sync_engine,
)
summary.synced_assets.extend(synced_assets)
payload = _build_component_payload(
component=component,
component_prompt=component_prompt,
extra_data=extra_data,
custom_nodes_bundle=summary.custom_nodes_bundle,
)
_rewrite_component_into_proxy(
component=component,
rewritten_prompt=rewritten_prompt,
payload=payload,
nodes_module=resolved_nodes_module,
)
return rewritten_prompt, summary
async def _queue_prompt_json(prompt_server: Any, json_data: dict[str, Any]) -> web.Response:
"""Queue a possibly rewritten prompt using ComfyUI's native semantics."""
execution = _get_execution_module()
json_data = prompt_server.trigger_on_prompt(json_data)
if "number" in json_data:
number = float(json_data["number"])
else:
number = prompt_server.number
if json_data.get("front"):
number = -number
prompt_server.number += 1
if "prompt" not in json_data:
return web.json_response(
{
"error": {
"type": "no_prompt",
"message": "No prompt provided",
"details": "No prompt provided",
"extra_info": {},
}
},
status=400,
)
prompt = json_data["prompt"]
prompt_id = str(json_data.get("prompt_id", uuid.uuid4()))
partial_execution_targets = json_data.get("partial_execution_targets")
valid = await execution.validate_prompt(prompt_id, prompt, partial_execution_targets)
extra_data = dict(json_data.get("extra_data", {}))
if "client_id" in json_data:
extra_data["client_id"] = json_data["client_id"]
if not valid[0]:
logger.warning("invalid prompt: %s", valid[1])
return web.json_response({"error": valid[1], "node_errors": valid[3]}, status=400)
outputs_to_execute = valid[2]
sensitive: dict[str, Any] = {}
for sensitive_key in execution.SENSITIVE_EXTRA_DATA_KEYS:
if sensitive_key in extra_data:
sensitive[sensitive_key] = extra_data.pop(sensitive_key)
extra_data["create_time"] = int(time.time() * 1000)
prompt_server.prompt_queue.put(
(number, prompt_id, prompt, extra_data, outputs_to_execute, sensitive)
)
return web.json_response(
{"prompt_id": prompt_id, "number": number, "node_errors": valid[3]}
)
def setup_modal_queue_route(
prompt_server: Any | None = None,
sync_engine: ModalAssetSyncEngine | None = None,
settings: ModalSyncSettings | None = None,
) -> None:
"""Register the `/modal/queue_prompt` route once for the active PromptServer."""
global _ROUTE_REGISTERED
if _ROUTE_REGISTERED:
return
try:
resolved_server_module = _get_server_module()
except ModuleNotFoundError:
logger.debug("ComfyUI server module is not available; skipping route registration.")
return
resolved_settings = settings or get_settings()
prompt_server = prompt_server or getattr(resolved_server_module.PromptServer, "instance", None)
if prompt_server is None:
logger.debug("PromptServer.instance is not available; skipping route registration.")
return
resolved_sync_engine = sync_engine or ModalAssetSyncEngine.from_environment(resolved_settings)
@prompt_server.routes.post(resolved_settings.route_path)
async def modal_queue_prompt(request: web.Request) -> web.Response:
"""Handle prompt queue requests that include Modal remote markers."""
logger.info("Received Modal queue request.")
json_data: dict[str, Any] | None = None
workflow: dict[str, Any] | None = None
remote_node_ids: list[str] = []
summary = RewriteSummary()
try:
request_started_at = time.perf_counter()
json_data = await request.json()
json_data.setdefault("prompt_id", str(uuid.uuid4()))
json_data.setdefault("extra_data", {})
if json_data.get("client_id") is not None:
json_data["extra_data"]["client_id"] = json_data["client_id"]
extra_pnginfo = ((json_data.get("extra_data") or {}).get("extra_pnginfo") or {})
workflow = extra_pnginfo.get("workflow")
remote_node_ids = sorted(extract_remote_node_ids(workflow, resolved_settings))
if "prompt" in json_data:
rewrite_started_at = time.perf_counter()
rewritten_prompt, summary = rewrite_prompt_for_modal(
prompt=json_data["prompt"],
workflow=workflow,
sync_engine=resolved_sync_engine,
settings=resolved_settings,
extra_data=json_data.get("extra_data"),
)
logger.info(
"Modal prompt rewrite finished in %.3fs for %d remote nodes across %d components.",
time.perf_counter() - rewrite_started_at,
len(summary.remote_node_ids),
len(summary.remote_component_ids),
)
remote_node_ids = list(summary.remote_node_ids)
json_data["prompt"] = rewritten_prompt
if json_data.get("partial_execution_targets"):
rewritten_targets = {
summary.rewritten_node_id_map.get(str(target), str(target))
for target in json_data["partial_execution_targets"]
}
json_data["partial_execution_targets"] = sorted(rewritten_targets)
json_data.setdefault("extra_data", {}).setdefault("modal", {})
json_data["extra_data"]["modal"]["remote_node_ids"] = summary.remote_node_ids
json_data["extra_data"]["modal"]["remote_component_ids"] = summary.remote_component_ids
json_data["extra_data"]["modal"]["synced_assets"] = [
asset.remote_path for asset in summary.synced_assets
]
if summary.custom_nodes_bundle is not None:
json_data["extra_data"]["modal"]["custom_nodes_bundle"] = (
summary.custom_nodes_bundle.remote_path
)
_emit_modal_status(
prompt_server=prompt_server,
phase="setup",
client_id=str(json_data.get("client_id")) if json_data.get("client_id") else None,
prompt_id=str(json_data.get("prompt_id")) if json_data.get("prompt_id") else None,
node_ids=remote_node_ids,
component_node_ids_by_representative=summary.component_node_ids_by_representative,
)
response = await _queue_prompt_json(prompt_server, json_data)
logger.info(
"Modal queue request completed in %.3fs.",
time.perf_counter() - request_started_at,
)
return response
except FileNotFoundError as exc:
logger.exception("Modal asset sync failed.")
if json_data is not None:
_emit_modal_status(
prompt_server=prompt_server,
phase="error",
client_id=str(json_data.get("client_id")) if json_data.get("client_id") else None,
prompt_id=str(json_data.get("prompt_id")) if json_data.get("prompt_id") else None,
node_ids=remote_node_ids,
error_message=str(exc),
)
return web.json_response({"error": str(exc), "node_errors": []}, status=400)
except ModalPromptValidationError as exc:
logger.exception("Modal prompt validation failed.")
if json_data is not None:
_emit_modal_status(
prompt_server=prompt_server,
phase="error",
client_id=str(json_data.get("client_id")) if json_data.get("client_id") else None,
prompt_id=str(json_data.get("prompt_id")) if json_data.get("prompt_id") else None,
node_ids=remote_node_ids,
error_message=str(exc),
)
return web.json_response({"error": str(exc), "node_errors": []}, status=400)
except Exception as exc:
logger.exception("Modal queue handler failed.")
if json_data is not None:
_emit_modal_status(
prompt_server=prompt_server,
phase="error",
client_id=str(json_data.get("client_id")) if json_data.get("client_id") else None,
prompt_id=str(json_data.get("prompt_id")) if json_data.get("prompt_id") else None,
node_ids=remote_node_ids,
error_message=str(exc),
)
return web.json_response({"error": str(exc), "node_errors": []}, status=500)
_ROUTE_REGISTERED = True
logger.info("Registered Modal queue route at %s", resolved_settings.route_path)