|
| 1 | +# RFC: ComfyUI API Improvements |
| 2 | + |
| 3 | +- Start Date: 2025-02-03 |
| 4 | +- Target Major Version: TBD |
| 5 | + |
| 6 | +## Summary |
| 7 | + |
| 8 | +This RFC proposes three key improvements to the ComfyUI API: |
| 9 | + |
| 10 | +1. Lazy loading for COMBO input options to reduce initial payload size |
| 11 | +2. Restructuring node output specifications for better maintainability |
| 12 | +3. Explicit COMBO type definition for clearer client-side handling |
| 13 | + |
| 14 | +## Basic example |
| 15 | + |
| 16 | +### 1\. Lazy Loading COMBO Options |
| 17 | + |
| 18 | +```python |
| 19 | +# Before |
| 20 | +class CheckpointLoader: |
| 21 | + @classmethod |
| 22 | + def INPUT_TYPES(s): |
| 23 | + return { |
| 24 | + "required": { |
| 25 | + "config_name": (folder_paths.get_filename_list("configs"),), |
| 26 | + } |
| 27 | + } |
| 28 | + |
| 29 | +# After |
| 30 | +class CheckpointLoader: |
| 31 | + @classmethod |
| 32 | + def INPUT_TYPES(s): |
| 33 | + return { |
| 34 | + "required": { |
| 35 | + "config_name": ("COMBO", { |
| 36 | + "type" : "remote", |
| 37 | + "route": "/internal/files", |
| 38 | + "response_key" : "files", |
| 39 | + "query_params" : { |
| 40 | + "folder_path" : "configs" |
| 41 | + } |
| 42 | + }), |
| 43 | + } |
| 44 | + } |
| 45 | +``` |
| 46 | + |
| 47 | +### 2\. Improved Output Specification |
| 48 | + |
| 49 | +```python |
| 50 | +# Before |
| 51 | +RETURN_TYPES = ("CONDITIONING","CONDITIONING") |
| 52 | +RETURN_NAMES = ("positive", "negative") |
| 53 | +OUTPUT_IS_LIST = (False, False) |
| 54 | +OUTPUT_TOOLTIPS = ("positive-tooltip", "negative-tooltip") |
| 55 | + |
| 56 | +# After |
| 57 | +RETURNS = ( |
| 58 | + {"type": "CONDITIONING", "name": "positive", "is_list": False, "tooltip": "positive-tooltip"}, |
| 59 | + {"type": "CONDITIONING", "name": "negative", "is_list": False, "tooltip": "negative-tooltip"}, |
| 60 | +) |
| 61 | +``` |
| 62 | + |
| 63 | +### 3\. Explicit COMBO Type |
| 64 | + |
| 65 | +```python |
| 66 | +# Before |
| 67 | +"combo input": [[1, 2, 3], { default: 2 }] |
| 68 | + |
| 69 | +# After |
| 70 | +"combo input": ["COMBO", { options: [1, 2, 3], default: 2}] |
| 71 | +``` |
| 72 | + |
| 73 | +## Motivation |
| 74 | + |
| 75 | +1. **Full recompute**: If the user wants to refresh the COMBO options for a single folder, they need to recompute the entire node definitions. This is a very slow process and not user friendly. |
| 76 | + |
| 77 | +2. **Large Payload Issue**: The `/object_info` API currently returns several MB of JSON data, primarily due to eager loading of COMBO options. This impacts initial load times and overall performance. |
| 78 | + |
| 79 | +3. **Output Specification Maintenance**: The current format for defining node outputs requires modifications in multiple lists, making it error-prone and difficult to maintain. Adding new features like tooltips would further complicate this. |
| 80 | + |
| 81 | +4. **Implicit COMBO Type**: The current implementation requires client-side code to infer COMBO types by checking if the first parameter is a list, which is not intuitive and could lead to maintenance issues. |
| 82 | + |
| 83 | +## Detailed design |
| 84 | + |
| 85 | +The implementation will be split into two phases to minimize disruption: |
| 86 | + |
| 87 | +### Phase 1: Combo Specification Changes |
| 88 | + |
| 89 | +#### 1.1 New Combo Specification |
| 90 | + |
| 91 | +Input types will be explicitly defined using tuples with configuration objects. A variant of the `COMBO` type will be added to support lazy loading options from the server. |
| 92 | + |
| 93 | +```python |
| 94 | +@classmethod |
| 95 | +def INPUT_TYPES(s): |
| 96 | + return { |
| 97 | + "required": { |
| 98 | + # Remote combo |
| 99 | + "ckpt_name": ("COMBO", { |
| 100 | + "type": "remote", |
| 101 | + "route": "/internal/files", |
| 102 | + "response_key": "files", |
| 103 | + "refresh": 0, # TTL in ms. 0 = do not refresh after initial load. |
| 104 | + "query_params": { |
| 105 | + "folder_path": "checkpoints", |
| 106 | + "filter_ext": [".ckpt", ".safetensors"] |
| 107 | + } |
| 108 | + }), |
| 109 | + "mode": ("COMBO", { |
| 110 | + "options": ["balanced", "speed", "quality"], |
| 111 | + "default": "balanced", |
| 112 | + "tooltip": "Processing mode" |
| 113 | + }) |
| 114 | + } |
| 115 | + } |
| 116 | +``` |
| 117 | + |
| 118 | +Use a Proxy on remote combo widgets' values property that doesn't compute/fetch until first access. |
| 119 | + |
| 120 | +```typescript |
| 121 | + COMBO(node, inputName, inputData: InputSpec, app, widgetName) { |
| 122 | + |
| 123 | + // ... |
| 124 | + |
| 125 | + const res = { |
| 126 | + widget: node.addWidget('combo', inputName, defaultValue, () => {}, { |
| 127 | + // Support old and new combo input specs |
| 128 | + values: widgetStore.isComboInputV2(inputData) |
| 129 | + ? inputData[1].options |
| 130 | + : inputType |
| 131 | + }) |
| 132 | + } |
| 133 | + |
| 134 | + if (type === 'remote') { |
| 135 | + const remoteWidget = useRemoteWidget(inputData) |
| 136 | + |
| 137 | + const origOptions = res.widget.options |
| 138 | + res.widget.options = new Proxy(origOptions, { |
| 139 | + // Defer fetching until first access (node added to graph) |
| 140 | + get(target, prop: string | symbol) { |
| 141 | + if (prop !== 'values') return target[prop] |
| 142 | + |
| 143 | + // Start non-blocking fetch |
| 144 | + remoteWidget.fetchOptions().then((data) => {}) |
| 145 | + |
| 146 | + const current = remoteWidget.getCacheEntry() |
| 147 | + return current?.data || widgetStore.getDefaultValue(inputData) |
| 148 | + } |
| 149 | + }) |
| 150 | + } |
| 151 | +``` |
| 152 | +
|
| 153 | +Backoff time will be determined by the number of failed attempts: |
| 154 | +
|
| 155 | +```typescript |
| 156 | +// Exponential backoff with max of 10 seconds |
| 157 | +const backoff = Math.min(1000 * 2 ** (failedAttempts - 1), 10000); |
| 158 | + |
| 159 | +// Example backoff times: |
| 160 | +// Attempt 1: 1000ms (1s) |
| 161 | +// Attempt 2: 2000ms (2s) |
| 162 | +// Attempt 3: 4000ms (4s) |
| 163 | +// Attempt 4: 8000ms (8s) |
| 164 | +// Attempt 5+: 10000ms (10s) |
| 165 | +``` |
| 166 | +
|
| 167 | +Share computation results between widgets using a key based on the route and query params: |
| 168 | +
|
| 169 | +```typescript |
| 170 | +// Global cache for memoizing fetches |
| 171 | +const dataCache = new Map<string, CacheEntry<any>>(); |
| 172 | + |
| 173 | +function getCacheKey(options: RemoteWidgetOptions): string { |
| 174 | + return JSON.stringify({ route: options.route, params: options.query_params }); |
| 175 | +} |
| 176 | +``` |
| 177 | +
|
| 178 | +The cache can be invalidated in two ways: |
| 179 | +
|
| 180 | +1. **TTL-based**: Using the `refresh` parameter to specify a time-to-live in milliseconds. When TTL expires, next access triggers a new fetch. |
| 181 | +2. **Manual**: Using the `forceUpdate` method of the widget, which deletes the cache entry and triggers a new fetch on next access. |
| 182 | +
|
| 183 | +Example TTL usage: |
| 184 | +
|
| 185 | +```python |
| 186 | +"ckpt_name": ("COMBO", { |
| 187 | + "type": "remote", |
| 188 | + "refresh": 60000, # Refresh every minute |
| 189 | + // ... other options |
| 190 | +}) |
| 191 | +``` |
| 192 | +
|
| 193 | +#### 1.2 New Endpoints |
| 194 | +
|
| 195 | +```python |
| 196 | +@routes.get("/internal/files/{folder_name}") |
| 197 | +async def list_folder_files(request): |
| 198 | + folder_name = request.match_info["folder_name"] |
| 199 | + filter_ext = request.query.get("filter_ext", "").split(",") |
| 200 | + filter_content_type = request.query.get("filter_content_type", "").split(",") |
| 201 | + |
| 202 | + files = folder_paths.get_filename_list(folder_name) |
| 203 | + if filter_ext and filter_ext[0]: |
| 204 | + files = [f for f in files if any(f.endswith(ext) for ext in filter_ext)] |
| 205 | + if filter_content_type and filter_content_type[0]: |
| 206 | + files = folder_paths.filter_files_content_type(files, filter_content_type) |
| 207 | + |
| 208 | + return web.json_response({ |
| 209 | + "files": files, |
| 210 | + }) |
| 211 | +``` |
| 212 | +
|
| 213 | +#### 1.3 Gradual Change with Nodes |
| 214 | +
|
| 215 | +Nodes will be updated incrementally to use the new combo specification. |
| 216 | +
|
| 217 | +### Phase 2: Node Output Specification Changes |
| 218 | +
|
| 219 | +#### 2.1 New Output Format |
| 220 | +
|
| 221 | +Nodes will transition from multiple return definitions to a single `RETURNS` tuple: |
| 222 | +
|
| 223 | +```python |
| 224 | +# Current format will be supported during transition |
| 225 | +RETURN_TYPES = ("CONDITIONING", "CONDITIONING") |
| 226 | +RETURN_NAMES = ("positive", "negative") |
| 227 | +OUTPUT_IS_LIST = (False, False) |
| 228 | +OUTPUT_TOOLTIPS = ("positive-tooltip", "negative-tooltip") |
| 229 | + |
| 230 | +# New format |
| 231 | +RETURNS = ( |
| 232 | + { |
| 233 | + "type": "CONDITIONING", |
| 234 | + "name": "positive", |
| 235 | + "is_list": False, |
| 236 | + "tooltip": "positive-tooltip", |
| 237 | + "optional": False # New field for optional outputs |
| 238 | + }, |
| 239 | + { |
| 240 | + "type": "CONDITIONING", |
| 241 | + "name": "negative", |
| 242 | + "is_list": False, |
| 243 | + "tooltip": "negative-tooltip" |
| 244 | + } |
| 245 | +) |
| 246 | +``` |
| 247 | +
|
| 248 | +#### 2.2 New Response Format |
| 249 | +
|
| 250 | +Old format: |
| 251 | +
|
| 252 | +```javascript |
| 253 | +{ |
| 254 | + "CheckpointLoader": { |
| 255 | + "input": { |
| 256 | + "required": { |
| 257 | + "ckpt_name": [[ |
| 258 | + "file1", |
| 259 | + "file2", |
| 260 | + ... |
| 261 | + "fileN", |
| 262 | + ]], |
| 263 | + "combo_input": [[ |
| 264 | + "option1", |
| 265 | + "option2", |
| 266 | + ... |
| 267 | + "optionN", |
| 268 | + ], { |
| 269 | + "default": "option1", |
| 270 | + "tooltip": "Processing mode" |
| 271 | + }], |
| 272 | + }, |
| 273 | + "optional": {} |
| 274 | + }, |
| 275 | + "output": ["MODEL"], |
| 276 | + "output_name": ["model"], |
| 277 | + "output_is_list": [false], |
| 278 | + "output_tooltip": ["The loaded model"], |
| 279 | + "output_node": false, |
| 280 | + "category": "loaders" |
| 281 | + } |
| 282 | +} |
| 283 | +``` |
| 284 | +
|
| 285 | +New format: |
| 286 | +
|
| 287 | +```javascript |
| 288 | +{ |
| 289 | + "CheckpointLoader": { |
| 290 | + "input": { |
| 291 | + "required": { |
| 292 | + "ckpt_name": [ |
| 293 | + "COMBO", |
| 294 | + { |
| 295 | + "type" : "remote", |
| 296 | + "route": "/internal/files", |
| 297 | + "response_key" : "files", |
| 298 | + "query_params" : { |
| 299 | + "folder_path" : "checkpoints" |
| 300 | + } |
| 301 | + } |
| 302 | + ], |
| 303 | + "combo_input": [ |
| 304 | + "COMBO", |
| 305 | + { |
| 306 | + "options": ["option1", "option2", ... "optionN"], |
| 307 | + "default": "option1", |
| 308 | + "tooltip": "Processing mode" |
| 309 | + } |
| 310 | + ], |
| 311 | + }, |
| 312 | + "optional": {} |
| 313 | + }, |
| 314 | + "output": [ |
| 315 | + { |
| 316 | + "type": "MODEL", |
| 317 | + "name": "model", |
| 318 | + "is_list": false, |
| 319 | + "tooltip": "The loaded model" |
| 320 | + } |
| 321 | + ], |
| 322 | + "output_node": false, |
| 323 | + "category": "loaders" |
| 324 | + } |
| 325 | +} |
| 326 | +``` |
| 327 | +
|
| 328 | +#### 2.3 Compatibility Layer |
| 329 | +
|
| 330 | +Transformations will be applied on the frontend to convert the old format to the new format. |
| 331 | +
|
| 332 | +#### 2.4 Gradual Change with Nodes |
| 333 | +
|
| 334 | +Nodes will be updated incrementally to use the new output specification format. |
| 335 | +
|
| 336 | +### Migration Support |
| 337 | +
|
| 338 | +To support gradual migration, the API will: |
| 339 | +
|
| 340 | +1. **Dual Support**: Accept both old and new node definitions |
| 341 | +2. **Compatibility Layer**: Include a compatibility layer in the frontend that can type check and handle both old and new formats. |
| 342 | +
|
| 343 | +## Drawbacks |
| 344 | +
|
| 345 | +1. **Migration Effort**: Users and node developers will need to update their code to match the new formats. |
| 346 | +2. **Additional Complexity**: Lazy loading adds network requests, which could complicate error handling and state management. |
| 347 | +
|
| 348 | +## Adoption strategy |
| 349 | +
|
| 350 | +1. **Version Support**: Maintain backward compatibility for at least one major version. |
| 351 | +2. **Migration Guide**: Provide detailed documentation and migration scripts. |
| 352 | +3. **Gradual Rollout**: Implement changes in phases, starting with lazy loading. |
| 353 | +
|
| 354 | +## Unresolved questions |
| 355 | +
|
| 356 | +### Resolved |
| 357 | +
|
| 358 | +1. ~~Network failure handling~~ - Implemented with exponential backoff |
| 359 | +2. ~~Caching strategy~~ - Per-widget initialization with manual invalidation |
| 360 | +
|
| 361 | +### Implementation Details |
| 362 | +
|
| 363 | +3. Should we provide a migration utility for updating existing nodes? |
| 364 | +4. How do we handle custom node types that may not fit the new output specification format? |
| 365 | +
|
| 366 | +### Future Considerations |
| 367 | +
|
| 368 | +5. Should an option to set an invalidation signal be added to the remote COMBO type? |
| 369 | +6. Should an option for a custom cache key for the remote COMBO type be added? |
| 370 | +
|
| 371 | +### Security Concerns |
| 372 | +
|
| 373 | +7. Implementation details needed for: |
| 374 | + - Rate limiting strategy |
| 375 | + - Input validation approach |
| 376 | + - Cache poisoning prevention measures |
| 377 | + - Access control mechanisms |
0 commit comments