diff --git a/openapi.yaml b/openapi.yaml index 86ac9f2f1..eba4079f9 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1,45 +1,49 @@ openapi: 3.0.3 info: title: ComfyUI API - description: | - API for ComfyUI - A powerful and modular UI for Stable Diffusion. - + description: 'API for ComfyUI - A powerful and modular UI for Stable Diffusion. + + This API allows you to interact with ComfyUI programmatically, including: + - Submitting workflows for execution + - Managing the execution queue + - Retrieving generated images + - Managing models + - Retrieving node information + + ' version: 1.0.0 license: name: GNU General Public License v3.0 url: https://github.com/comfyanonymous/ComfyUI/blob/master/LICENSE - servers: - - url: /api - description: Default ComfyUI server - +- url: / + description: Default ComfyUI server tags: - - name: workflow - description: Workflow execution and management - - name: queue - description: Queue management - - name: image - description: Image handling - - name: node - description: Node information - - name: model - description: Model management - - name: system - description: System information - - name: internal - description: Internal API routes - +- name: workflow + description: Workflow execution and management +- name: queue + description: Queue management +- name: image + description: Image handling +- name: node + description: Node information +- name: model + description: Model management +- name: system + description: System information +- name: internal + description: Internal API routes paths: - /prompt: + /api/prompt: get: tags: - - workflow + - workflow summary: Get information about current prompt execution description: Returns information about the current prompt in the execution queue operationId: getPromptInfo @@ -52,11 +56,13 @@ paths: $ref: '#/components/schemas/PromptInfo' post: tags: - - workflow + - workflow summary: Submit a workflow for execution - description: | - Submit a workflow to be executed by the backend. + description: 'Submit a workflow to be executed by the backend. + The workflow is a JSON object describing the nodes and their connections. + + ' operationId: executePrompt requestBody: required: true @@ -77,11 +83,10 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - - /queue: + /api/queue: get: tags: - - queue + - queue summary: Get queue information description: Returns information about running and pending items in the queue operationId: getQueueInfo @@ -94,7 +99,7 @@ paths: $ref: '#/components/schemas/QueueInfo' post: tags: - - queue + - queue summary: Manage queue description: Clear the queue or delete specific items operationId: manageQueue @@ -117,22 +122,20 @@ paths: responses: '200': description: Success - - /interrupt: + /api/interrupt: post: tags: - - workflow + - workflow summary: Interrupt the current execution description: Interrupts the currently running workflow execution operationId: interruptExecution responses: '200': description: Success - - /free: + /api/free: post: tags: - - system + - system summary: Free resources description: Unload models and/or free memory operationId: freeResources @@ -152,22 +155,21 @@ paths: responses: '200': description: Success - - /history: + /api/history: get: tags: - - workflow + - workflow summary: Get execution history description: Returns the history of executed workflows operationId: getHistory parameters: - - name: max_items - in: query - description: Maximum number of history items to return - required: false - schema: - type: integer - format: int32 + - name: max_items + in: query + description: Maximum number of history items to return + required: false + schema: + type: integer + format: int32 responses: '200': description: Success @@ -179,7 +181,7 @@ paths: $ref: '#/components/schemas/HistoryItem' post: tags: - - workflow + - workflow summary: Manage history description: Clear history or delete specific items operationId: manageHistory @@ -202,22 +204,21 @@ paths: responses: '200': description: Success - - /history/{prompt_id}: + /api/history/{prompt_id}: get: tags: - - workflow + - workflow summary: Get specific history item description: Returns a specific history item by ID operationId: getHistoryItem parameters: - - name: prompt_id - in: path - description: ID of the prompt to retrieve - required: true - schema: - type: string - format: uuid + - name: prompt_id + in: path + description: ID of the prompt to retrieve + required: true + schema: + type: string + format: uuid responses: '200': description: Success @@ -225,11 +226,10 @@ paths: application/json: schema: $ref: '#/components/schemas/HistoryItem' - - /object_info: + /api/object_info: get: tags: - - node + - node summary: Get all node information description: Returns information about all available nodes operationId: getNodeInfo @@ -242,21 +242,20 @@ paths: type: object additionalProperties: $ref: '#/components/schemas/NodeInfo' - - /object_info/{node_class}: + /api/object_info/{node_class}: get: tags: - - node + - node summary: Get specific node information description: Returns information about a specific node class operationId: getNodeClassInfo parameters: - - name: node_class - in: path - description: Name of the node class - required: true - schema: - type: string + - name: node_class + in: path + description: Name of the node class + required: true + schema: + type: string responses: '200': description: Success @@ -266,11 +265,10 @@ paths: type: object additionalProperties: $ref: '#/components/schemas/NodeInfo' - - /upload/image: + /api/upload/image: post: tags: - - image + - image summary: Upload an image description: Uploads an image to the server operationId: uploadImage @@ -290,7 +288,10 @@ paths: description: Whether to overwrite if file exists (true/false) type: type: string - enum: [input, temp, output] + enum: + - input + - temp + - output description: Type of directory to store the image in subfolder: type: string @@ -314,11 +315,10 @@ paths: description: Type of directory the image was stored in '400': description: Bad request - - /upload/mask: + /api/upload/mask: post: tags: - - image + - image summary: Upload a mask for an image description: Uploads a mask image and applies it to a referenced original image operationId: uploadMask @@ -355,49 +355,54 @@ paths: description: Type of directory the mask was stored in '400': description: Bad request - - /view: + /api/view: get: tags: - - image + - image summary: View an image description: Retrieves an image from the server operationId: viewImage parameters: - - name: filename - in: query - description: Name of the file to retrieve - required: true - schema: - type: string - - name: type - in: query - description: Type of directory to retrieve from - required: false - schema: - type: string - enum: [input, temp, output] - default: output - - name: subfolder - in: query - description: Subfolder to retrieve from - required: false - schema: - type: string - - name: preview - in: query - description: Preview options (format;quality) - required: false - schema: - type: string - - name: channel - in: query - description: Channel to retrieve (rgb, a, rgba) - required: false - schema: - type: string - enum: [rgb, a, rgba] - default: rgba + - name: filename + in: query + description: Name of the file to retrieve + required: true + schema: + type: string + - name: type + in: query + description: Type of directory to retrieve from + required: false + schema: + type: string + enum: + - input + - temp + - output + default: output + - name: subfolder + in: query + description: Subfolder to retrieve from + required: false + schema: + type: string + - name: preview + in: query + description: Preview options (format;quality) + required: false + schema: + type: string + - name: channel + in: query + description: Channel to retrieve (rgb, a, rgba) + required: false + schema: + type: string + enum: + - rgb + - a + - rgba + default: rgba responses: '200': description: Success @@ -410,27 +415,26 @@ paths: description: Bad request '404': description: File not found - - /view_metadata/{folder_name}: + /api/view_metadata/{folder_name}: get: tags: - - model + - model summary: View model metadata description: Retrieves metadata from a safetensors file operationId: viewModelMetadata parameters: - - name: folder_name - in: path - description: Name of the model folder - required: true - schema: - type: string - - name: filename - in: query - description: Name of the safetensors file - required: true - schema: - type: string + - name: folder_name + in: path + description: Name of the model folder + required: true + schema: + type: string + - name: filename + in: query + description: Name of the safetensors file + required: true + schema: + type: string responses: '200': description: Success @@ -440,11 +444,10 @@ paths: type: object '404': description: File not found - - /models: + /api/models: get: tags: - - model + - model summary: Get model types description: Returns a list of available model types operationId: getModelTypes @@ -457,21 +460,20 @@ paths: type: array items: type: string - - /models/{folder}: + /api/models/{folder}: get: tags: - - model + - model summary: Get models of a specific type description: Returns a list of available models of a specific type operationId: getModels parameters: - - name: folder - in: path - description: Model type folder - required: true - schema: - type: string + - name: folder + in: path + description: Model type folder + required: true + schema: + type: string responses: '200': description: Success @@ -483,11 +485,10 @@ paths: type: string '404': description: Folder not found - - /embeddings: + /api/embeddings: get: tags: - - model + - model summary: Get embeddings description: Returns a list of available embeddings operationId: getEmbeddings @@ -500,11 +501,10 @@ paths: type: array items: type: string - - /extensions: + /api/extensions: get: tags: - - system + - system summary: Get extensions description: Returns a list of available extensions operationId: getExtensions @@ -517,11 +517,10 @@ paths: type: array items: type: string - - /system_stats: + /api/system_stats: get: tags: - - system + - system summary: Get system statistics description: Returns system information including RAM, VRAM, and ComfyUI version operationId: getSystemStats @@ -532,31 +531,32 @@ paths: application/json: schema: $ref: '#/components/schemas/SystemStats' - - /ws: + /api/ws: get: tags: - - workflow + - workflow summary: WebSocket connection - description: | - Establishes a WebSocket connection for real-time communication. - This endpoint is used for receiving progress updates, status changes, and results from workflow executions. + description: 'Establishes a WebSocket connection for real-time communication. + + This endpoint is used for receiving progress updates, status changes, and + results from workflow executions. + + ' operationId: webSocketConnect parameters: - - name: clientId - in: query - description: Optional client ID for reconnection - required: false - schema: - type: string + - name: clientId + in: query + description: Optional client ID for reconnection + required: false + schema: + type: string responses: '101': description: Switching Protocols to WebSocket - /internal/logs: get: tags: - - internal + - internal summary: Get logs description: Returns system logs as a single string operationId: getLogs @@ -567,11 +567,10 @@ paths: application/json: schema: type: string - /internal/logs/raw: get: tags: - - internal + - internal summary: Get raw logs description: Returns raw system logs with terminal size information operationId: getRawLogs @@ -603,11 +602,10 @@ paths: rows: type: integer description: Terminal rows - /internal/logs/subscribe: patch: tags: - - internal + - internal summary: Subscribe to logs description: Subscribe or unsubscribe to log updates operationId: subscribeToLogs @@ -627,11 +625,10 @@ paths: responses: '200': description: Success - /internal/folder_paths: get: tags: - - internal + - internal summary: Get folder paths description: Returns a map of folder names to their paths operationId: getFolderPaths @@ -644,22 +641,24 @@ paths: type: object additionalProperties: type: string - /internal/files/{directory_type}: get: tags: - - internal + - internal summary: Get files description: Returns a list of files in a specific directory type operationId: getFiles parameters: - - name: directory_type - in: path - description: Type of directory (output, input, temp) - required: true - schema: - type: string - enum: [output, input, temp] + - name: directory_type + in: path + description: Type of directory (output, input, temp) + required: true + schema: + type: string + enum: + - output + - input + - temp responses: '200': description: Success @@ -671,13 +670,12 @@ paths: type: string '400': description: Invalid directory type - components: schemas: PromptRequest: type: object required: - - prompt + - prompt properties: prompt: type: object @@ -696,7 +694,6 @@ components: client_id: type: string description: Client ID for attribution of the prompt - PromptResponse: type: object properties: @@ -711,7 +708,6 @@ components: type: object description: Any errors in the nodes of the prompt additionalProperties: true - ErrorResponse: type: object properties: @@ -735,7 +731,6 @@ components: type: object description: Node-specific errors additionalProperties: true - PromptInfo: type: object properties: @@ -745,7 +740,6 @@ components: queue_remaining: type: integer description: Number of items remaining in the queue - QueueInfo: type: object properties: @@ -761,7 +755,6 @@ components: type: object description: Pending items in the queue additionalProperties: true - HistoryItem: type: object properties: @@ -781,7 +774,6 @@ components: type: object description: Output data from the execution additionalProperties: true - NodeInfo: type: object properties: @@ -843,7 +835,6 @@ components: api_node: type: boolean description: Whether this is an API node - SystemStats: type: object properties: @@ -901,4 +892,4 @@ components: description: Total VRAM as reported by PyTorch torch_vram_free: type: number - description: Free VRAM as reported by PyTorch \ No newline at end of file + description: Free VRAM as reported by PyTorch diff --git a/tests-api/conftest.py b/tests-api/conftest.py index 4e6df7330..7f373eff1 100644 --- a/tests-api/conftest.py +++ b/tests-api/conftest.py @@ -93,8 +93,8 @@ def api_client(base_url: str) -> Generator[Optional[requests.Session], None, Non # Helper function to construct URLs def get_url(path: str) -> str: - # All API endpoints use the /api prefix - return urljoin(base_url, '/api' + path) + # Paths in the OpenAPI spec already include /api prefix where needed + return urljoin(base_url, path) # Add url helper to the session session.get_url = get_url # type: ignore diff --git a/tests-api/test_endpoint_existence.py b/tests-api/test_endpoint_existence.py index 8340dc2ac..71864093e 100644 --- a/tests-api/test_endpoint_existence.py +++ b/tests-api/test_endpoint_existence.py @@ -77,12 +77,12 @@ def test_endpoints_exist(all_endpoints: List[Dict[str, Any]]): @pytest.mark.parametrize("endpoint_path", [ - "/", # Root path - "/prompt", # Get prompt info - "/queue", # Get queue - "/models", # Get model types - "/object_info", # Get node info - "/system_stats" # Get system stats + "/", # Root path (doesn't have /api prefix) + "/api/prompt", # Get prompt info + "/api/queue", # Get queue + "/api/models", # Get model types + "/api/object_info", # Get node info + "/api/system_stats" # Get system stats ]) def test_basic_get_endpoints(require_server, api_client, endpoint_path: str): """ @@ -116,7 +116,7 @@ def test_websocket_endpoint_exists(require_server, base_url: str): require_server: Fixture that skips if server is not available base_url: Base server URL """ - # WebSocket endpoint uses /api prefix + # WebSocket endpoint path from OpenAPI spec ws_url = urljoin(base_url, "/api/ws") # For WebSocket, we can't use a normal GET request @@ -143,7 +143,7 @@ def test_api_models_folder_endpoint(require_server, api_client): api_client: API client fixture """ # First get available model types - models_url = api_client.get_url("/models") # type: ignore + models_url = api_client.get_url("/api/models") # type: ignore try: models_response = api_client.get(models_url) @@ -157,14 +157,14 @@ def test_api_models_folder_endpoint(require_server, api_client): # Test with the first model type model_type = model_types[0] - models_folder_url = api_client.get_url(f"/models/{model_type}") # type: ignore + models_folder_url = api_client.get_url(f"/api/models/{model_type}") # type: ignore folder_response = api_client.get(models_folder_url) # We're just checking that the endpoint exists - assert folder_response.status_code != 404, f"Endpoint /models/{model_type} does not exist" + assert folder_response.status_code != 404, f"Endpoint /api/models/{model_type} does not exist" - logger.info(f"Endpoint /models/{model_type} exists with status code {folder_response.status_code}") + logger.info(f"Endpoint /api/models/{model_type} exists with status code {folder_response.status_code}") except requests.RequestException as e: pytest.fail(f"Request failed: {str(e)}") @@ -181,7 +181,7 @@ def test_api_object_info_node_endpoint(require_server, api_client): api_client: API client fixture """ # First get available node classes - objects_url = api_client.get_url("/object_info") # type: ignore + objects_url = api_client.get_url("/api/object_info") # type: ignore try: objects_response = api_client.get(objects_url) @@ -195,14 +195,14 @@ def test_api_object_info_node_endpoint(require_server, api_client): # Test with the first node class node_class = next(iter(node_classes.keys())) - node_url = api_client.get_url(f"/object_info/{node_class}") # type: ignore + node_url = api_client.get_url(f"/api/object_info/{node_class}") # type: ignore node_response = api_client.get(node_url) # We're just checking that the endpoint exists - assert node_response.status_code != 404, f"Endpoint /object_info/{node_class} does not exist" + assert node_response.status_code != 404, f"Endpoint /api/object_info/{node_class} does not exist" - logger.info(f"Endpoint /object_info/{node_class} exists with status code {node_response.status_code}") + logger.info(f"Endpoint /api/object_info/{node_class} exists with status code {node_response.status_code}") except requests.RequestException as e: pytest.fail(f"Request failed: {str(e)}") diff --git a/tests-api/test_schema_validation.py b/tests-api/test_schema_validation.py index 6b1143efe..65c374a14 100644 --- a/tests-api/test_schema_validation.py +++ b/tests-api/test_schema_validation.py @@ -132,11 +132,11 @@ logger = logging.getLogger(__name__) @pytest.mark.parametrize("endpoint_path,method", [ - ("/system_stats", "get"), - ("/prompt", "get"), - ("/queue", "get"), - ("/models", "get"), - ("/embeddings", "get") + ("/api/system_stats", "get"), + ("/api/prompt", "get"), + ("/api/queue", "get"), + ("/api/models", "get"), + ("/api/embeddings", "get") ]) def test_response_schema_validation( require_server, @@ -182,7 +182,7 @@ def test_response_schema_validation( return # Special handling for system_stats endpoint - if endpoint_path == '/system_stats' and isinstance(response_data, dict): + if endpoint_path == '/api/system_stats' and isinstance(response_data, dict): # Remove null index fields before validation for device in response_data.get('devices', []): if 'index' in device and device['index'] is None: @@ -217,7 +217,7 @@ def test_system_stats_response(require_server, api_client, api_spec: Dict[str, A api_client: API client fixture api_spec: Loaded OpenAPI spec """ - url = api_client.get_url("/system_stats") # type: ignore + url = api_client.get_url("/api/system_stats") # type: ignore try: response = api_client.get(url) @@ -259,7 +259,7 @@ def test_system_stats_response(require_server, api_client, api_spec: Dict[str, A validation_result = validate_response( stats, api_spec, - "/system_stats", + "/api/system_stats", "get" ) @@ -279,7 +279,7 @@ def test_system_stats_response(require_server, api_client, api_spec: Dict[str, A assert validation_result['valid'], "System stats response does not match schema" except requests.RequestException as e: - pytest.fail(f"Request to /system_stats failed: {str(e)}") + pytest.fail(f"Request to /api/system_stats failed: {str(e)}") def test_models_listing_response(require_server, api_client, api_spec: Dict[str, Any]): @@ -291,7 +291,7 @@ def test_models_listing_response(require_server, api_client, api_spec: Dict[str, api_client: API client fixture api_spec: Loaded OpenAPI spec """ - url = api_client.get_url("/models") # type: ignore + url = api_client.get_url("/api/models") # type: ignore try: response = api_client.get(url) @@ -312,7 +312,7 @@ def test_models_listing_response(require_server, api_client, api_spec: Dict[str, validation_result = validate_response( models, api_spec, - "/models", + "/api/models", "get" ) @@ -333,7 +333,7 @@ def test_models_listing_response(require_server, api_client, api_spec: Dict[str, assert validation_result['valid'], "Models response does not match schema" except requests.RequestException as e: - pytest.fail(f"Request to /models failed: {str(e)}") + pytest.fail(f"Request to /api/models failed: {str(e)}") def test_object_info_response(require_server, api_client, api_spec: Dict[str, Any]): @@ -345,7 +345,7 @@ def test_object_info_response(require_server, api_client, api_spec: Dict[str, An api_client: API client fixture api_spec: Loaded OpenAPI spec """ - url = api_client.get_url("/object_info") # type: ignore + url = api_client.get_url("/api/object_info") # type: ignore try: response = api_client.get(url) @@ -373,7 +373,7 @@ def test_object_info_response(require_server, api_client, api_spec: Dict[str, An validation_result = validate_response( objects, api_spec, - "/object_info", + "/api/object_info", "get" ) @@ -394,7 +394,7 @@ def test_object_info_response(require_server, api_client, api_spec: Dict[str, An assert validation_result['valid'], "Object info response does not match schema" except requests.RequestException as e: - pytest.fail(f"Request to /object_info failed: {str(e)}") + pytest.fail(f"Request to /api/object_info failed: {str(e)}") except (KeyError, StopIteration) as e: pytest.fail(f"Failed to process response: {str(e)}") @@ -408,7 +408,7 @@ def test_queue_response(require_server, api_client, api_spec: Dict[str, Any]): api_client: API client fixture api_spec: Loaded OpenAPI spec """ - url = api_client.get_url("/queue") # type: ignore + url = api_client.get_url("/api/queue") # type: ignore try: response = api_client.get(url) @@ -430,7 +430,7 @@ def test_queue_response(require_server, api_client, api_spec: Dict[str, Any]): validation_result = validate_response( queue, api_spec, - "/queue", + "/api/queue", "get" ) diff --git a/tests-api/test_spec_validation.py b/tests-api/test_spec_validation.py index 57c493a22..d5fbf8721 100644 --- a/tests-api/test_spec_validation.py +++ b/tests-api/test_spec_validation.py @@ -61,9 +61,9 @@ def test_workflow_endpoints_exist(api_spec: Dict[str, Any]): Args: api_spec: Loaded OpenAPI spec """ - assert '/prompt' in api_spec['paths'], "Spec must define /prompt endpoint" - assert 'post' in api_spec['paths']['/prompt'], "Spec must define POST /prompt" - assert 'get' in api_spec['paths']['/prompt'], "Spec must define GET /prompt" + assert '/api/prompt' in api_spec['paths'], "Spec must define /api/prompt endpoint" + assert 'post' in api_spec['paths']['/api/prompt'], "Spec must define POST /api/prompt" + assert 'get' in api_spec['paths']['/api/prompt'], "Spec must define GET /api/prompt" def test_image_endpoints_exist(api_spec: Dict[str, Any]): @@ -73,8 +73,8 @@ def test_image_endpoints_exist(api_spec: Dict[str, Any]): Args: api_spec: Loaded OpenAPI spec """ - assert '/upload/image' in api_spec['paths'], "Spec must define /upload/image endpoint" - assert '/view' in api_spec['paths'], "Spec must define /view endpoint" + assert '/api/upload/image' in api_spec['paths'], "Spec must define /api/upload/image endpoint" + assert '/api/view' in api_spec['paths'], "Spec must define /api/view endpoint" def test_model_endpoints_exist(api_spec: Dict[str, Any]): @@ -84,8 +84,8 @@ def test_model_endpoints_exist(api_spec: Dict[str, Any]): Args: api_spec: Loaded OpenAPI spec """ - assert '/models' in api_spec['paths'], "Spec must define /models endpoint" - assert '/models/{folder}' in api_spec['paths'], "Spec must define /models/{folder} endpoint" + assert '/api/models' in api_spec['paths'], "Spec must define /api/models endpoint" + assert '/api/models/{folder}' in api_spec['paths'], "Spec must define /api/models/{folder} endpoint" def test_operation_ids_are_unique(api_spec: Dict[str, Any]):