[api] Add /api prefix to all paths in OpenAPI spec

- Updated all API paths (except internal routes) to include /api/ prefix
- Changed server URL from "/api" back to "/" and added prefix to individual paths
- Updated test fixtures to not add /api prefix since paths already include it
- Fixed all test assertions to use the new paths with /api/ prefix

This addresses the review comment about endpoints needing the /api/ prefix
and implements it correctly by hardcoding the prefix in each path definition.

Fixes #8219
This commit is contained in:
bymyself 2025-06-29 18:36:51 -07:00
parent 82c1852390
commit d6270cbdf3
5 changed files with 221 additions and 230 deletions

View File

@ -1,42 +1,46 @@
openapi: 3.0.3 openapi: 3.0.3
info: info:
title: ComfyUI API title: ComfyUI API
description: | description: 'API for ComfyUI - A powerful and modular UI for Stable Diffusion.
API for ComfyUI - A powerful and modular UI for Stable Diffusion.
This API allows you to interact with ComfyUI programmatically, including: This API allows you to interact with ComfyUI programmatically, including:
- Submitting workflows for execution - Submitting workflows for execution
- Managing the execution queue - Managing the execution queue
- Retrieving generated images - Retrieving generated images
- Managing models - Managing models
- Retrieving node information - Retrieving node information
'
version: 1.0.0 version: 1.0.0
license: license:
name: GNU General Public License v3.0 name: GNU General Public License v3.0
url: https://github.com/comfyanonymous/ComfyUI/blob/master/LICENSE url: https://github.com/comfyanonymous/ComfyUI/blob/master/LICENSE
servers: servers:
- url: /api - url: /
description: Default ComfyUI server description: Default ComfyUI server
tags: tags:
- name: workflow - name: workflow
description: Workflow execution and management description: Workflow execution and management
- name: queue - name: queue
description: Queue management description: Queue management
- name: image - name: image
description: Image handling description: Image handling
- name: node - name: node
description: Node information description: Node information
- name: model - name: model
description: Model management description: Model management
- name: system - name: system
description: System information description: System information
- name: internal - name: internal
description: Internal API routes description: Internal API routes
paths: paths:
/prompt: /api/prompt:
get: get:
tags: tags:
- workflow - workflow
@ -54,9 +58,11 @@ paths:
tags: tags:
- workflow - workflow
summary: Submit a workflow for execution summary: Submit a workflow for execution
description: | description: 'Submit a workflow to be executed by the backend.
Submit a workflow to be executed by the backend.
The workflow is a JSON object describing the nodes and their connections. The workflow is a JSON object describing the nodes and their connections.
'
operationId: executePrompt operationId: executePrompt
requestBody: requestBody:
required: true required: true
@ -77,8 +83,7 @@ paths:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/ErrorResponse' $ref: '#/components/schemas/ErrorResponse'
/api/queue:
/queue:
get: get:
tags: tags:
- queue - queue
@ -117,8 +122,7 @@ paths:
responses: responses:
'200': '200':
description: Success description: Success
/api/interrupt:
/interrupt:
post: post:
tags: tags:
- workflow - workflow
@ -128,8 +132,7 @@ paths:
responses: responses:
'200': '200':
description: Success description: Success
/api/free:
/free:
post: post:
tags: tags:
- system - system
@ -152,8 +155,7 @@ paths:
responses: responses:
'200': '200':
description: Success description: Success
/api/history:
/history:
get: get:
tags: tags:
- workflow - workflow
@ -202,8 +204,7 @@ paths:
responses: responses:
'200': '200':
description: Success description: Success
/api/history/{prompt_id}:
/history/{prompt_id}:
get: get:
tags: tags:
- workflow - workflow
@ -225,8 +226,7 @@ paths:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/HistoryItem' $ref: '#/components/schemas/HistoryItem'
/api/object_info:
/object_info:
get: get:
tags: tags:
- node - node
@ -242,8 +242,7 @@ paths:
type: object type: object
additionalProperties: additionalProperties:
$ref: '#/components/schemas/NodeInfo' $ref: '#/components/schemas/NodeInfo'
/api/object_info/{node_class}:
/object_info/{node_class}:
get: get:
tags: tags:
- node - node
@ -266,8 +265,7 @@ paths:
type: object type: object
additionalProperties: additionalProperties:
$ref: '#/components/schemas/NodeInfo' $ref: '#/components/schemas/NodeInfo'
/api/upload/image:
/upload/image:
post: post:
tags: tags:
- image - image
@ -290,7 +288,10 @@ paths:
description: Whether to overwrite if file exists (true/false) description: Whether to overwrite if file exists (true/false)
type: type:
type: string type: string
enum: [input, temp, output] enum:
- input
- temp
- output
description: Type of directory to store the image in description: Type of directory to store the image in
subfolder: subfolder:
type: string type: string
@ -314,8 +315,7 @@ paths:
description: Type of directory the image was stored in description: Type of directory the image was stored in
'400': '400':
description: Bad request description: Bad request
/api/upload/mask:
/upload/mask:
post: post:
tags: tags:
- image - image
@ -355,8 +355,7 @@ paths:
description: Type of directory the mask was stored in description: Type of directory the mask was stored in
'400': '400':
description: Bad request description: Bad request
/api/view:
/view:
get: get:
tags: tags:
- image - image
@ -376,7 +375,10 @@ paths:
required: false required: false
schema: schema:
type: string type: string
enum: [input, temp, output] enum:
- input
- temp
- output
default: output default: output
- name: subfolder - name: subfolder
in: query in: query
@ -396,7 +398,10 @@ paths:
required: false required: false
schema: schema:
type: string type: string
enum: [rgb, a, rgba] enum:
- rgb
- a
- rgba
default: rgba default: rgba
responses: responses:
'200': '200':
@ -410,8 +415,7 @@ paths:
description: Bad request description: Bad request
'404': '404':
description: File not found description: File not found
/api/view_metadata/{folder_name}:
/view_metadata/{folder_name}:
get: get:
tags: tags:
- model - model
@ -440,8 +444,7 @@ paths:
type: object type: object
'404': '404':
description: File not found description: File not found
/api/models:
/models:
get: get:
tags: tags:
- model - model
@ -457,8 +460,7 @@ paths:
type: array type: array
items: items:
type: string type: string
/api/models/{folder}:
/models/{folder}:
get: get:
tags: tags:
- model - model
@ -483,8 +485,7 @@ paths:
type: string type: string
'404': '404':
description: Folder not found description: Folder not found
/api/embeddings:
/embeddings:
get: get:
tags: tags:
- model - model
@ -500,8 +501,7 @@ paths:
type: array type: array
items: items:
type: string type: string
/api/extensions:
/extensions:
get: get:
tags: tags:
- system - system
@ -517,8 +517,7 @@ paths:
type: array type: array
items: items:
type: string type: string
/api/system_stats:
/system_stats:
get: get:
tags: tags:
- system - system
@ -532,15 +531,17 @@ paths:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/SystemStats' $ref: '#/components/schemas/SystemStats'
/api/ws:
/ws:
get: get:
tags: tags:
- workflow - workflow
summary: WebSocket connection summary: WebSocket connection
description: | description: 'Establishes a WebSocket connection for real-time communication.
Establishes a WebSocket connection for real-time communication.
This endpoint is used for receiving progress updates, status changes, and results from workflow executions. This endpoint is used for receiving progress updates, status changes, and
results from workflow executions.
'
operationId: webSocketConnect operationId: webSocketConnect
parameters: parameters:
- name: clientId - name: clientId
@ -552,7 +553,6 @@ paths:
responses: responses:
'101': '101':
description: Switching Protocols to WebSocket description: Switching Protocols to WebSocket
/internal/logs: /internal/logs:
get: get:
tags: tags:
@ -567,7 +567,6 @@ paths:
application/json: application/json:
schema: schema:
type: string type: string
/internal/logs/raw: /internal/logs/raw:
get: get:
tags: tags:
@ -603,7 +602,6 @@ paths:
rows: rows:
type: integer type: integer
description: Terminal rows description: Terminal rows
/internal/logs/subscribe: /internal/logs/subscribe:
patch: patch:
tags: tags:
@ -627,7 +625,6 @@ paths:
responses: responses:
'200': '200':
description: Success description: Success
/internal/folder_paths: /internal/folder_paths:
get: get:
tags: tags:
@ -644,7 +641,6 @@ paths:
type: object type: object
additionalProperties: additionalProperties:
type: string type: string
/internal/files/{directory_type}: /internal/files/{directory_type}:
get: get:
tags: tags:
@ -659,7 +655,10 @@ paths:
required: true required: true
schema: schema:
type: string type: string
enum: [output, input, temp] enum:
- output
- input
- temp
responses: responses:
'200': '200':
description: Success description: Success
@ -671,7 +670,6 @@ paths:
type: string type: string
'400': '400':
description: Invalid directory type description: Invalid directory type
components: components:
schemas: schemas:
PromptRequest: PromptRequest:
@ -696,7 +694,6 @@ components:
client_id: client_id:
type: string type: string
description: Client ID for attribution of the prompt description: Client ID for attribution of the prompt
PromptResponse: PromptResponse:
type: object type: object
properties: properties:
@ -711,7 +708,6 @@ components:
type: object type: object
description: Any errors in the nodes of the prompt description: Any errors in the nodes of the prompt
additionalProperties: true additionalProperties: true
ErrorResponse: ErrorResponse:
type: object type: object
properties: properties:
@ -735,7 +731,6 @@ components:
type: object type: object
description: Node-specific errors description: Node-specific errors
additionalProperties: true additionalProperties: true
PromptInfo: PromptInfo:
type: object type: object
properties: properties:
@ -745,7 +740,6 @@ components:
queue_remaining: queue_remaining:
type: integer type: integer
description: Number of items remaining in the queue description: Number of items remaining in the queue
QueueInfo: QueueInfo:
type: object type: object
properties: properties:
@ -761,7 +755,6 @@ components:
type: object type: object
description: Pending items in the queue description: Pending items in the queue
additionalProperties: true additionalProperties: true
HistoryItem: HistoryItem:
type: object type: object
properties: properties:
@ -781,7 +774,6 @@ components:
type: object type: object
description: Output data from the execution description: Output data from the execution
additionalProperties: true additionalProperties: true
NodeInfo: NodeInfo:
type: object type: object
properties: properties:
@ -843,7 +835,6 @@ components:
api_node: api_node:
type: boolean type: boolean
description: Whether this is an API node description: Whether this is an API node
SystemStats: SystemStats:
type: object type: object
properties: properties:

