mirror of
https://github.com/Comfy-Org/comfy-typescript-sdk.git
synced 2026-11-04 23:47:27 +00:00
[PR #76] [MERGED] feat: run partner models with comfy.models.run, returning { data, requestId } (BE-8503) #78
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#78
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/76
Author: @mattmillerai
Created: 8/23/2026
Status: ✅ Merged
Merged: 8/24/2026
Merged by: @mattmillerai
Base:
main← Head:matt/be-8503-models-run📝 Commits (1)
3d73256feat: run partner models with comfy.models.run, returning { data, requestId }📊 Changes
12 files changed (+1225 additions, -172 deletions)
View changed files
📝
CHANGELOG.md(+18 -0)📝
README.md(+80 -13)📝
src/low/index.ts(+1 -0)📝
src/low/transport.ts(+14 -2)📝
src/sdk/credentials.test.ts(+83 -11)📝
src/sdk/credentials.ts(+97 -2)📝
src/sdk/exceptions.ts(+17 -14)📝
src/sdk/index.ts(+13 -2)➖
src/sdk/models.spec-contract.test.ts(+0 -66)📝
src/sdk/models.test.ts(+356 -27)📝
src/sdk/models.ts(+351 -35)➕
test/support/router-stub-server.ts(+195 -0)📄 Description
ELI-5
comfy.models.run("fal-ai/flux-pro", { prompt: "a cat" })used to throw "not implemented". Now it actually runs the model and hands you back{ data, requestId }—datais whatever the provider sends, untouched, andrequestIdis the id to quote if you ever have to ask us what happened to that call. Youawaitit once and you get the finished result; there is no job to poll.What changed
comfy.models.run(model, input, options?)calls the model router's canonical, model-ID-addressed route —POST /v1/models/{provider}/{model}— and resolves to a{ data, requestId }result.datais the provider's native JSON payload, forwarded unchanged in both directions: the request body is the model's own input document with no Comfy envelope around it, and the response is the model's own output. It is typedunknownby default and narrowable withrun<FluxOutput>(...). Deliberately notany— per-model schemas are published by the server per model (each model serves its own OpenAPI document) rather than carried in this package, andanywould silently switch off type-checking on every field a caller touches.requestIdis the response'sX-Comfy-Request-Id, and it is on the thrownComfyErrortoo (newComfyError.requestId) — an error response is exactly when a user needs an id to quote. It isnullonly when the response carried no such header; see the falsification section for why that is a real case and not a hypothetical.runresolves only when the generation is complete. Nothing in the client polls; for a provider whose own API is submit-then-poll, the server does that polling inside the call. A202(a task handle rather than a result) is refused rather than returned asdata, because handing back a handle typed as a finished result is a failure a caller would only discover in production.timeoutMs(ornullto disable), composed with a caller'ssignal. A caller's own abort is re-thrown untouched; only the SDK's own deadline is translated into arequest_timeoutComfyErrorthat names the knob to turn.Idempotency-Keyon every call, minted per call unless you passidempotencyKey. Supplying your own is what makes a retry of a call whose response you lost replay the original instead of running — and billing — the model twice.code(model_not_found,invalid_input,provider_timeout,content_policy_violation, ...), read fromX-Comfy-Error-Typefirst because the model-level validation body has noerror_typefield of its own. The four buckets that already have idiomatic classes get them (Unauthorized,Forbidden,InsufficientCredits,NotFound); per-field validation detail survives onerror.details.detail.comfy.config({ baseUrl })/COMFY_ROUTER_BASE_URLpoint the namespace at another deployment. This is a different surface from the class client'sCOMFY_BASE_URL:new Comfy()speaks the Comfy API v2 job/asset routes (which a self-hosted proxy or serverless deployment also serves), whilecomfy.modelsspeaks the hosted router's/v1/models/...routes. Pointing one at the other 404s, so they are two settings.The
{ data, requestId }wrapper is a deliberate asymmetry with the Python SDK, which returns the payload directly. It matches what a TypeScript integration being ported from a comparable hosted-inference client already expects. It is documented as intentional in the module doc, the README and the changelog, so a later cross-SDK parity check reads it as a decision rather than a gap.Why the previous placeholder's premise did not hold
The
ModelRunNotImplementedplaceholder this replaces was waiting on a model-execution operation to appear inspec/openapi.yaml. That was never going to happen:spec/openapi.yamlis the vendored Comfy API v2 contract (/api/v2/..., jobs and assets), and the router is a separate v1 surface on its own host, declared in the canonical Comfy API contract in the private backend repo asoperationId: runRouterModel. Two documents, not one — so waiting on the v2 spec meant waiting forever.That also means the wire types here are hand-written rather than generated, which
AGENTS.md§1 permits: its rule is "never hand-edit a generated file" and "do not patch the vendored spec", and neither applies to a surface that is not in the vendored spec at all.src/low/models.tsis the existing precedent for hand-maintained shapes codegen cannot reach. Nothing undersrc/low/generated/is touched and the drift check is green.src/sdk/models.spec-contract.test.ts, whose only job was to fail once a model route landed in the v2 spec, is deleted: its assertion is still true and no longer means anything.ModelRunNotImplementedis removed from the public exports. It was added in #67 and has never been published (latest on npm is0.1.7), so no consumer can depend on it.Negative-claim falsification
The headline change removes a capability denial rather than adding one, but the diff does add two refusal paths, so both were checked against the product rather than against my own premise:
POST /v1/models/{provider}/{model}withoperationId: runRouterModel, theX-Comfy-Request-Idheader on every response including errors, theX-Comfy-Error-Typebucket, and the record-and-replayIdempotency-Keysemantics. Every wire detail in this diff is read off that contract, not guessed.GET https://api.comfy.org/v1/models,/v1/models/fal-ai/flux-proand/v1/models/fal-ai/flux-pro/openapi.jsoneach answer404 {"message":"Not Found"}with noX-Comfy-*headers at all (probed 2026-08-23; read-only GETs only, no model was invoked and nothing was billed). The route is contract-declared and gated but not yet wired in production. So there is no end-to-end verification against a live server in this PR — acceptance is against the stub, which is what the story asks for. That live probe is also the direct evidence forrequestId: string | null: an edge response generated before the request reaches the router genuinely carries no request-id header, so a non-nullable type would have been a lie.provider/model/variant) is rejected locally with a message that says why. The contract states in as many words that how a variant is addressed over HTTP is not settled and that the catalog must not advertise an ID the invocation route is not defined to accept — so this refuses only what cannot address the route at all. Beyond segment count there is deliberately no client-side alphabet validation: the server resolves IDs against the catalog and answers a miss withmodel_not_foundplus close-match suggestions, and a narrower local copy of that check would replace a helpful round trip with a local rejection and go stale the moment the alphabet widens.Testing
src/sdk/models.test.tsruns against a newtest/support/router-stub-server.ts(separate from the v2stub-server.ts— different surface, different envelope): success and payload passthrough, the exact path and method, bearer auth and content negotiation, the env-var credential fallback, request-id extraction and its absence,Idempotency-Keypresence/uniqueness/override, the 10-minute default, a slow response that trips a short deadline, a stalled body that trips it mid-stream, a disabled deadline, a caller abort staying a caller abort, and the failure map (404 / 401 / 403 / 402 / unmapped bucket / header-only bucket on a validation body / non-router HTML body /202/ non-JSON200).credentials.test.tsgainsbaseUrlprecedence, clearing, trailing-slash stripping and malformed-value rejection, and its redaction test now runs a real failed call.The stalled-body test was confirmed red against the pre-fix code and green after — the first draft of this change translated the deadline around
fetchonly, so a deadline firing during the body read leaked a bareDOMException. Same self-review pass caughtbuildUserAgent()reading a bareprocess, which would be aReferenceErrorfor a browser caller ofcomfy.models(a browser drops a caller-setUser-Agentanyway); it now degrades instead of throwing, with the Node string unchanged.Judgment calls
codeso that later work is additive.baseUrloncomfy.configrather than a per-call argument: it mirrors howcredentialsalready works on this namespace, and it is what lets the tests point at a stub. Likecredentials, it is process-global — the wrong tool for a multi-tenant server, and documented as such.Unexercised artifacts
X-Comfy-Request-Idcasing and the exact error-body shapes.Provenance
pnpm test(vitest): 18 files, 253 tests passed, 0 failed;pnpm typecheckclean;pnpm lint(oxlint) 0 warnings;pnpm format:checkclean;node scripts/check-spec-drift.mjsin sync;node scripts/check-public-repo-hygiene.mjsclean;pnpm build+npm pack --dry-runOK; live read-only probe of three router GET routes (all 404, recorded above)🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.