mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-07-12 17:37:17 +08:00
Merge branch 'master' into synap5e/feat/experiment-models-extensions
This commit is contained in:
commit
84a4e466f7
46
comfy/comfy_api_env.py
Normal file
46
comfy/comfy_api_env.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
"""Runtime config the frontend reads from /features to follow --comfy-api-base.
|
||||||
|
|
||||||
|
For a non-prod comfy.org backend (staging or an ephemeral preview env), "/features" exposes the api and
|
||||||
|
platform base so the frontend talks to it without a rebuild, plus the Firebase environment it should use.
|
||||||
|
Prod bases are left alone and keep their build-time defaults.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
from comfy.cli_args import args
|
||||||
|
|
||||||
|
_STAGING_API_HOST = "stagingapi.comfy.org"
|
||||||
|
_TESTENV_HOST_SUFFIX = ".testenvs.comfy.org"
|
||||||
|
_STAGING_PLATFORM_BASE_URL = "https://stagingplatform.comfy.org"
|
||||||
|
|
||||||
|
|
||||||
|
def _is_staging_tier(host: str) -> bool:
|
||||||
|
return host == _STAGING_API_HOST or host.endswith(_TESTENV_HOST_SUFFIX)
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_comfy_api_base(url: str) -> str:
|
||||||
|
"""Rewrite a testenv's friendly main host to its comfy-api '-registry' sibling."""
|
||||||
|
parsed = urlparse(url)
|
||||||
|
host = parsed.hostname or ""
|
||||||
|
if not host.endswith(_TESTENV_HOST_SUFFIX):
|
||||||
|
return url
|
||||||
|
label = host[: -len(_TESTENV_HOST_SUFFIX)]
|
||||||
|
if label.endswith("-registry"):
|
||||||
|
return url
|
||||||
|
return f"{parsed.scheme or 'https'}://{label}-registry{_TESTENV_HOST_SUFFIX}"
|
||||||
|
|
||||||
|
|
||||||
|
def environment_overrides_for_base(base_url: str) -> dict[str, Any] | None:
|
||||||
|
"""The /features overrides for a staging-tier base, or None for prod."""
|
||||||
|
if not _is_staging_tier(urlparse(base_url).hostname or ""):
|
||||||
|
return None
|
||||||
|
return {
|
||||||
|
"comfy_api_base_url": normalize_comfy_api_base(base_url).rstrip("/"),
|
||||||
|
"comfy_platform_base_url": _STAGING_PLATFORM_BASE_URL,
|
||||||
|
"firebase_env": "dev",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_environment_overrides() -> dict[str, Any] | None:
|
||||||
|
return environment_overrides_for_base(getattr(args, "comfy_api_base", "") or "")
|
||||||
@ -90,6 +90,27 @@ class Qwen3VL(BaseLlama, BaseQwen3, BaseGenerate, torch.nn.Module):
|
|||||||
deepstack = [torch.cat([deepstack[i], ds[i]], dim=0) for i in range(len(ds))]
|
deepstack = [torch.cat([deepstack[i], ds[i]], dim=0) for i in range(len(ds))]
|
||||||
return position_ids, visual_pos_masks, deepstack
|
return position_ids, visual_pos_masks, deepstack
|
||||||
|
|
||||||
|
def forward(self, input_ids, attention_mask=None, embeds=None, num_tokens=None, intermediate_output=None, final_layer_norm_intermediate=True, dtype=None, embeds_info=[], **kwargs):
|
||||||
|
position_ids = kwargs.pop("position_ids", None)
|
||||||
|
visual_pos_masks = kwargs.pop("visual_pos_masks", None)
|
||||||
|
deepstack_embeds = kwargs.pop("deepstack_embeds", None)
|
||||||
|
if embeds is not None and position_ids is None:
|
||||||
|
position_ids, visual_pos_masks, deepstack_embeds = self.build_image_inputs(embeds, embeds_info)
|
||||||
|
return self.model(
|
||||||
|
input_ids,
|
||||||
|
attention_mask=attention_mask,
|
||||||
|
embeds=embeds,
|
||||||
|
num_tokens=num_tokens,
|
||||||
|
intermediate_output=intermediate_output,
|
||||||
|
final_layer_norm_intermediate=final_layer_norm_intermediate,
|
||||||
|
dtype=dtype,
|
||||||
|
position_ids=position_ids,
|
||||||
|
embeds_info=embeds_info,
|
||||||
|
visual_pos_masks=visual_pos_masks,
|
||||||
|
deepstack_embeds=deepstack_embeds,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _make_qwen3vl_model(model_type):
|
def _make_qwen3vl_model(model_type):
|
||||||
class Qwen3VL_(Qwen3VL):
|
class Qwen3VL_(Qwen3VL):
|
||||||
|
|||||||
@ -11,6 +11,7 @@ from io import BytesIO
|
|||||||
from yarl import URL
|
from yarl import URL
|
||||||
|
|
||||||
from comfy.cli_args import args
|
from comfy.cli_args import args
|
||||||
|
from comfy.comfy_api_env import normalize_comfy_api_base
|
||||||
from comfy.deploy_environment import get_deploy_environment
|
from comfy.deploy_environment import get_deploy_environment
|
||||||
from comfy.model_management import processing_interrupted
|
from comfy.model_management import processing_interrupted
|
||||||
from comfy_api.latest import IO
|
from comfy_api.latest import IO
|
||||||
@ -63,7 +64,7 @@ def get_comfy_api_headers(node_cls: type[IO.ComfyNode]) -> dict[str, str]:
|
|||||||
|
|
||||||
|
|
||||||
def default_base_url() -> str:
|
def default_base_url() -> str:
|
||||||
return getattr(args, "comfy_api_base", "https://api.comfy.org")
|
return normalize_comfy_api_base(getattr(args, "comfy_api_base", "https://api.comfy.org"))
|
||||||
|
|
||||||
|
|
||||||
async def sleep_with_interrupt(
|
async def sleep_with_interrupt(
|
||||||
|
|||||||
@ -61,14 +61,10 @@ class Load3D(IO.ComfyNode):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def execute(cls, model_file, image, **kwargs) -> IO.NodeOutput:
|
def execute(cls, model_file, image, **kwargs) -> IO.NodeOutput:
|
||||||
image_path = folder_paths.get_annotated_filepath(image['image'])
|
|
||||||
mask_path = folder_paths.get_annotated_filepath(image['mask'])
|
|
||||||
normal_path = folder_paths.get_annotated_filepath(image['normal'])
|
|
||||||
|
|
||||||
load_image_node = nodes.LoadImage()
|
load_image_node = nodes.LoadImage()
|
||||||
output_image, ignore_mask = load_image_node.load_image(image=image_path)
|
output_image, ignore_mask = load_image_node.load_image(image=image['image'])
|
||||||
ignore_image, output_mask = load_image_node.load_image(image=mask_path)
|
ignore_image, output_mask = load_image_node.load_image(image=image['mask'])
|
||||||
normal_image, ignore_mask2 = load_image_node.load_image(image=normal_path)
|
normal_image, ignore_mask2 = load_image_node.load_image(image=image['normal'])
|
||||||
|
|
||||||
video = None
|
video = None
|
||||||
|
|
||||||
|
|||||||
@ -39,6 +39,7 @@ from comfy.deploy_environment import get_deploy_environment
|
|||||||
import comfy.utils
|
import comfy.utils
|
||||||
import comfy.model_management
|
import comfy.model_management
|
||||||
from comfy_api import feature_flags
|
from comfy_api import feature_flags
|
||||||
|
from comfy.comfy_api_env import get_environment_overrides
|
||||||
import node_helpers
|
import node_helpers
|
||||||
from comfyui_version import __version__
|
from comfyui_version import __version__
|
||||||
from app.frontend_management import FrontendManager, parse_version
|
from app.frontend_management import FrontendManager, parse_version
|
||||||
@ -727,7 +728,11 @@ class PromptServer():
|
|||||||
|
|
||||||
@routes.get("/features")
|
@routes.get("/features")
|
||||||
async def get_features(request):
|
async def get_features(request):
|
||||||
return web.json_response(feature_flags.get_server_features())
|
features = feature_flags.get_server_features()
|
||||||
|
overrides = get_environment_overrides()
|
||||||
|
if overrides:
|
||||||
|
features.update(overrides)
|
||||||
|
return web.json_response(features)
|
||||||
|
|
||||||
@routes.get("/prompt")
|
@routes.get("/prompt")
|
||||||
async def get_prompt(request):
|
async def get_prompt(request):
|
||||||
|
|||||||
@ -11,6 +11,11 @@ from comfy_api.feature_flags import (
|
|||||||
_coerce_flag_value,
|
_coerce_flag_value,
|
||||||
_parse_cli_feature_flags,
|
_parse_cli_feature_flags,
|
||||||
)
|
)
|
||||||
|
from comfy.comfy_api_env import (
|
||||||
|
environment_overrides_for_base,
|
||||||
|
get_environment_overrides,
|
||||||
|
normalize_comfy_api_base,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestFeatureFlags:
|
class TestFeatureFlags:
|
||||||
@ -183,3 +188,65 @@ class TestCliFeatureFlagRegistry:
|
|||||||
assert "type" in info, f"{key} missing 'type'"
|
assert "type" in info, f"{key} missing 'type'"
|
||||||
assert "default" in info, f"{key} missing 'default'"
|
assert "default" in info, f"{key} missing 'default'"
|
||||||
assert "description" in info, f"{key} missing 'description'"
|
assert "description" in info, f"{key} missing 'description'"
|
||||||
|
|
||||||
|
|
||||||
|
class TestComfyApiEnv:
|
||||||
|
"""--comfy-api-base staging-tier detection + testenv main-host -> -registry rewrite."""
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"url, expected",
|
||||||
|
[
|
||||||
|
# testenv friendly main host -> comfy-api -registry sibling (slash trimmed)
|
||||||
|
("https://pr-4398.testenvs.comfy.org", "https://pr-4398-registry.testenvs.comfy.org"),
|
||||||
|
("https://pr-4398.testenvs.comfy.org/", "https://pr-4398-registry.testenvs.comfy.org"),
|
||||||
|
("https://pr-4398-registry.testenvs.comfy.org", "https://pr-4398-registry.testenvs.comfy.org"),
|
||||||
|
# staging + everything else -> unchanged (no -registry split)
|
||||||
|
("https://stagingapi.comfy.org", "https://stagingapi.comfy.org"),
|
||||||
|
("https://api.comfy.org", "https://api.comfy.org"),
|
||||||
|
("https://pr-1.testenvs.comfy.org.evil.com", "https://pr-1.testenvs.comfy.org.evil.com"),
|
||||||
|
("", ""),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_normalize_comfy_api_base(self, url, expected):
|
||||||
|
assert normalize_comfy_api_base(url) == expected
|
||||||
|
|
||||||
|
def test_config_for_staging_tier_else_none(self):
|
||||||
|
# ephemeral testenv: friendly main host -> -registry, staging platform, dev Firebase env
|
||||||
|
eph = environment_overrides_for_base("https://pr-1234.testenvs.comfy.org/")
|
||||||
|
assert eph["comfy_api_base_url"] == "https://pr-1234-registry.testenvs.comfy.org"
|
||||||
|
assert eph["comfy_platform_base_url"] == "https://stagingplatform.comfy.org"
|
||||||
|
assert eph["firebase_env"] == "dev"
|
||||||
|
# staging api host: emitted as-is
|
||||||
|
stg = environment_overrides_for_base("https://stagingapi.comfy.org")
|
||||||
|
assert stg["comfy_api_base_url"] == "https://stagingapi.comfy.org"
|
||||||
|
assert stg["comfy_platform_base_url"] == "https://stagingplatform.comfy.org"
|
||||||
|
assert stg["firebase_env"] == "dev"
|
||||||
|
# prod / unknown: nothing
|
||||||
|
assert environment_overrides_for_base("https://api.comfy.org") is None
|
||||||
|
|
||||||
|
def test_environment_overrides_only_for_staging_tier(self, monkeypatch):
|
||||||
|
def set_base(url):
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"comfy.comfy_api_env.args",
|
||||||
|
type("Args", (), {"comfy_api_base": url})(),
|
||||||
|
)
|
||||||
|
|
||||||
|
# The overrides merged into the HTTP /features response are present for staging-tier bases...
|
||||||
|
set_base("https://stagingapi.comfy.org")
|
||||||
|
assert "comfy_api_base_url" in get_environment_overrides()
|
||||||
|
set_base("https://pr-7.testenvs.comfy.org")
|
||||||
|
assert "comfy_api_base_url" in get_environment_overrides()
|
||||||
|
# ...but never for prod.
|
||||||
|
set_base("https://api.comfy.org")
|
||||||
|
assert get_environment_overrides() is None
|
||||||
|
|
||||||
|
def test_server_features_never_carry_env_overrides(self, monkeypatch):
|
||||||
|
"""The WebSocket capability handshake must stay free of routing keys."""
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"comfy.comfy_api_env.args",
|
||||||
|
type("Args", (), {"comfy_api_base": "https://pr-7.testenvs.comfy.org"})(),
|
||||||
|
)
|
||||||
|
features = get_server_features()
|
||||||
|
assert "comfy_api_base_url" not in features
|
||||||
|
assert "comfy_platform_base_url" not in features
|
||||||
|
assert "firebase_env" not in features
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user