mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-05-10 01:02:56 +08:00
Compare commits
5 Commits
5130628ed5
...
a67c8a1d3b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a67c8a1d3b | ||
|
|
4e823431cc | ||
|
|
01730bd730 | ||
|
|
fe1ac8963d | ||
|
|
66669b2ded |
@ -160,10 +160,12 @@ def _build_asset_response(result: schemas.AssetDetailResult | schemas.UploadResu
|
||||
preview_url = None
|
||||
else:
|
||||
preview_url = _build_preview_url_from_view(result.tags, result.ref.user_metadata)
|
||||
asset_content_hash = result.asset.hash if result.asset else None
|
||||
return schemas_out.Asset(
|
||||
id=result.ref.id,
|
||||
name=result.ref.name,
|
||||
asset_hash=result.asset.hash if result.asset else None,
|
||||
hash=asset_content_hash,
|
||||
asset_hash=asset_content_hash,
|
||||
size=int(result.asset.size_bytes) if result.asset else None,
|
||||
mime_type=result.asset.mime_type if result.asset else None,
|
||||
tags=result.tags,
|
||||
|
||||
@ -10,6 +10,7 @@ class Asset(BaseModel):
|
||||
|
||||
id: str
|
||||
name: str
|
||||
hash: str | None = None
|
||||
asset_hash: str | None = None
|
||||
size: int | None = None
|
||||
mime_type: str | None = None
|
||||
|
||||
@ -1390,7 +1390,7 @@ def convert_old_quants(state_dict, model_prefix="", metadata={}):
|
||||
k_out = "{}.weight_scale".format(layer)
|
||||
|
||||
if layer is not None:
|
||||
layer_conf = {"format": "float8_e4m3fn"} # TODO: check if anyone did some non e4m3fn scaled checkpoints
|
||||
layer_conf = {"format": "float8_e4m3fn"}
|
||||
if full_precision_matrix_mult:
|
||||
layer_conf["full_precision_matrix_mult"] = full_precision_matrix_mult
|
||||
layers[layer] = layer_conf
|
||||
|
||||
106
openapi.yaml
106
openapi.yaml
@ -74,6 +74,8 @@ tags:
|
||||
description: Cloud workflow management and versioning (cloud-only)
|
||||
- name: task
|
||||
description: Background task management (cloud-only)
|
||||
- name: runtime-only
|
||||
description: Operations served exclusively by the cloud runtime with no local equivalent
|
||||
|
||||
paths:
|
||||
# ---------------------------------------------------------------------------
|
||||
@ -2573,35 +2575,38 @@ paths:
|
||||
# ---------------------------------------------------------------------------
|
||||
/api/experiment/nodes:
|
||||
get:
|
||||
operationId: listCloudNodes
|
||||
tags: [node]
|
||||
summary: List installed custom nodes
|
||||
description: "[cloud-only] Returns the list of custom node packages installed in the cloud runtime."
|
||||
operationId: getNodeInfoSchema
|
||||
tags: [runtime-only]
|
||||
summary: Get pre-rendered node info schema
|
||||
description: "[cloud-only] Returns the static ComfyUI object_info schema, identical for every caller, rendered once at startup with empty model/user-file context. Served by a raw HTTP handler that writes pre-rendered bytes with ETag + Cache-Control validators for RFC 7232 conditional GETs."
|
||||
x-runtime: [cloud]
|
||||
parameters:
|
||||
- name: limit
|
||||
in: query
|
||||
- name: If-None-Match
|
||||
in: header
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
description: Maximum number of results
|
||||
- name: offset
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
description: Pagination offset
|
||||
type: string
|
||||
description: Entity tag previously returned by this endpoint. When present and matching, the server returns 304 Not Modified.
|
||||
responses:
|
||||
"200":
|
||||
description: Custom node list
|
||||
description: Node info schema
|
||||
headers:
|
||||
ETag:
|
||||
schema:
|
||||
type: string
|
||||
description: Entity tag for conditional request validation
|
||||
Cache-Control:
|
||||
schema:
|
||||
type: string
|
||||
description: Cache directives for the response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/CloudNodeList"
|
||||
"401":
|
||||
description: Unauthorized
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/CloudError"
|
||||
type: object
|
||||
additionalProperties:
|
||||
$ref: "#/components/schemas/NodeInfo"
|
||||
"304":
|
||||
description: Not Modified — returned when the client sends a matching If-None-Match header
|
||||
post:
|
||||
operationId: installCloudNode
|
||||
tags: [node]
|
||||
@ -2651,10 +2656,10 @@ paths:
|
||||
|
||||
/api/experiment/nodes/{id}:
|
||||
get:
|
||||
operationId: getCloudNode
|
||||
tags: [node]
|
||||
summary: Get details of an installed custom node
|
||||
description: "[cloud-only] Returns details about a specific installed custom node package."
|
||||
operationId: getNodeByID
|
||||
tags: [runtime-only]
|
||||
summary: Get a single node definition by ID
|
||||
description: "[cloud-only] Returns one node's definition from the pre-indexed object_info schema. Served by a raw HTTP handler that writes pre-rendered bytes with ETag + Cache-Control validators for RFC 7232 conditional GETs."
|
||||
x-runtime: [cloud]
|
||||
parameters:
|
||||
- name: id
|
||||
@ -2662,26 +2667,33 @@ paths:
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: Custom node package ID
|
||||
description: Node class identifier
|
||||
- name: If-None-Match
|
||||
in: header
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
description: Entity tag previously returned by this endpoint. When present and matching, the server returns 304 Not Modified.
|
||||
responses:
|
||||
"200":
|
||||
description: Node detail
|
||||
description: Single node definition
|
||||
headers:
|
||||
ETag:
|
||||
schema:
|
||||
type: string
|
||||
description: Entity tag for conditional request validation
|
||||
Cache-Control:
|
||||
schema:
|
||||
type: string
|
||||
description: Cache directives for the response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/CloudNode"
|
||||
"401":
|
||||
description: Unauthorized
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/CloudError"
|
||||
$ref: "#/components/schemas/NodeInfo"
|
||||
"304":
|
||||
description: Not Modified — returned when the client sends a matching If-None-Match header
|
||||
"404":
|
||||
description: Not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/CloudError"
|
||||
description: Node not found
|
||||
delete:
|
||||
operationId: uninstallCloudNode
|
||||
tags: [node]
|
||||
@ -7100,22 +7112,6 @@ components:
|
||||
enabled:
|
||||
type: boolean
|
||||
|
||||
CloudNodeList:
|
||||
type: object
|
||||
x-runtime: [cloud]
|
||||
description: "[cloud-only] Paginated list of installed custom node packages."
|
||||
required:
|
||||
- nodes
|
||||
properties:
|
||||
nodes:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/CloudNode"
|
||||
total:
|
||||
type: integer
|
||||
has_more:
|
||||
type: boolean
|
||||
|
||||
HubLabel:
|
||||
type: object
|
||||
x-runtime: [cloud]
|
||||
|
||||
@ -236,6 +236,8 @@ def seeded_asset(request: pytest.FixtureRequest, http: requests.Session, api_bas
|
||||
r = http.post(api_base + "/api/assets", files=files, data=form_data, timeout=120)
|
||||
body = r.json()
|
||||
assert r.status_code == 201, body
|
||||
from helpers import assert_hash_fields_consistent
|
||||
assert_hash_fields_consistent(body)
|
||||
return body
|
||||
|
||||
|
||||
|
||||
@ -26,3 +26,26 @@ def trigger_sync_seed_assets(session: requests.Session, base_url: str) -> None:
|
||||
|
||||
def get_asset_filename(asset_hash: str, extension: str) -> str:
|
||||
return asset_hash.removeprefix("blake3:") + extension
|
||||
|
||||
|
||||
def assert_hash_fields_consistent(body: dict, expected_hash: str | None = None) -> None:
|
||||
"""Assert hash and asset_hash invariants on an Asset response.
|
||||
|
||||
Both must be present or both absent (so a regression that drops only one
|
||||
is caught). When present, they must equal each other and, if expected_hash
|
||||
is provided, must equal that value.
|
||||
"""
|
||||
hash_present = "hash" in body
|
||||
asset_hash_present = "asset_hash" in body
|
||||
assert hash_present == asset_hash_present, (
|
||||
f"hash and asset_hash must both be present or both absent: "
|
||||
f"hash present={hash_present}, asset_hash present={asset_hash_present}"
|
||||
)
|
||||
if hash_present:
|
||||
h = body["hash"]
|
||||
ah = body["asset_hash"]
|
||||
assert h == ah, f"hash and asset_hash must match: hash={h!r}, asset_hash={ah!r}"
|
||||
if expected_hash is not None:
|
||||
assert h == expected_hash, (
|
||||
f"hash must equal expected: got {h!r}, expected {expected_hash!r}"
|
||||
)
|
||||
|
||||
@ -40,7 +40,9 @@ def test_seed_asset_removed_when_file_is_deleted(
|
||||
# there should be exactly one with that name
|
||||
matches = [a for a in body1.get("assets", []) if a.get("name") == name]
|
||||
assert matches
|
||||
assert matches[0].get("asset_hash") is None
|
||||
# Seed assets have no hash; exclude_none drops both keys from the response
|
||||
assert "asset_hash" not in matches[0]
|
||||
assert "hash" not in matches[0]
|
||||
asset_info_id = matches[0]["id"]
|
||||
|
||||
# Remove the underlying file and sync again
|
||||
|
||||
@ -21,6 +21,8 @@ def test_create_from_hash_success(
|
||||
b1 = r1.json()
|
||||
assert r1.status_code == 201, b1
|
||||
assert b1["asset_hash"] == h
|
||||
assert b1["hash"] == h
|
||||
assert b1["hash"] == b1["asset_hash"]
|
||||
assert b1["created_new"] is False
|
||||
aid = b1["id"]
|
||||
|
||||
@ -39,6 +41,7 @@ def test_get_and_delete_asset(http: requests.Session, api_base: str, seeded_asse
|
||||
detail = rg.json()
|
||||
assert rg.status_code == 200, detail
|
||||
assert detail["id"] == aid
|
||||
assert detail["hash"] == detail["asset_hash"]
|
||||
assert "user_metadata" in detail
|
||||
assert "filename" in detail["user_metadata"]
|
||||
|
||||
@ -97,6 +100,7 @@ def test_delete_upon_reference_count(
|
||||
copy = r2.json()
|
||||
assert r2.status_code == 201, copy
|
||||
assert copy["asset_hash"] == src_hash
|
||||
assert copy["hash"] == src_hash
|
||||
assert copy["created_new"] is False
|
||||
|
||||
# Soft-delete original reference (default) -> asset identity must remain
|
||||
@ -139,6 +143,7 @@ def test_update_asset_fields(http: requests.Session, api_base: str, seeded_asset
|
||||
body = ru.json()
|
||||
assert ru.status_code == 200, body
|
||||
assert body["name"] == payload["name"]
|
||||
assert body["hash"] == body["asset_hash"]
|
||||
assert body["tags"] == original_tags # tags unchanged
|
||||
assert body["user_metadata"]["purpose"] == "updated"
|
||||
# filename should still be present and normalized by server
|
||||
@ -289,7 +294,9 @@ def test_metadata_filename_is_set_for_seed_asset_without_hash(
|
||||
assert r1.status_code == 200, body
|
||||
matches = [a for a in body.get("assets", []) if a.get("name") == name]
|
||||
assert matches, "Seed asset should be visible after sync"
|
||||
assert matches[0].get("asset_hash") is None # still a seed
|
||||
# Seed assets have no hash; exclude_none drops both keys from the response
|
||||
assert "asset_hash" not in matches[0]
|
||||
assert "hash" not in matches[0]
|
||||
aid = matches[0]["id"]
|
||||
|
||||
r2 = http.get(f"{api_base}/api/assets/{aid}", timeout=120)
|
||||
|
||||
@ -3,6 +3,7 @@ import uuid
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
from helpers import assert_hash_fields_consistent
|
||||
|
||||
|
||||
def test_list_assets_paging_and_sort(http: requests.Session, api_base: str, asset_factory, make_asset_bytes):
|
||||
@ -26,6 +27,10 @@ def test_list_assets_paging_and_sort(http: requests.Session, api_base: str, asse
|
||||
got1 = [a["name"] for a in b1["assets"]]
|
||||
assert got1 == sorted(names)[:2]
|
||||
assert b1["has_more"] is True
|
||||
# Populated assets in list responses must carry both `hash` and `asset_hash` consistently
|
||||
for asset in b1["assets"]:
|
||||
assert_hash_fields_consistent(asset)
|
||||
assert "hash" in asset, "populated asset must emit hash on list endpoint"
|
||||
|
||||
r2 = http.get(
|
||||
api_base + "/api/assets",
|
||||
|
||||
@ -5,6 +5,20 @@ from concurrent.futures import ThreadPoolExecutor
|
||||
import requests
|
||||
import pytest
|
||||
|
||||
from app.assets.api.schemas_out import Asset, AssetCreated
|
||||
|
||||
|
||||
def test_asset_created_inherits_hash_field():
|
||||
"""AssetCreated must inherit `hash` from Asset so POST /api/assets responses emit it.
|
||||
|
||||
Schema-level guard: integration tests cover the wire shape, but inheritance
|
||||
drift (e.g. AssetCreated ever being redefined to no longer extend Asset)
|
||||
would silently drop `hash` from a major endpoint without this check.
|
||||
"""
|
||||
assert "hash" in Asset.model_fields
|
||||
assert "hash" in AssetCreated.model_fields
|
||||
assert AssetCreated.model_fields["hash"].annotation == Asset.model_fields["hash"].annotation
|
||||
|
||||
|
||||
def test_upload_ok_duplicate_reference(http: requests.Session, api_base: str, make_asset_bytes):
|
||||
name = "dup_a.safetensors"
|
||||
@ -17,6 +31,7 @@ def test_upload_ok_duplicate_reference(http: requests.Session, api_base: str, ma
|
||||
a1 = r1.json()
|
||||
assert r1.status_code == 201, a1
|
||||
assert a1["created_new"] is True
|
||||
assert a1["hash"] == a1["asset_hash"]
|
||||
|
||||
# Second upload with the same data and name creates a new AssetReference (duplicates allowed)
|
||||
# Returns 200 because Asset already exists, but a new AssetReference is created
|
||||
@ -26,6 +41,7 @@ def test_upload_ok_duplicate_reference(http: requests.Session, api_base: str, ma
|
||||
a2 = r2.json()
|
||||
assert r2.status_code in (200, 201), a2
|
||||
assert a2["asset_hash"] == a1["asset_hash"]
|
||||
assert a2["hash"] == a1["hash"]
|
||||
assert a2["id"] != a1["id"] # new reference with same content
|
||||
|
||||
# Third upload with the same data but different name also creates new AssetReference
|
||||
@ -50,6 +66,7 @@ def test_upload_fastpath_from_existing_hash_no_file(http: requests.Session, api_
|
||||
b1 = r1.json()
|
||||
assert r1.status_code == 201, b1
|
||||
h = b1["asset_hash"]
|
||||
assert b1["hash"] == h
|
||||
|
||||
# Now POST /api/assets with only hash and no file
|
||||
files = [
|
||||
@ -63,6 +80,7 @@ def test_upload_fastpath_from_existing_hash_no_file(http: requests.Session, api_
|
||||
assert r2.status_code == 200, b2 # fast path returns 200 with created_new == False
|
||||
assert b2["created_new"] is False
|
||||
assert b2["asset_hash"] == h
|
||||
assert b2["hash"] == h
|
||||
|
||||
|
||||
def test_upload_fastpath_with_known_hash_and_file(
|
||||
@ -75,6 +93,7 @@ def test_upload_fastpath_with_known_hash_and_file(
|
||||
b1 = r1.json()
|
||||
assert r1.status_code == 201, b1
|
||||
h = b1["asset_hash"]
|
||||
assert b1["hash"] == h
|
||||
|
||||
# Send both file and hash of existing content -> server must drain file and create from hash (200)
|
||||
files = {"file": ("ignored.bin", b"ignored" * 10, "application/octet-stream")}
|
||||
@ -84,6 +103,7 @@ def test_upload_fastpath_with_known_hash_and_file(
|
||||
assert r2.status_code == 200, b2
|
||||
assert b2["created_new"] is False
|
||||
assert b2["asset_hash"] == h
|
||||
assert b2["hash"] == h
|
||||
|
||||
|
||||
def test_upload_multiple_tags_fields_are_merged(http: requests.Session, api_base: str):
|
||||
@ -142,6 +162,8 @@ def test_concurrent_upload_identical_bytes_different_names(
|
||||
assert r1.status_code in (200, 201), b1
|
||||
assert r2.status_code in (200, 201), b2
|
||||
assert b1["asset_hash"] == b2["asset_hash"]
|
||||
assert b1["hash"] == b2["hash"]
|
||||
assert b1["hash"] == b1["asset_hash"]
|
||||
assert b1["id"] != b2["id"]
|
||||
|
||||
created_flags = sorted([bool(b1.get("created_new")), bool(b2.get("created_new"))])
|
||||
|
||||
Loading…
Reference in New Issue
Block a user