View File

@ -93,8 +93,8 @@ def api_client(base_url: str) -> Generator[Optional[requests.Session], None, Non
# Helper function to construct URLs # Helper function to construct URLs
def get_url(path: str) -> str: def get_url(path: str) -> str:
# All API endpoints use the /api prefix # Paths in the OpenAPI spec already include /api prefix where needed
return urljoin(base_url, '/api' + path) return urljoin(base_url, path)
# Add url helper to the session # Add url helper to the session
session.get_url = get_url # type: ignore session.get_url = get_url # type: ignore

View File

@ -77,12 +77,12 @@ def test_endpoints_exist(all_endpoints: List[Dict[str, Any]]):
@pytest.mark.parametrize("endpoint_path", [ @pytest.mark.parametrize("endpoint_path", [
"/", # Root path "/", # Root path (doesn't have /api prefix)
"/prompt", # Get prompt info "/api/prompt", # Get prompt info
"/queue", # Get queue "/api/queue", # Get queue
"/models", # Get model types "/api/models", # Get model types
"/object_info", # Get node info "/api/object_info", # Get node info
"/system_stats" # Get system stats "/api/system_stats" # Get system stats
]) ])
def test_basic_get_endpoints(require_server, api_client, endpoint_path: str): 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 require_server: Fixture that skips if server is not available
base_url: Base server URL base_url: Base server URL
""" """
# WebSocket endpoint uses /api prefix # WebSocket endpoint path from OpenAPI spec
ws_url = urljoin(base_url, "/api/ws") ws_url = urljoin(base_url, "/api/ws")
# For WebSocket, we can't use a normal GET request # 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 api_client: API client fixture
""" """
# First get available model types # 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: try:
models_response = api_client.get(models_url) 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 # Test with the first model type
model_type = model_types[0] 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) folder_response = api_client.get(models_folder_url)
# We're just checking that the endpoint exists # 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: except requests.RequestException as e:
pytest.fail(f"Request failed: {str(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 api_client: API client fixture
""" """
# First get available node classes # 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: try:
objects_response = api_client.get(objects_url) 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 # Test with the first node class
node_class = next(iter(node_classes.keys())) 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) node_response = api_client.get(node_url)
# We're just checking that the endpoint exists # 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: except requests.RequestException as e:
pytest.fail(f"Request failed: {str(e)}") pytest.fail(f"Request failed: {str(e)}")

