comfyui: route custom workflows through image/video selectors
The selector path previously hid the custom-workflow feature: video_selector filtered tools on per-operation readiness (bundled WAN models) and both selectors only chose ToolStatus.AVAILABLE providers, so comfyui_image/ comfyui_video — DEGRADED when bundled model metadata is missing — were dropped even when the ComfyUI server was up and the caller supplied a full workflow_json/workflow_path plus output_node. - Add a custom-workflow readiness path to both selectors: when a custom workflow is supplied, eligibility is based on server availability (status != UNAVAILABLE) for any provider advertising supports.custom_workflow, not on bundled-model readiness. A custom workflow also restricts routing to custom-workflow-capable providers, since the graph JSON is ComfyUI specific. - Expose workflow_json, workflow_path, output_node, workflow_name, workflow_model, and workflow_model_stack in both selector schemas so agents can discover the feature without bypassing the selectors. - image_selector only forwards the workflow inputs to providers that declare them. - Add contract tests for the new eligibility path and schema exposure. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -96,6 +96,38 @@ class VideoSelector(BaseTool):
|
||||
"type": "string",
|
||||
"description": "Resolution hint for providers that support named output resolutions.",
|
||||
},
|
||||
"workflow_json": {
|
||||
"type": "string",
|
||||
"description": (
|
||||
"Optional full ComfyUI workflow JSON. Routes to a custom-workflow-capable "
|
||||
"provider (e.g. comfyui_video) based on server availability, not bundled "
|
||||
"model readiness. Requires output_node."
|
||||
),
|
||||
},
|
||||
"workflow_path": {
|
||||
"type": "string",
|
||||
"description": (
|
||||
"Optional path to a ComfyUI workflow JSON file. Routes to a custom-workflow-"
|
||||
"capable provider based on server availability. Requires output_node."
|
||||
),
|
||||
},
|
||||
"output_node": {
|
||||
"type": "string",
|
||||
"description": "ComfyUI output node ID for a custom workflow_json/workflow_path.",
|
||||
},
|
||||
"workflow_name": {
|
||||
"type": "string",
|
||||
"description": "Optional human-readable provenance label for a custom workflow.",
|
||||
},
|
||||
"workflow_model": {
|
||||
"type": "string",
|
||||
"description": "Optional model/provenance label for a custom workflow.",
|
||||
},
|
||||
"workflow_model_stack": {
|
||||
"type": "array",
|
||||
"items": {"type": "object"},
|
||||
"description": "Optional provenance metadata for custom workflow dependencies.",
|
||||
},
|
||||
"output_path": {"type": "string"},
|
||||
},
|
||||
}
|
||||
@@ -231,10 +263,10 @@ class VideoSelector(BaseTool):
|
||||
|
||||
rankings = rank_providers(candidates, task_context)
|
||||
|
||||
# Build tool lookup: provider → tool (first available per provider)
|
||||
# Build tool lookup: provider → tool (first selectable per provider)
|
||||
tool_by_provider: dict[str, BaseTool] = {}
|
||||
for tool in candidates:
|
||||
if tool.provider not in tool_by_provider and tool.get_status() == ToolStatus.AVAILABLE:
|
||||
if tool.provider not in tool_by_provider and self._tool_selectable(tool, inputs):
|
||||
tool_by_provider[tool.provider] = tool
|
||||
|
||||
# If a preferred provider is explicitly requested and available,
|
||||
@@ -298,6 +330,12 @@ class VideoSelector(BaseTool):
|
||||
inputs: dict[str, object],
|
||||
candidates: list[BaseTool],
|
||||
) -> list[BaseTool]:
|
||||
# A caller-supplied custom workflow is provider-specific (ComfyUI graph
|
||||
# JSON). Route it only to custom-workflow-capable providers whose server
|
||||
# is reachable — bundled-model readiness is irrelevant in that case.
|
||||
if self._has_custom_workflow(inputs):
|
||||
return [t for t in candidates if self._custom_workflow_eligible(t, inputs)]
|
||||
|
||||
operation = inputs.get("operation", "text_to_video")
|
||||
if operation == "rank":
|
||||
operation = inputs.get("target_operation", "text_to_video")
|
||||
@@ -333,3 +371,31 @@ class VideoSelector(BaseTool):
|
||||
if not callable(checker):
|
||||
return True
|
||||
return bool(checker(operation))
|
||||
|
||||
@staticmethod
|
||||
def _has_custom_workflow(inputs: dict[str, object]) -> bool:
|
||||
return bool(inputs.get("workflow_json") or inputs.get("workflow_path"))
|
||||
|
||||
def _custom_workflow_eligible(self, tool: BaseTool, inputs: dict[str, object]) -> bool:
|
||||
"""Whether a tool can run the caller-supplied custom workflow.
|
||||
|
||||
Eligibility is based on server availability, not bundled-model readiness:
|
||||
a provider qualifies when it advertises ``custom_workflow`` support, an
|
||||
``output_node`` is supplied, and its backend is reachable (status is not
|
||||
UNAVAILABLE).
|
||||
"""
|
||||
if not self._has_custom_workflow(inputs):
|
||||
return False
|
||||
if not inputs.get("output_node"):
|
||||
return False
|
||||
supports = getattr(tool, "supports", {})
|
||||
if not supports.get("custom_workflow"):
|
||||
return False
|
||||
return tool.get_status() != ToolStatus.UNAVAILABLE
|
||||
|
||||
def _tool_selectable(self, tool: BaseTool, inputs: dict[str, object]) -> bool:
|
||||
"""A provider is selectable if it is AVAILABLE, or if it can serve a
|
||||
caller-supplied custom workflow even while bundled models report DEGRADED."""
|
||||
if tool.get_status() == ToolStatus.AVAILABLE:
|
||||
return True
|
||||
return self._custom_workflow_eligible(tool, inputs)
|
||||
|
||||
Reference in New Issue
Block a user