From 9617546a9c9c26136cb62fa0c41dd2fed10d4bab Mon Sep 17 00:00:00 2001 From: Matt Miller Date: Thu, 3 Sep 2026 01:00:30 -0700 Subject: [PATCH 1/2] docs: use the real bfl/flux-2-pro id in the model-run examples MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The quickstart told a reader to call `fal-ai/flux-pro`, which cannot succeed: `flux-pro` is not an enrolled Comfy Router model under any provider, and `fal-ai` is not a Comfy provider slug either — the fal provider's slug is `fal`, and `fal-ai` is the upstream path segment inside it. Flux is served direct from Black Forest Labs under `bfl/*`. Replace it with `bfl/flux-2-pro` everywhere it appears — the README quickstart call, the `POST /v2/models/...` prose, the id-format bullet (including its three-segment variant example), the async example, the three `example:` values in the vendored Router spec, and the one percent-encoding fixture that used it as a synthetic id. Where the provider and model segments are shown apart they become `bfl` and `flux-2-pro`. Documentation and examples only: no generated code carried the string and no behaviour changes. --- README.md | 10 +++++----- spec/router-openapi.yaml | 6 +++--- tests/test_models_run.py | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index ac04659..02e1416 100644 --- a/README.md +++ b/README.md @@ -400,11 +400,11 @@ operations are added to this namespace as they land. ### `models.run` — one call, one result ```python -result = client.models.run("fal-ai/flux-pro", {"prompt": "a cat", "steps": 4}) +result = client.models.run("bfl/flux-2-pro", {"prompt": "a cat", "steps": 4}) result["images"][0]["url"] ``` -That call is `POST https://api.comfy.org/v2/models/fal-ai/flux-pro` with +That call is `POST https://api.comfy.org/v2/models/bfl/flux-2-pro` with `{"prompt": "a cat", "steps": 4}` as the body. Three things follow from that, and they are the whole contract of this method: @@ -413,8 +413,8 @@ Three things follow from that, and they are the whole contract of this method: `COMFY_ROUTER_BASE_URL`, not by `COMFY_BASE_URL`. Your API key goes with it. - **The first argument is the model's canonical id**, `{provider}/{model}` — exactly the two segments that address the route, and exactly what Router's - model catalog lists. It must be two non-empty segments: `"fal-ai"` alone, - `"fal-ai/flux-pro/fp8"` (the three-segment variant form, which this route does + model catalog lists. It must be two non-empty segments: `"bfl"` alone, + `"bfl/flux-2-pro/fp8"` (the three-segment variant form, which this route does not take yet), and anything containing a `.` or `..` segment all raise `ValueError` locally, before any request. A non-string raises `TypeError`. - **The second argument is the model's own native input**, forwarded to the @@ -432,7 +432,7 @@ The awaitable form is the **async client**, not a differently-named method: ```python async with AsyncComfy(api_key="comfyui-...") as client: - result = await client.models.run("fal-ai/flux-pro", {"prompt": "a cat"}) + result = await client.models.run("bfl/flux-2-pro", {"prompt": "a cat"}) ``` There is no `run_async()`, and there will not be one — one operation, one name, diff --git a/spec/router-openapi.yaml b/spec/router-openapi.yaml index fca146e..86a6e00 100644 --- a/spec/router-openapi.yaml +++ b/spec/router-openapi.yaml @@ -278,7 +278,7 @@ components: description: A canonical Comfy Router model ID, `{provider}/{model}` - exactly the value that addresses the model on `POST /v2/models/{provider}/{model}`, so a caller can interpolate it into that path without re-deriving it from anything. Its `pattern` is `RouterProviderSegment` and `RouterModelSegment` joined by a single `/`, and `maxLength` is their sum plus that separator. pattern: ^[a-z0-9]+([._-][a-z0-9]+)*/[a-z0-9]+([._-][a-z0-9]+)*$ maxLength: 193 - example: fal-ai/flux-pro + example: bfl/flux-2-pro RouterModelInput: type: object description: 'A partner model''s native JSON input document, forwarded to the provider as-is. Its concrete shape is owned by the partner rather than by Comfy, so this is an open object: Router does not narrow, rename, or re-envelope the fields. It is a named component (never an inline anonymous object) because ComfyUI''s spec-driven codegen needs a class to generate.' @@ -337,7 +337,7 @@ components: description: Lowercase `model` segment of the canonical `{provider}/{model}[/{variant}]` model ID - the model to run within that provider. Shared by the invocation route's `model` path parameter and a catalog entry's `model` field, for the same no-drift reason as `RouterProviderSegment`. pattern: ^[a-z0-9]+([._-][a-z0-9]+)*$ maxLength: 128 - example: flux-pro + example: flux-2-pro RouterPageCursor: type: string description: 'An OPAQUE cursor into a Router list. It is produced by the server and only ever round-tripped: it is not an offset, not a model ID, not ordered, and not stable across catalog rebuilds, so parsing one, incrementing one, or persisting one beyond the walk it came from are all outside the contract. Cursor rather than offset because the catalog is a moving list - an offset walk silently skips or repeats entries when entries are added or removed mid-walk, and a caller cannot tell that it happened.' @@ -350,7 +350,7 @@ components: description: Lowercase `provider` segment of the canonical `{provider}/{model}[/{variant}]` model ID - the partner whose model is being addressed. The invocation route's `provider` path parameter and a catalog entry's `provider` field both reference this one schema, which is what keeps the listed IDs and the accepted IDs from drifting apart. pattern: ^[a-z0-9]+([._-][a-z0-9]+)*$ maxLength: 64 - example: fal-ai + example: bfl RouterValidationErrorContext: type: object description: 'The violated bound for one `RouterValidationErrorDetail`, carried from the provider verbatim - for example `{"limit_value": 8}` alongside `greater_than`, `{"min_width": 512}` alongside `image_too_small`, or `{"max_size_bytes": 10485760}` alongside `file_too_large`. The key set is specific to the provider and the error type, so this is deliberately an open object: narrowing it to a fixed field list, or folding it into the `msg` string, is precisely how a ported integration compiles and then silently loses the branch that read the bound. Absent when the error type carries no bound.' diff --git a/tests/test_models_run.py b/tests/test_models_run.py index 9dbb2c5..fbe2bf1 100644 --- a/tests/test_models_run.py +++ b/tests/test_models_run.py @@ -137,7 +137,7 @@ def test_the_sans_io_request_builder_agrees_with_the_wire() -> None: # `RouterModelSegment` pattern, and none of them is percent-encoded: # they are unreserved (or sub-delims) in a path segment, so the URL the # caller reads in a log is the id they passed. - ("fal-ai/flux-pro", "/v2/models/fal-ai/flux-pro"), + ("bfl/flux-2-pro", "/v2/models/bfl/flux-2-pro"), ("acme/sd_xl.turbo", "/v2/models/acme/sd_xl.turbo"), ("acme_labs/v1.5", "/v2/models/acme_labs/v1.5"), # ...while anything that would change the *structure* of the path is From 9675e8e48b9b192719eff11ce788e702a326c583 Mon Sep 17 00:00:00 2001 From: Matt Miller Date: Thu, 3 Sep 2026 02:13:44 -0700 Subject: [PATCH 2/2] docs: leave the vendored Router spec alone; fix the id in README and tests only `spec/router-openapi.yaml` is a one-way vendored copy of the canonical Router contract. Both `spec/README.md` ("do not hand-edit either file") and `AGENTS.md` ("Vendored, synced one-way. Never hand-edit.") say so, and nothing in CI catches an in-place edit: `scripts/check_drift.py` and `tests/test_router_spec_contract.py` only compare `RouterErrorType.x-comfy-error-types`. So the three `example:` values this PR rewrote (`RouterModelId`, `RouterModelSegment`, `RouterProviderSegment`) would have diverged silently from upstream and either been reverted by the next sync or read as phantom churn in a diff reviewers are meant to take as pure upstream change. Reverts the vendored file to its `main` contents. The example values belong in the canonical contract and should arrive here via a sync; the README prose and the routing-test fixture are this repo's to fix and are unchanged by this commit. Co-Authored-By: Claude Opus 5 --- spec/router-openapi.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/router-openapi.yaml b/spec/router-openapi.yaml index 86a6e00..fca146e 100644 --- a/spec/router-openapi.yaml +++ b/spec/router-openapi.yaml @@ -278,7 +278,7 @@ components: description: A canonical Comfy Router model ID, `{provider}/{model}` - exactly the value that addresses the model on `POST /v2/models/{provider}/{model}`, so a caller can interpolate it into that path without re-deriving it from anything. Its `pattern` is `RouterProviderSegment` and `RouterModelSegment` joined by a single `/`, and `maxLength` is their sum plus that separator. pattern: ^[a-z0-9]+([._-][a-z0-9]+)*/[a-z0-9]+([._-][a-z0-9]+)*$ maxLength: 193 - example: bfl/flux-2-pro + example: fal-ai/flux-pro RouterModelInput: type: object description: 'A partner model''s native JSON input document, forwarded to the provider as-is. Its concrete shape is owned by the partner rather than by Comfy, so this is an open object: Router does not narrow, rename, or re-envelope the fields. It is a named component (never an inline anonymous object) because ComfyUI''s spec-driven codegen needs a class to generate.' @@ -337,7 +337,7 @@ components: description: Lowercase `model` segment of the canonical `{provider}/{model}[/{variant}]` model ID - the model to run within that provider. Shared by the invocation route's `model` path parameter and a catalog entry's `model` field, for the same no-drift reason as `RouterProviderSegment`. pattern: ^[a-z0-9]+([._-][a-z0-9]+)*$ maxLength: 128 - example: flux-2-pro + example: flux-pro RouterPageCursor: type: string description: 'An OPAQUE cursor into a Router list. It is produced by the server and only ever round-tripped: it is not an offset, not a model ID, not ordered, and not stable across catalog rebuilds, so parsing one, incrementing one, or persisting one beyond the walk it came from are all outside the contract. Cursor rather than offset because the catalog is a moving list - an offset walk silently skips or repeats entries when entries are added or removed mid-walk, and a caller cannot tell that it happened.' @@ -350,7 +350,7 @@ components: description: Lowercase `provider` segment of the canonical `{provider}/{model}[/{variant}]` model ID - the partner whose model is being addressed. The invocation route's `provider` path parameter and a catalog entry's `provider` field both reference this one schema, which is what keeps the listed IDs and the accepted IDs from drifting apart. pattern: ^[a-z0-9]+([._-][a-z0-9]+)*$ maxLength: 64 - example: bfl + example: fal-ai RouterValidationErrorContext: type: object description: 'The violated bound for one `RouterValidationErrorDetail`, carried from the provider verbatim - for example `{"limit_value": 8}` alongside `greater_than`, `{"min_width": 512}` alongside `image_too_small`, or `{"max_size_bytes": 10485760}` alongside `file_too_large`. The key set is specific to the provider and the error type, so this is deliberately an open object: narrowing it to a fixed field list, or folding it into the `msg` string, is precisely how a ported integration compiles and then silently loses the branch that read the bound. Absent when the error type carries no bound.'