[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,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
description: Free VRAM as reported by PyTorch

View File

@ -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

View File

@ -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)}")

View File

@ -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"
)

View File

@ -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]):