-
Notifications
You must be signed in to change notification settings - Fork 152
feat(models): add --host/--port to search/list-folders/list-folder/show #876
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,8 @@ | ||
| """``comfy models`` — live model discovery against local or cloud. | ||
|
|
||
| Four subcommands, all routed by ``--where`` (cloud auto-detect by default): | ||
| Four subcommands, all routed by ``--where`` (cloud auto-detect by default); | ||
| each also accepts ``--host``/``--port`` to aim a *local* query at a specific | ||
| ComfyUI (rejected against a cloud target), mirroring ``comfy upload``: | ||
|
|
||
| comfy models list-folders # GET /api/experiment/models | /models | ||
| comfy models list-folder <folder> # GET /api/experiment/models/<folder> | /models/<folder> | ||
|
|
@@ -178,18 +180,91 @@ def _emit_http_error(e: urllib.error.HTTPError, *, renderer, target, message: st | |
| raise typer.Exit(code=1) from e | ||
|
|
||
|
|
||
| def _resolve_and_stamp(renderer, where: str | None): | ||
| # How a `cloud` routing decision was reached, in words, for the --host/--port | ||
| # rejection message. Keys are `where.WhereResolution.source` values. Mirrors the | ||
| # copy `cmdline.upload` uses (BE-5662) — kept local rather than imported so this | ||
| # module stays free of a `cmdline` import cycle. | ||
| _WHERE_SOURCE_PHRASES = { | ||
| "flag": "targeting cloud via --where cloud", | ||
| "env": "targeting cloud via the COMFY_WHERE environment variable", | ||
| "project": "targeting cloud via this project's configured default", | ||
| "config": "targeting cloud via your saved `where_default` setting", | ||
| "auto": "targeting cloud because you're signed in (no explicit --where)", | ||
| } | ||
|
|
||
|
|
||
| def _resolve_and_stamp(renderer, where: str | None, *, host: str | None = None, port: int | None = None): | ||
| """Resolve the routing Target for a ``models`` verb and stamp it on the renderer. | ||
|
|
||
| Every verb here calls this at the point it decides local-vs-cloud, so the | ||
| error envelopes raised downstream carry ``where`` instead of ``null``. | ||
| Errors raised *before* this (an unsafe path segment) keep ``where: null``, | ||
| which is correct — nothing had routed yet. Explicit | ||
| ``emit(..., where=...)`` arguments still take precedence over the stamp. | ||
|
|
||
| ``host``/``port`` route a **local** ``models`` query at a specific ComfyUI | ||
| (the ``--host``/``--port`` flags), mirroring ``comfy upload`` (BE-5662): | ||
| they are validated the same way, rejected against an effective ``cloud`` | ||
| target, and otherwise handed to ``resolve_target``, which applies the local | ||
| precedence explicit flag > ``COMFY_LOCAL_URL`` > ``127.0.0.1:8188``. With | ||
| both ``None`` (no flags), resolution is exactly what it was before the flags | ||
| existed. The cloud target's address comes from the signed-in account and | ||
| ignores host/port entirely, so pairing them with a cloud target is a usage | ||
| error rather than a silently-ignored flag — the root cause of the comfy-mcp | ||
| bug where ``search_models`` answered from the local machine regardless of | ||
| the configured remote target. | ||
| """ | ||
| from comfy_cli import where as where_module | ||
| from comfy_cli.config_manager import ConfigManager | ||
| from comfy_cli.host_port import report_usage_error, validate_host | ||
| from comfy_cli.target import resolve_target | ||
|
|
||
| target = resolve_target(where=where) | ||
| # Validate the flags before resolving anything: the host lands verbatim in | ||
| # ``http://{host}:{port}/...``, so a URL-special or control character is a | ||
| # usage error (BadParameter, exit 2) regardless of target. Port range is | ||
| # checked the same way ``comfy upload`` does. ``report_usage_error`` emits | ||
| # the terminating envelope for that rejection in JSON/NDJSON mode; the | ||
| # exception still escapes, so click's exit-2 usage contract is unchanged. | ||
| with report_usage_error(renderer): | ||
| if host is not None: | ||
| host = validate_host(host) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Medium — |
||
| if port is not None and not (1 <= port <= 65535): | ||
| raise typer.BadParameter(f"invalid port: {port} is out of range (1-65535)") | ||
|
|
||
| try: | ||
| decision = where_module.resolve( | ||
| flag=where, config_value=ConfigManager().get(where_module.CONFIG_KEY_WHERE_DEFAULT) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: sed -n '180,270p' comfy_cli/command/models/search.py
sed -n '1,240p' comfy_cli/target.py
sed -n '1,260p' comfy_cli/config_manager.py
git diff -- comfy_cli/command/models/search.py tests/comfy_cli/command/models/test_search.py
rg -n 'ConfigManager|resolve_target|malformed|config.*fallback|CONFIG_KEY_WHERE_DEFAULT' tests comfy_cliRepository: Comfy-Org/comfy-cli Length of output: 50375 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- focused diff ---'
git diff --unified=40 -- comfy_cli/command/models/search.py tests/comfy_cli/command/models/test_search.py
printf '%s\n' '--- where module ---'
sed -n '1,260p' comfy_cli/where.py
printf '%s\n' '--- model search tests: routing sections ---'
sed -n '140,230p' tests/comfy_cli/command/models/test_search.py
sed -n '880,1040p' tests/comfy_cli/command/models/test_search.py
printf '%s\n' '--- search command callers ---'
rg -n -C 5 '_resolve_and_stamp|models (search|list|download|info)|def (search|list|download|info)' comfy_cli/command/models tests/comfy_cli/command/models/test_search.pyRepository: Comfy-Org/comfy-cli Length of output: 43474 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- repository state ---'
git status --short
git log -2 --oneline -- comfy_cli/command/models/search.py
printf '%s\n' '--- current helper from HEAD ---'
git show HEAD:comfy_cli/command/models/search.py | sed -n '196,275p'
printf '%s\n' '--- parent helper ---'
git show HEAD^:comfy_cli/command/models/search.py | sed -n '180,265p'
printf '%s\n' '--- commit diff for helper/tests ---'
git diff --unified=25 HEAD^ HEAD -- comfy_cli/command/models/search.py tests/comfy_cli/command/models/test_search.py | sed -n '1,260p'
printf '%s\n' '--- all current helper tests ---'
rg -n -C 8 '_resolve_and_stamp|no_flags|malformed.*config|config.*exception|where_default' tests/comfy_cli/command/models/test_search.py tests/comfy_cli/auth/test_where.pyRepository: Comfy-Org/comfy-cli Length of output: 28212 Preserve the configuration-loading fallback. Before this change, all four commands called The current helper reads Catch configuration-loading exceptions separately and pass Proposed fix- try:
- decision = where_module.resolve(
- flag=where, config_value=ConfigManager().get(where_module.CONFIG_KEY_WHERE_DEFAULT)
- )
+ try:
+ config_value = ConfigManager().get(where_module.CONFIG_KEY_WHERE_DEFAULT)
+ except Exception: # noqa: BLE001 — preserve resolve_target's bad-config fallback
+ config_value = None
+
+ try:
+ decision = where_module.resolve(flag=where, config_value=config_value)
except ValueError as e:🤖 Prompt for AI AgentsThere was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Medium — |
||
| ) | ||
| except ValueError as e: | ||
| renderer.error(code="where_invalid", message=str(e), hint="use --where local or --where cloud") | ||
| raise typer.Exit(code=1) from e | ||
|
|
||
| effective_where = "cloud" if decision.target is where_module.WhereTarget.CLOUD else "local" | ||
| # Routing is decided, so every error envelope from here down can name the | ||
| # target — including the `host_flag_cloud` rejection immediately below. | ||
| renderer.where = effective_where | ||
| # --host/--port address a local ComfyUI; the cloud target's address comes | ||
| # from the signed-in account and ignores them (``Target.host``/``port`` are | ||
| # documented local-only). Reject the combination rather than silently | ||
| # answering from a machine the caller didn't name. | ||
| if effective_where == "cloud" and (host is not None or port is not None): | ||
| # The cloud target can come from an explicit --where, but equally from | ||
| # COMFY_WHERE, a project/config default, or credential auto-detection — | ||
| # so name the source rather than accusing the user of a flag they may | ||
| # never have typed. | ||
| source = _WHERE_SOURCE_PHRASES.get(decision.source, f"resolved to cloud by {decision.source}") | ||
| renderer.error( | ||
| code="host_flag_cloud", | ||
| message=f"--host/--port target a local ComfyUI server, but this run is {source}", | ||
| hint=( | ||
| "pass --where local to aim at a local server; to reach a different cloud address " | ||
| "set COMFY_CLOUD_BASE_URL or run `comfy cloud set-base-url`" | ||
| ), | ||
| details={"host": host, "port": port, "where": effective_where, "where_source": decision.source}, | ||
| ) | ||
| raise typer.Exit(code=1) | ||
|
|
||
| target = resolve_target(where=effective_where, host=host, port=port) | ||
| renderer.where = target.kind | ||
| return target | ||
|
|
||
|
|
@@ -211,9 +286,17 @@ def list_folders_cmd( | |
| str | None, | ||
| typer.Option("--where", show_default=False, help="Override the resolved routing mode."), | ||
| ] = None, | ||
| host: Annotated[ | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟢 Low — A credential-bearing host like |
||
| str | None, | ||
| typer.Option(help="Server host (defaults to COMFY_LOCAL_URL or 127.0.0.1). Local targets only."), | ||
| ] = None, | ||
| port: Annotated[ | ||
| int | None, | ||
| typer.Option(help="Server port (defaults to COMFY_LOCAL_URL or 8188). Local targets only."), | ||
| ] = None, | ||
| ): | ||
| renderer = get_renderer() | ||
| target = _resolve_and_stamp(renderer, where) | ||
| target = _resolve_and_stamp(renderer, where, host=host, port=port) | ||
| url = target.url(*_models_path_parts(target)) | ||
|
|
||
| try: | ||
|
|
@@ -286,10 +369,18 @@ def list_folder_cmd( | |
| int | None, | ||
| typer.Option("--limit", show_default=False, help="Cap output to N rows."), | ||
| ] = None, | ||
| host: Annotated[ | ||
| str | None, | ||
| typer.Option(help="Server host (defaults to COMFY_LOCAL_URL or 127.0.0.1). Local targets only."), | ||
| ] = None, | ||
| port: Annotated[ | ||
| int | None, | ||
| typer.Option(help="Server port (defaults to COMFY_LOCAL_URL or 8188). Local targets only."), | ||
| ] = None, | ||
| ): | ||
| renderer = get_renderer() | ||
| _reject_unsafe_path_segment(folder, kind="folder", renderer=renderer) | ||
| target = _resolve_and_stamp(renderer, where) | ||
| target = _resolve_and_stamp(renderer, where, host=host, port=port) | ||
| # Percent-encoded for the same reason `_local_folder_matches` does it: the | ||
| # relaxed validation above admits spaces, `?`/`#`, and non-ASCII, none of | ||
| # which may be allowed to alter the request. Error payloads below carry the | ||
|
|
@@ -662,11 +753,19 @@ def search_cmd( | |
| str | None, | ||
| typer.Option("--where", show_default=False, help="Override the resolved routing mode."), | ||
| ] = None, | ||
| host: Annotated[ | ||
| str | None, | ||
| typer.Option(help="Server host (defaults to COMFY_LOCAL_URL or 127.0.0.1). Local targets only."), | ||
| ] = None, | ||
| port: Annotated[ | ||
| int | None, | ||
| typer.Option(help="Server port (defaults to COMFY_LOCAL_URL or 8188). Local targets only."), | ||
| ] = None, | ||
| ): | ||
| renderer = get_renderer() | ||
| if type_ is not None: | ||
| _reject_unsafe_path_segment(type_, kind="type", renderer=renderer) | ||
| target = _resolve_and_stamp(renderer, where) | ||
| target = _resolve_and_stamp(renderer, where, host=host, port=port) | ||
|
|
||
| try: | ||
| if target.is_cloud: | ||
|
|
@@ -738,9 +837,17 @@ def show_cmd( | |
| str | None, | ||
| typer.Option("--where", show_default=False, help="Override the resolved routing mode."), | ||
| ] = None, | ||
| host: Annotated[ | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟢 Low — |
||
| str | None, | ||
| typer.Option(help="Server host (defaults to COMFY_LOCAL_URL or 127.0.0.1). Local targets only."), | ||
| ] = None, | ||
| port: Annotated[ | ||
| int | None, | ||
| typer.Option(help="Server port (defaults to COMFY_LOCAL_URL or 8188). Local targets only."), | ||
| ] = None, | ||
| ): | ||
| renderer = get_renderer() | ||
| target = _resolve_and_stamp(renderer, where) | ||
| target = _resolve_and_stamp(renderer, where, host=host, port=port) | ||
|
|
||
| if not target.is_cloud: | ||
| # On local there's no asset catalog. We can confirm the file exists by | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟢 Low —
report_usage_erroris called withoutcommand=, as are thewhere_invalidandhost_flag_cloudemissions below, contrary to the helper's documented contract of passing it wherever the success envelope does. Because_resolve_and_stampis shared across the four verbs it can't name the subcommand itself, so these failure envelopes carry the renderer default (model/modelsor nothing) while successes carrymodels list-folders,models search, etc.; thread the verb name down from each call site. Raised by 4 of 6 reviewers (claude-opus-5-thinking-max adversarial, gpt-5.6-sol-max adversarial, kimi-k3-high adversarial, gpt-5.6-sol-max edge-case).