[PR #137] [MERGED] feat(models): queue a model request with submit, subscribe and handle #137

Closed
opened 2026-10-31 22:01:42 +00:00 by bsakaguchi · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/Comfy-Org/comfy-python-sdk/pull/137
Author: @mattmillerai
Created: 9/9/2026
Status: ✅ Merged
Merged: 9/14/2026
Merged by: @mattmillerai

Base: main ← Head: matt/be-12725-models-queue-handle


📝 Commits (4)

  • 4d03724 feat(models): queue a model request with submit, subscribe and handle
  • a7a8e11 fix: address bot review comments on the queued model surface
  • 0f5426d fix: address bot review comments on the queue tests and result docs
  • d83b342 Merge origin/main into matt/be-12725-models-queue-handle

📊 Changes

12 files changed (+2553 additions, -26 deletions)

View changed files

📝 CHANGELOG.md (+28 -0)
📝 README.md (+114 -5)
📝 src/comfy_low/transport.py (+243 -3)
📝 src/comfy_sdk/__init__.py (+6 -0)
📝 src/comfy_sdk/client.py (+3 -2)
📝 src/comfy_sdk/exceptions.py (+11 -10)
➕ src/comfy_sdk/model_requests.py (+717 -0)
📝 src/comfy_sdk/models.py (+290 -2)
📝 src/comfy_sdk/router_exceptions.py (+58 -0)
📝 tests/conftest.py (+244 -2)
➕ tests/test_models_queue.py (+830 -0)
📝 tests/test_sync_async_parity.py (+9 -2)

📄 Description

ELI-5

client.models.run(...) sits on the phone until your picture is ready. That's fine for a script and useless for a web server. This adds the other way to do the same thing: client.models.submit(...) hands the request to the queue and gives you back a ticket stub, and you go and get the picture later — from a different process if you like, because the stub is just two ids. subscribe(...) is the same thing with the waiting done for you and a callback for the progress bar. The queue itself is entirely the server's — this only knows how to ask "is it done yet?" politely, at the pace the server asks for.

What this adds

handle = client.models.submit("fal-ai/flux-pro", {"prompt": "a cat"})
handle.request_id            # 'req_...' — all another process needs
result = handle.get()        # poll to completion, return the provider's payload

# ...or in one call, with progress
result = client.models.subscribe(model, args, on_queue_update=print, timeout=300)

# ...or from somewhere that never made the submit
result = client.models.handle(model, request_id).get()
models.submit(model, arguments) queue it; returns a RequestHandle
models.subscribe(model, arguments, on_queue_update=…, timeout=…) submit + poll + collect
models.handle(model, request_id) rehydrate a handle from the two ids, no request made
RequestHandle.status() / .get() / .cancel() / .iter_events() the handle

Plus QueueUpdate (the poll observation) and router_exceptions.error_from_completion().

The shape is deliberately the one comfy_sdk.jobs already uses for workflow jobs — submit, hold a handle, poll to terminal with adaptive backoff, collect or cancel — rather than a second idiom for the same thing. No queue behaviour is implemented client-side: ordering, admission, retries, timeouts, billing and expiry are all the server's, and this adds polling and ergonomics on top and nothing else.

Three properties that are contract rather than implementation detail:

  • Poll-authoritative, server-paced. There is no stream on this surface, so iter_events is the poll loop with its updates exposed, not SSE. It backs off adaptively and a Retry-After the server names on a poll beats that schedule; a Retry-After: 0 names no pace and falls back to the schedule rather than becoming a zero-delay loop.
  • A 200 is not a success here. The server reports a failed and a cancelled request as COMPLETED carrying an error_type, so every path that hands back a result runs the payload through the typed RouterError mapping first. An error_type this version has never heard of still raises the catchable base class.
  • One Idempotency-Key per submit call. Two deliberate submits are two requests; a transport-level retry inside one call keeps the one key and replays the original. Every exception carries it on .idempotency_key, which is the only route back to a request whose response was lost.

AsyncComfy awaits the same names with the same arguments in the same order. The existing introspection parity test discovers the new RequestHandle/AsyncRequestHandle pair and the three new models methods automatically; I extended its one explicit assertion to name them so the coverage cannot go vacuous.

client.models.run is unchanged. The only edits to models.py outside the new methods are two import lines (git diff on that file shows exactly two removed lines, both imports).

The one thing to look at first

The four routes are hand-bound and provisional:

_MODEL_REQUESTS_PATH_TEMPLATE        = _MODEL_RUN_PATH_TEMPLATE + "/requests"
_MODEL_REQUEST_PATH_TEMPLATE         = _MODEL_REQUESTS_PATH_TEMPLATE + "/{request_id}"
_MODEL_REQUEST_STATUS_PATH_TEMPLATE  = _MODEL_REQUEST_PATH_TEMPLATE + "/status"
_MODEL_REQUEST_CANCEL_PATH_TEMPLATE  = _MODEL_REQUEST_PATH_TEMPLATE + "/cancel"

The upstream contract that declares these four operations is authored but held, and the one-way sync into spec/router-openapi.yaml strips a held operation — so unlike _MODEL_RUN_PATH_TEMPLATE, which tests/test_router_spec_contract.py pins against the vendored file byte for byte, these have nothing to pin them to. They are gathered in one place and nowhere else precisely so the sync that publishes them is a four-line diff plus the assertion that pins them, exactly as the run path was hand-bound before its own spec arrived. test_the_queue_routes_extend_the_run_route pins what can be pinned today (the relationship to the run path and the segments each template fills) and says in its docstring what to replace it with.

Wire shapes assumed, all derived from what the surface itself forces rather than invented freely: the submit response names request_id; the status body carries status / queue_position / error_type; COMPLETED is the one terminal status. That both submit and handle need the model id and the request id is what says the request routes hang under the model-addressed prefix. Every one of these is parsed defensively — a missing field degrades rather than raising a decode error, an unknown status is treated as not yet terminal, and an unknown error_type still raises a catchable RouterError.

Judgment calls

  • No submit_async / subscribe_async aliases. The plan lists them as aliases; this repo's tests/test_sync_async_parity.py bans the _async suffix anywhere on the public surface (_SUFFIXED), models.py states there is deliberately no run_async "and there never will be", and the acceptance criterion itself asks for identical method names on AsyncComfy. Adding the aliases would mean weakening an existing guard to publish a second name for one operation, which cannot be withdrawn once released. I implemented the identical-names half and left the aliases out.
  • AsyncModels.handle is async def even though it makes no request. The alternative was declaring it a deliberate asymmetry in the parity test's _SYNC_ON_BOTH table. Awaiting it keeps the "add await" contract exception-free, needs no edit to that table, and leaves room to validate server-side later without a breaking change.
  • No client-side feature flag. The gate is the server's: an unflagged caller is answered 403 not_enabled, which arrives as the typed NotEnabled (tested). A second, client-side switch would be a local re-statement of a decision made somewhere authoritative — the same reason no queue behaviour is reimplemented here. The release-lifecycle half is the pre-release, below.
  • On the result route, an error_type only counts inside the queue envelope. That body is the provider's own payload forwarded verbatim, and a partner model is free to have a field called error_type in its native schema; raising on one would fail a generation that succeeded. So the result body is only read as a failure when it carries a COMPLETED status alongside it. The status read has no such ambiguity and is checked unconditionally, so the authoritative report of a failure is never the one that gets missed. Both directions are tested.
  • subscribe's TimeoutError catch is scoped to the iteration alone, not to the callback. A progress callback that makes its own HTTP call can raise TimeoutError for reasons that have nothing to do with this wait, and cancelling a healthy queued request on the strength of it would throw a charge away. Tested.

Verification of the pre-release path

The release-candidate publish is a human step, but the tooling half is checkable and worth knowing before the tag is cut: publish.yml validates the release tag as SemVer, where a pre-release is a --separated segment. Measured against that check —

tag result
v0.2.0-rc1 accepted
v0.2.0-rc.1 accepted
v0.2.0rc1 rejected (this is PEP 440's own spelling)

and building with the accepted form produces comfy_sdk-0.2.0rc1-py3-none-any.whl, twine check PASSED — the version normalises to PEP 440 on the way through, so pip install comfy-sdk will not pick it up without --pre, which is the point of shipping one. The README's Releases section now says all of this.

Provenance

  • Authored by: agent-work loop
  • Verified: uv run --extra dev pytest -q: 774 passed, 4 skipped, 0 failed (53 of them new, in tests/test_models_queue.py); ruff check . clean; ruff format --check .: 53 files already formatted; mypy src: no issues in 20 files; scripts/check_public_repo_hygiene.py: no internal-only references; scripts/check_drift.py: models in sync, all 15 router error types covered, run path matches the vendored spec; a local python -m build of the tree at version 0.2.0-rc1 → comfy_sdk-0.2.0rc1-py3-none-any.whl, twine check PASSED.
  • Deviations: the submit_async / subscribe_async aliases in the plan are deliberately not implemented (reason above); the release-candidate publish is not done here (it is a human step — see Residual).

Residual

Not fixed by this PR, and actionable on its own:

  1. The four route templates are unverified against a real deployment, and against the contract that declares them. The upstream OpenAPI operations are authored but held, so they are absent from spec/router-openapi.yaml and there was nothing to compare against; the queued surface is also gated server-side, so there was no deployment I could exercise it against from here. Everything in this PR is proven against the recorded stub in tests/conftest.py, which is a stub I wrote to my own assumption of the shape — it cannot falsify that assumption. When the operations land in the vendored spec, reconcile the _MODEL_REQUEST* constants in src/comfy_low/transport.py against it and extend tests/test_router_spec_contract.py to pin them the way it pins the run path (test_run_path_matches_vendored_spec currently asserts the spec declares exactly one POST path, so it will need to admit the new ones). If the real routes differ, the four constants and the stub's four regexes are the whole change; the handle and the namespace methods above them are route-agnostic.
  2. The response field names are assumed, not read from a contract. request_id, status, queue_position, error_type, detail, and COMPLETED as the one terminal status. Each is parsed defensively so a mismatch degrades rather than crashes — but a wrong field name degrades silently into "the queue never completes" (an unknown status is treated as not-yet-terminal) rather than into an error. Worth an explicit check against the contract in the same pass as item 1.
  3. The internal design appendix carrying the exact signatures and examples was not readable from here, nor was the parent epic. The surface implemented here is the one the ticket text itself enumerates; if the appendix pins a different parameter name or ordering, that is a published-signature change and is cheaper to catch before the pre-release than after.
  4. A server-issued request_id containing / is refused (ComfyError, at submit, rather than at the first poll). Under the route shape above a request id is a single path segment, so this cannot arise — but it is a guard written against my own assumption, and it belongs in the item-1 reconciliation. It follows the discipline the shipped parse_model_id already applies to the model id's segments.
  5. The release-candidate publish itself is not done, and is not this PR's to do — the ticket names it as human follow-through. What is done: the tag spelling the publish workflow will and will not accept is measured above, and an RC wheel was built and twine checked locally. To ship it: merge this, then create a GitHub Release tagged v0.2.0-rc1 (not v0.2.0rc1). A plain release rather than a pre-release would not satisfy the ticket even though the code would be identical.
  6. Diff size. ~2,000 lines, above the 400-line guideline for one unattended PR. It is one coherent feature in one module family — roughly 60% of it is docstrings and tests, matching this repo's house style — and splitting it would have produced PRs that could not be reviewed against each other. Flagging it rather than pretending it is small.

Summary by CodeRabbit

  • New Features

    • Added synchronous and asynchronous queued model requests with submission, subscription, and request-handle workflows.
    • Added queue status updates, progress-event iteration, result retrieval, cancellation, and timeout handling.
    • Added server-paced polling, idempotency-key support, request-handle rehydration, and typed errors for failed completed requests.
    • Added feature gating for environments where queued requests are unavailable.
  • Documentation

    • Documented queued request workflows, asynchronous usage, polling, cancellation, errors, idempotency, and release versioning guidance.

🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/Comfy-Org/comfy-python-sdk/pull/137 **Author:** [@mattmillerai](https://github.com/mattmillerai) **Created:** 9/9/2026 **Status:** ✅ Merged **Merged:** 9/14/2026 **Merged by:** [@mattmillerai](https://github.com/mattmillerai) **Base:** `main` ← **Head:** `matt/be-12725-models-queue-handle` --- ### 📝 Commits (4) - [`4d03724`](https://github.com/Comfy-Org/comfy-python-sdk/commit/4d03724dcf22a11314f599b1063beebf5eafa8ce) feat(models): queue a model request with submit, subscribe and handle - [`a7a8e11`](https://github.com/Comfy-Org/comfy-python-sdk/commit/a7a8e118e4a14c77be721d6ce1eb9c1ea590db17) fix: address bot review comments on the queued model surface - [`0f5426d`](https://github.com/Comfy-Org/comfy-python-sdk/commit/0f5426dce20005c4e19fc8f73bcb113533c305ee) fix: address bot review comments on the queue tests and result docs - [`d83b342`](https://github.com/Comfy-Org/comfy-python-sdk/commit/d83b34272ee48566b5c52b274e6e9dcda8f55d79) Merge origin/main into matt/be-12725-models-queue-handle ### 📊 Changes **12 files changed** (+2553 additions, -26 deletions) <details> <summary>View changed files</summary> 📝 `CHANGELOG.md` (+28 -0) 📝 `README.md` (+114 -5) 📝 `src/comfy_low/transport.py` (+243 -3) 📝 `src/comfy_sdk/__init__.py` (+6 -0) 📝 `src/comfy_sdk/client.py` (+3 -2) 📝 `src/comfy_sdk/exceptions.py` (+11 -10) ➕ `src/comfy_sdk/model_requests.py` (+717 -0) 📝 `src/comfy_sdk/models.py` (+290 -2) 📝 `src/comfy_sdk/router_exceptions.py` (+58 -0) 📝 `tests/conftest.py` (+244 -2) ➕ `tests/test_models_queue.py` (+830 -0) 📝 `tests/test_sync_async_parity.py` (+9 -2) </details> ### 📄 Description ## ELI-5 `client.models.run(...)` sits on the phone until your picture is ready. That's fine for a script and useless for a web server. This adds the other way to do the same thing: `client.models.submit(...)` hands the request to the queue and gives you back a ticket stub, and you go and get the picture later — from a different process if you like, because the stub is just two ids. `subscribe(...)` is the same thing with the waiting done for you and a callback for the progress bar. The queue itself is entirely the server's — this only knows how to ask "is it done yet?" politely, at the pace the server asks for. ## What this adds ```python handle = client.models.submit("fal-ai/flux-pro", {"prompt": "a cat"}) handle.request_id # 'req_...' — all another process needs result = handle.get() # poll to completion, return the provider's payload # ...or in one call, with progress result = client.models.subscribe(model, args, on_queue_update=print, timeout=300) # ...or from somewhere that never made the submit result = client.models.handle(model, request_id).get() ``` | | | |---|---| | `models.submit(model, arguments)` | queue it; returns a `RequestHandle` | | `models.subscribe(model, arguments, on_queue_update=…, timeout=…)` | submit + poll + collect | | `models.handle(model, request_id)` | rehydrate a handle from the two ids, no request made | | `RequestHandle.status()` / `.get()` / `.cancel()` / `.iter_events()` | the handle | Plus `QueueUpdate` (the poll observation) and `router_exceptions.error_from_completion()`. The shape is deliberately the one `comfy_sdk.jobs` already uses for workflow jobs — submit, hold a handle, poll to terminal with adaptive backoff, collect or cancel — rather than a second idiom for the same thing. **No queue behaviour is implemented client-side**: ordering, admission, retries, timeouts, billing and expiry are all the server's, and this adds polling and ergonomics on top and nothing else. Three properties that are contract rather than implementation detail: - **Poll-authoritative, server-paced.** There is no stream on this surface, so `iter_events` is the poll loop with its updates exposed, not SSE. It backs off adaptively and a `Retry-After` the server names on a poll beats that schedule; a `Retry-After: 0` names no pace and falls back to the schedule rather than becoming a zero-delay loop. - **A `200` is not a success here.** The server reports a failed *and* a cancelled request as `COMPLETED` carrying an `error_type`, so every path that hands back a result runs the payload through the typed `RouterError` mapping first. An `error_type` this version has never heard of still raises the catchable base class. - **One `Idempotency-Key` per `submit` *call*.** Two deliberate submits are two requests; a transport-level retry inside one call keeps the one key and replays the original. Every exception carries it on `.idempotency_key`, which is the only route back to a request whose response was lost. `AsyncComfy` awaits the same names with the same arguments in the same order. The existing introspection parity test discovers the new `RequestHandle`/`AsyncRequestHandle` pair and the three new `models` methods automatically; I extended its one explicit assertion to name them so the coverage cannot go vacuous. **`client.models.run` is unchanged.** The only edits to `models.py` outside the new methods are two import lines (`git diff` on that file shows exactly two removed lines, both imports). ## The one thing to look at first The four routes are **hand-bound and provisional**: ```python _MODEL_REQUESTS_PATH_TEMPLATE = _MODEL_RUN_PATH_TEMPLATE + "/requests" _MODEL_REQUEST_PATH_TEMPLATE = _MODEL_REQUESTS_PATH_TEMPLATE + "/{request_id}" _MODEL_REQUEST_STATUS_PATH_TEMPLATE = _MODEL_REQUEST_PATH_TEMPLATE + "/status" _MODEL_REQUEST_CANCEL_PATH_TEMPLATE = _MODEL_REQUEST_PATH_TEMPLATE + "/cancel" ``` The upstream contract that declares these four operations is authored but **held**, and the one-way sync into `spec/router-openapi.yaml` strips a held operation — so unlike `_MODEL_RUN_PATH_TEMPLATE`, which `tests/test_router_spec_contract.py` pins against the vendored file byte for byte, these have nothing to pin them to. They are gathered in one place and nowhere else precisely so the sync that publishes them is a four-line diff plus the assertion that pins them, exactly as the run path was hand-bound before its own spec arrived. `test_the_queue_routes_extend_the_run_route` pins what *can* be pinned today (the relationship to the run path and the segments each template fills) and says in its docstring what to replace it with. Wire shapes assumed, all derived from what the surface itself forces rather than invented freely: the submit response names `request_id`; the status body carries `status` / `queue_position` / `error_type`; `COMPLETED` is the one terminal status. That both `submit` and `handle` need the model id *and* the request id is what says the request routes hang under the model-addressed prefix. Every one of these is parsed defensively — a missing field degrades rather than raising a decode error, an unknown status is treated as *not yet terminal*, and an unknown `error_type` still raises a catchable `RouterError`. ## Judgment calls - **No `submit_async` / `subscribe_async` aliases.** The plan lists them as aliases; this repo's `tests/test_sync_async_parity.py` bans the `_async` suffix anywhere on the public surface (`_SUFFIXED`), `models.py` states there is deliberately no `run_async` "and there never will be", and the acceptance criterion itself asks for *identical* method names on `AsyncComfy`. Adding the aliases would mean weakening an existing guard to publish a second name for one operation, which cannot be withdrawn once released. I implemented the identical-names half and left the aliases out. - **`AsyncModels.handle` is `async def` even though it makes no request.** The alternative was declaring it a deliberate asymmetry in the parity test's `_SYNC_ON_BOTH` table. Awaiting it keeps the "add `await`" contract exception-free, needs no edit to that table, and leaves room to validate server-side later without a breaking change. - **No client-side feature flag.** The gate is the server's: an unflagged caller is answered `403 not_enabled`, which arrives as the typed `NotEnabled` (tested). A second, client-side switch would be a local re-statement of a decision made somewhere authoritative — the same reason no queue behaviour is reimplemented here. The release-lifecycle half is the pre-release, below. - **On the result route, an `error_type` only counts inside the queue envelope.** That body is the provider's own payload forwarded verbatim, and a partner model is free to have a field called `error_type` in its native schema; raising on one would fail a generation that succeeded. So the result body is only read as a failure when it carries a `COMPLETED` status alongside it. The *status* read has no such ambiguity and is checked unconditionally, so the authoritative report of a failure is never the one that gets missed. Both directions are tested. - **`subscribe`'s `TimeoutError` catch is scoped to the iteration alone**, not to the callback. A progress callback that makes its own HTTP call can raise `TimeoutError` for reasons that have nothing to do with this wait, and cancelling a healthy queued request on the strength of it would throw a charge away. Tested. ## Verification of the pre-release path The release-candidate publish is a human step, but the tooling half is checkable and worth knowing before the tag is cut: `publish.yml` validates the release tag as **SemVer**, where a pre-release is a `-`-separated segment. Measured against that check — | tag | result | |---|---| | `v0.2.0-rc1` | accepted | | `v0.2.0-rc.1` | accepted | | `v0.2.0rc1` | **rejected** (this is PEP 440's own spelling) | and building with the accepted form produces `comfy_sdk-0.2.0rc1-py3-none-any.whl`, `twine check` PASSED — the version normalises to PEP 440 on the way through, so `pip install comfy-sdk` will not pick it up without `--pre`, which is the point of shipping one. The README's Releases section now says all of this. ## Provenance - **Authored by:** agent-work loop - **Verified:** `uv run --extra dev pytest -q`: 774 passed, 4 skipped, 0 failed (53 of them new, in `tests/test_models_queue.py`); `ruff check .` clean; `ruff format --check .`: 53 files already formatted; `mypy src`: no issues in 20 files; `scripts/check_public_repo_hygiene.py`: no internal-only references; `scripts/check_drift.py`: models in sync, all 15 router error types covered, run path matches the vendored spec; a local `python -m build` of the tree at version `0.2.0-rc1` → `comfy_sdk-0.2.0rc1-py3-none-any.whl`, `twine check` PASSED. - **Deviations:** the `submit_async` / `subscribe_async` aliases in the plan are deliberately not implemented (reason above); the release-candidate publish is not done here (it is a human step — see Residual). ## Residual Not fixed by this PR, and actionable on its own: 1. **The four route templates are unverified against a real deployment, and against the contract that declares them.** The upstream OpenAPI operations are authored but held, so they are absent from `spec/router-openapi.yaml` and there was nothing to compare against; the queued surface is also gated server-side, so there was no deployment I could exercise it against from here. Everything in this PR is proven against the recorded stub in `tests/conftest.py`, which is a stub I wrote to my own assumption of the shape — it cannot falsify that assumption. **When the operations land in the vendored spec, reconcile the `_MODEL_REQUEST*` constants in `src/comfy_low/transport.py` against it and extend `tests/test_router_spec_contract.py` to pin them the way it pins the run path** (`test_run_path_matches_vendored_spec` currently asserts the spec declares exactly one POST path, so it will need to admit the new ones). If the real routes differ, the four constants and the stub's four regexes are the whole change; the handle and the namespace methods above them are route-agnostic. 2. **The response field names are assumed, not read from a contract.** `request_id`, `status`, `queue_position`, `error_type`, `detail`, and `COMPLETED` as the one terminal status. Each is parsed defensively so a mismatch degrades rather than crashes — but a wrong field name degrades *silently* into "the queue never completes" (an unknown status is treated as not-yet-terminal) rather than into an error. Worth an explicit check against the contract in the same pass as item 1. 3. **The internal design appendix carrying the exact signatures and examples was not readable from here**, nor was the parent epic. The surface implemented here is the one the ticket text itself enumerates; if the appendix pins a different parameter name or ordering, that is a published-signature change and is cheaper to catch before the pre-release than after. 4. **A server-issued `request_id` containing `/` is refused** (`ComfyError`, at submit, rather than at the first poll). Under the route shape above a request id *is* a single path segment, so this cannot arise — but it is a guard written against my own assumption, and it belongs in the item-1 reconciliation. It follows the discipline the shipped `parse_model_id` already applies to the model id's segments. 5. **The release-candidate publish itself is not done, and is not this PR's to do** — the ticket names it as human follow-through. What is done: the tag spelling the publish workflow will and will not accept is measured above, and an RC wheel was built and `twine check`ed locally. **To ship it: merge this, then create a GitHub Release tagged `v0.2.0-rc1`** (not `v0.2.0rc1`). A plain release rather than a pre-release would not satisfy the ticket even though the code would be identical. 6. **Diff size.** ~2,000 lines, above the 400-line guideline for one unattended PR. It is one coherent feature in one module family — roughly 60% of it is docstrings and tests, matching this repo's house style — and splitting it would have produced PRs that could not be reviewed against each other. Flagging it rather than pretending it is small. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added synchronous and asynchronous queued model requests with submission, subscription, and request-handle workflows. - Added queue status updates, progress-event iteration, result retrieval, cancellation, and timeout handling. - Added server-paced polling, idempotency-key support, request-handle rehydration, and typed errors for failed completed requests. - Added feature gating for environments where queued requests are unavailable. - **Documentation** - Documented queued request workflows, asynchronous usage, polling, cancellation, errors, idempotency, and release versioning guidance. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
bsakaguchi 2026-10-31 22:01:42 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Comfy-Org/comfy-python-sdk#137
No description provided.