View File

@ -132,11 +132,11 @@ logger = logging.getLogger(__name__)
@pytest.mark.parametrize("endpoint_path,method", [ @pytest.mark.parametrize("endpoint_path,method", [
("/system_stats", "get"), ("/api/system_stats", "get"),
("/prompt", "get"), ("/api/prompt", "get"),
("/queue", "get"), ("/api/queue", "get"),
("/models", "get"), ("/api/models", "get"),
("/embeddings", "get") ("/api/embeddings", "get")
]) ])
def test_response_schema_validation( def test_response_schema_validation(
require_server, require_server,
@ -182,7 +182,7 @@ def test_response_schema_validation(
return return
# Special handling for system_stats endpoint # 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 # Remove null index fields before validation
for device in response_data.get('devices', []): for device in response_data.get('devices', []):
if 'index' in device and device['index'] is None: 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_client: API client fixture
api_spec: Loaded OpenAPI spec 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: try:
response = api_client.get(url) 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( validation_result = validate_response(
stats, stats,
api_spec, api_spec,
"/system_stats", "/api/system_stats",
"get" "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" assert validation_result['valid'], "System stats response does not match schema"
except requests.RequestException as e: 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]): 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_client: API client fixture
api_spec: Loaded OpenAPI spec api_spec: Loaded OpenAPI spec
""" """
url = api_client.get_url("/models") # type: ignore url = api_client.get_url("/api/models") # type: ignore
try: try:
response = api_client.get(url) 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( validation_result = validate_response(
models, models,
api_spec, api_spec,
"/models", "/api/models",
"get" "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" assert validation_result['valid'], "Models response does not match schema"
except requests.RequestException as e: 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]): 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_client: API client fixture
api_spec: Loaded OpenAPI spec 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: try:
response = api_client.get(url) 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( validation_result = validate_response(
objects, objects,
api_spec, api_spec,
"/object_info", "/api/object_info",
"get" "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" assert validation_result['valid'], "Object info response does not match schema"
except requests.RequestException as e: 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: except (KeyError, StopIteration) as e:
pytest.fail(f"Failed to process response: {str(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_client: API client fixture
api_spec: Loaded OpenAPI spec api_spec: Loaded OpenAPI spec
""" """
url = api_client.get_url("/queue") # type: ignore url = api_client.get_url("/api/queue") # type: ignore
try: try:
response = api_client.get(url) 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( validation_result = validate_response(
queue, queue,
api_spec, api_spec,
"/queue", "/api/queue",
"get" "get"
) )

View File

@ -61,9 +61,9 @@ def test_workflow_endpoints_exist(api_spec: Dict[str, Any]):
Args: Args:
api_spec: Loaded OpenAPI spec api_spec: Loaded OpenAPI spec
""" """
assert '/prompt' in api_spec['paths'], "Spec must define /prompt endpoint" assert '/api/prompt' in api_spec['paths'], "Spec must define /api/prompt endpoint"
assert 'post' in api_spec['paths']['/prompt'], "Spec must define POST /prompt" assert 'post' in api_spec['paths']['/api/prompt'], "Spec must define POST /api/prompt"
assert 'get' in api_spec['paths']['/prompt'], "Spec must define GET /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]): 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: Args:
api_spec: Loaded OpenAPI spec api_spec: Loaded OpenAPI spec
""" """
assert '/upload/image' in api_spec['paths'], "Spec must define /upload/image endpoint" assert '/api/upload/image' in api_spec['paths'], "Spec must define /api/upload/image endpoint"
assert '/view' in api_spec['paths'], "Spec must define /view endpoint" assert '/api/view' in api_spec['paths'], "Spec must define /api/view endpoint"
def test_model_endpoints_exist(api_spec: Dict[str, Any]): 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: Args:
api_spec: Loaded OpenAPI spec api_spec: Loaded OpenAPI spec
""" """
assert '/models' in api_spec['paths'], "Spec must define /models endpoint" assert '/api/models' in api_spec['paths'], "Spec must define /api/models endpoint"
assert '/models/{folder}' in api_spec['paths'], "Spec must define /models/{folder} 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]): def test_operation_ids_are_unique(api_spec: Dict[str, Any]):