mirror of
https://github.com/Comfy-Org/comfy-typescript-sdk.git
synced 2026-11-04 23:47:27 +00:00
[PR #133] [MERGED] feat(models): queue a model request with submit, subscribe and handle #135
Labels
No labels
audit:ai-readiness
audit:community
audit:github
audit:health-scoring
audit:policy
audit:release
audit:supply-chain
bug
cursor-review-pin
effort:medium
effort:small
effort:trivial
priority:nice-to-have
priority:recommended
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Comfy-Org/comfy-typescript-sdk#135
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
📋 Pull Request Information
Original PR: https://github.com/Comfy-Org/comfy-typescript-sdk/pull/133
Author: @mattmillerai
Created: 9/10/2026
Status: ✅ Merged
Merged: 9/14/2026
Merged by: @mattmillerai
Base:
main← Head:matt/be-13232-models-queue-submit-subscribe-handle📝 Commits (5)
7d087e7feat(models): queue a model request with submit, subscribe and handle50c7026Merge remote-tracking branch 'origin/main' into matt/be-13232-models-queue-submit-subscribe-handleeb41e54Merge branch 'main' into matt/be-13232-models-queue-submit-subscribe-handleebf1d2bfix: address the CodeRabbit review on the queued model surface7e37ed8docs: exclude onQueueUpdate callback time from subscribe's timeout contract📊 Changes
15 files changed (+5827 additions, -98 deletions)
View changed files
📝
AGENTS.md(+29 -9)📝
CHANGELOG.md(+26 -0)📝
README.md(+68 -1)➕
package-lock.json(+2993 -0)📝
src/sdk/credentials.ts(+24 -0)📝
src/sdk/index.ts(+14 -0)➕
src/sdk/modelRequests.test.ts(+935 -0)➕
src/sdk/modelRequests.ts(+1067 -0)➕
src/sdk/modelRoutes.ts(+173 -0)📝
src/sdk/models.ts(+131 -84)📝
src/sdk/router-spec-contract.test.ts(+76 -1)📝
src/sdk/routerErrors.test.ts(+92 -0)📝
src/sdk/routerErrors.ts(+64 -1)📝
src/sdk/surface-parity.test.ts(+68 -2)📝
test/support/router-stub-server.ts(+67 -0)📄 Description
ELI-5
comfy.models.runwaits on the phone until your picture is finished. That is fine until you cannot stay on the phone — a web request that has to answer now, or a worker that starts the job in one place and picks it up somewhere else. This adds the other way of doing it:comfy.models.submithands you a ticket the moment the server takes the job, and you come back for the picture later with that ticket.comfy.models.subscribedoes both halves for you and tells you where you are in the queue while you wait, andcomfy.models.handlerebuilds the ticket from just its number, so another process can collect a job it never started.What this adds
comfy.models.submit(model, input, options?)— POSTs the model's own native body to arequestscollection under the same/v2/models/{provider}/{model}prefixrunuses, and resolves to aRequestHandleas soon as the server accepts. The handle carriesrequestId,model,status(),get(options?),cancel()and an async-iterableevents(options?)— the TypeScript spelling of the Python SDK'siter_events.get()resolves to the same{ data, requestId }runreturns.comfy.models.subscribe(model, input, { onQueueUpdate?, timeoutMs?, signal?, idempotencyKey?, retry? })— submit + poll + collect in one call.comfy.models.handle(model, requestId)— rebuilds a handle from the two ids with no request made; both are validated locally.routerErrors.errorFromCompletion(body, requestId)— the typed exception a completed queued request reports, ornull.The server owns the queue. Ordering, admission, retries, timeouts, billing and expiry are all decided server-side; this adds polling and ergonomics and nothing else.
Behaviour worth reviewing
200is not a success. The server reports a failed and a cancelled request asCOMPLETEDcarrying anerror_type. Every path that reads a completion runs it througherrorFromCompletion, which maps the bucket through the same tabletoRouterErroruses and leaveshttpStatusnull (the poll that found it was a200). It runs unconditionally on the authoritative status read, and on the result body only inside the queue's own envelope — a partner model whose native output happens to have anerror_typefield must not have a successful generation failed for it.timeoutMs/signalbound the poll requests themselves, their retries, the pauses between them and the result fetch — not only the pauses. The first poll is always made, sotimeoutMs: 0reads "look once". Each individual request is floored at 1 s (MIN_REQUEST_TIMEOUT_MS) so the last poll before a deadline can still complete a handshake rather than being a certain transport failure; the loop's own bound is unfloored, so a spent budget still stops the next request and the wait overruns by at most one. This mirrors the Python SDK's_MIN_HTTP_TIMEOUT.Retry-Afterbeating the schedule and capped at 60 s before it is slept on.QueueUpdate.retryAfterMsreports it uncapped — the cap applies to what is slept on, not to what the server is reported as having said.subscribe's own timeout and an abort viasignaleach issue exactly one best-effort cancel: a single attempt (retry: false), a 10 s bound, and not the caller's already-aborted signal. Its failure is swallowed so it cannot mask the timeout or abort — asserted, including the request count. A failure in the caller's ownonQueueUpdatedeliberately does not cancel: a bug in a progress renderer is not a reason to throw away a paid-for generation.submitcall mints one fresh key; every retry inside that call reuses it, so a retry replays the original acceptance rather than queueing a second generation.idempotencyKeyoverrides. Cancel is sent asPUT.Judgment calls
routerErrors.*, not theComfyErrorfamilyrunmaps into. That is an asymmetry inside this SDK, and it is deliberate: a queue failure arrives as anerror_typein a200body where there is no status to classify, so the class keyed by the bucket is the only honest answer — and it is what the Python SDK does.run's own mapping is untouched. Flagging it because a reviewer will reasonably ask.spec/router-openapi.yaml. The operations are authored upstream but held, and the one-way sync strips a held operation, so there is nothing to compare them against. They are pinned by their relation toRUN_ROUTE_TEMPLATE(each is the run route plus a fixed suffix), which is how the Python SDK binds the same four — plus a rot guard that fails the day the vendored contract does declare the collection, naming the replacement to make.parseModelId/fillRoutemoved to a new leafsrc/sdk/modelRoutes.tsalongside the newparseRequestId.models.tsimports the queue methods to assemble the frozen namespace; leaving the helpers where they were would have made the queue module import it back, and that runtime cycle is safe only until someone adds aconstread at module-evaluation time. The built output confirms the dependency runs one way (dist/sdk/modelRequests.jshas nomodels.jsimport —RunResultis a type-only import and is erased).RUN_ROUTE_TEMPLATEstays inmodels.tsbecausescripts/router-route-contract.mjsreads it out of that file's source text.credentials.tsasrequireCredentials()so both surfaces cannot drift into two messages for the same misconfiguration.run's behaviour is byte-identical: same class, same message, same code.parseRequestId, the non-object-body guards), andNotEnabledis the server's gate surfaced through the existing typed hierarchy, not a client-side refusal. Nonot supported/unavailable/STOPstring is introduced.Residual
Not fixed here, and worth its own follow-up:
Comfy-Org/comfy-python-sdk#137, verified open withmergedAt: nullwhile writing this), sopnpm sync:python-surfaceagainst that repo's default branch still yieldsmodelsmethods of["run"]alone — andpnpm check:sdk-parityconfirms the committed snapshot already matches it. Addingsubmit/subscribe/handletherefore makes this SDK lead. Rather than allowlist that as an intentional asymmetry (which it is not — the two surfaces are the same shape, method for method), it is recorded with a newmodelsMethodsAheadOfPythonfield insurface-parity.test.ts: the same one-direction LEAD mechanismrouterErrorClassesAheadOfPythonalready provides, with a rot guard that fails the moment the refreshed snapshot grows any of the three names. The follow-up work is mechanical and must happen once the Python change lands on its default branch: runpnpm sync:python-surface, commit the refreshedparity/python-surface.json, and delete themodels-queue-leadentry the rot guard then names. Until that is done, the three method names are not actually being diffed against Python.Comfy-Org/docs#1648's TypeScript tab is not updated by this PR. The docs preview page (development/comfy-router/queue.mdx) still shows rawfetchcalls against the four routes with a comment saying the SDK methods land next. That is a change in a different repository and is not in this diff; it becomes actionable once this is published under a pre-release tag. I read the PR's title only — its content is in a repo this change does not touch, so I did not exercise it.test/support/router-stub-server.ts(a real Nodehttpstub, extended with arespondhook so one call sequence can script submit → status → result → cancel), which is what the repo's test style requires. The four queue routes are served only on the queue POC stack today, which this environment cannot reach, so the wire contract is asserted against a recording of it and not against the server itself. In particular the403 not_enabledgate, the400 ALREADY_COMPLETEDcancel answer and the result route's202are all stubbed rather than observed.400vs409; what the result read gives a cancelled or timed-out request). Neither changes this client surface, but both mean the cancel and result paths here are written against the shape as it stands today:cancel()surfaces whatever the server answers through the typed hierarchy rather than special-casing either status, and the result route's202is reported as an invalid response rather than silently retried.Provenance
pnpm test: 25 files, 495 passed, 0 failed (50 new insrc/sdk/modelRequests.test.ts, 7 new insrc/sdk/routerErrors.test.ts, 3 new insrc/sdk/router-spec-contract.test.ts, 1 new insrc/sdk/surface-parity.test.ts);pnpm typecheck: clean;pnpm lint: clean;pnpm format:check: clean;pnpm check:spec-drift: both checks OK;pnpm check:sdk-parity: snapshot matches the Python default branch;node scripts/check-public-repo-hygiene.mjs: no internal-only references;pnpm build && pnpm pack: tarball carriespackage/dist/index.jsandpackage/dist/index.d.ts.Summary by CodeRabbit
New Features
submit,subscribe, and request handles for polling, status updates, cancellation, timeouts, and result collection.Documentation
🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.