[PR #164] [MERGED] feat(models): surface Idempotent-Replayed as RunResult.replayed #165

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

📋 Pull Request Information

Original PR: https://github.com/Comfy-Org/comfy-typescript-sdk/pull/164
Author: @mattmillerai
Created: 9/19/2026
Status: ✅ Merged
Merged: 10/9/2026
Merged by: @mattmillerai

Base: main ← Head: matt/be-15940-idempotent-replayed


📝 Commits (3)

  • 79a07df feat(models): surface Idempotent-Replayed as RunResult.replayed
  • 0b22b6c Merge origin/main into matt/be-15940-idempotent-replayed
  • c8a0e84 Merge remote-tracking branch 'origin/main' into matt/be-15940-idempotent-replayed

📊 Changes

9 files changed (+287 additions, -7 deletions)

View changed files

📝 CHANGELOG.md (+13 -0)
📝 scripts/router-route-contract.mjs (+48 -0)
📝 src/sdk/modelRequests.test.ts (+26 -0)
📝 src/sdk/modelRequests.ts (+12 -1)
📝 src/sdk/models.test.ts (+88 -0)
📝 src/sdk/models.ts (+61 -3)
📝 src/sdk/router-spec-contract.test.ts (+30 -0)
📝 src/sdk/surface-parity.test.ts (+4 -1)
📝 test/support/router-stub-server.ts (+5 -2)

📄 Description

ELI-5

When you retry a model call under the same Idempotency-Key, Router does not run the model again — it hands you back the answer it already has, and does not charge you a second time. The answer looks identical to a fresh one: same body, same status, same headers. The only thing that says "this is a replay" is a response header, Idempotent-Replayed, and until now this SDK read it for nobody.

So a caller summing up what its runs cost had no way to tell a replayed answer from a fresh charge, and would double-count it. Now every comfy.models.run result carries replayed?: boolean (always set by the SDK; optional only for source compatibility, like creditsUsed). true means "you already paid for this one — skip it".

What changed

  • RunJsonResult.replayed / RunBinaryResult.replayed (src/sdk/models.ts), read in finish() next to servingProvider/droppedParams and set on all three return sites (binary, no-Content-Type binary, json). Derived from the header's presence via a new parseReplayed, not from its value — Router omits the header on a fresh run rather than sending false, and the vendored contract says so in as many words ("branch on its presence"). Same derivation the Python SDK's RouterRunResult.replayed uses (headers.get(...) is not None).
  • RequestHandle.get() (src/sdk/modelRequests.ts) reads the same header into the same field. It is false on every real response — see the verification below — and the get() TSDoc now says to deduplicate re-collection by RequestHandle.requestId rather than by replayed. Kept as a header read rather than a hard-coded false for the same "one union, both paths" reason the two alt-provider headers are read there.
  • A spec pin for IDEMPOTENT_REPLAYED_HEADER (src/sdk/router-spec-contract.test.ts, backed by a new runSuccessHeaders reader in scripts/router-route-contract.mjs): the run route's 200 must declare a header spelled exactly that, resolving to RouterIdempotentReplayedHeader, with required: false. All three matter, because this constant's drift is silent in both directions — a name wrong by one character reports replayed: false on every replay forever (indistinguishable from the fresh charge the field exists to tell a replay apart from), and a contract that made the header required would mean its arrival no longer marks anything and parseReplayed would have to read the value instead. The failure messages say which.
  • Parity allowlist (src/sdk/surface-parity.test.ts): a new pythonMethodsSubsumedByTypescript field on Asymmetry, with ["run_detailed", "runDetailed"] under the existing result-envelope entry (its why extended rather than a new entry added). Those names are filtered off the Python side of the models name comparison only, and a new rot guard fails the day this SDK grows a runDetailed — the mirror image of the existing lead guards, which fire when Python catches up.
  • Tests for both arms, both paths, the collect loop, the presence-not-value rule, and the double-collection case; a CHANGELOG.md entry under Unreleased / Added.

Verification

pnpm lint, pnpm format:check, pnpm typecheck, pnpm build, node scripts/check-spec-drift.mjs and node scripts/check-public-repo-hygiene.mjs all clean; pnpm test is 649 passed / 5 skipped, 27 files.

Break-and-restore, all three restored green:

  • Removed replayed from the binary return site in finish() → tsc fails TS2322 naming the missing replayed. (Run at 79a07df, when the field was required on the interface. After the merge with main the same guarantee comes from BuiltRunResult, which finish() and RequestHandle.get() return and which requires replayed.)
  • Set idempotentReplayed = true on the fresh-run test → AssertionError: expected true to be false.
  • Added a fake runDetailed to the method list the parity guard reads → it fails with its own message, comfy.models.runDetailed` now exists — delete the subsumed entry for `run_detailed.

Against the artifacts, not just the diff. Two claims in this change are about things outside it, so both were checked rather than assumed:

  • "The queued result route never carries the header." Read out of spec/router-openapi.yaml: getRouterModelRequestResult's 200 declares X-Comfy-Request-Id and X-Content-Type-Options and nothing else. So RequestHandle.get()'s replayed genuinely reads false, and the TSDoc's "deduplicate by requestId" is the real advice rather than a hedge.
  • "Python's run_detailed adds nothing run does not already return." This is the one capability-denial-shaped claim in the diff, so it was falsified empirically rather than unit-tested: every public comfy.models method was enumerated live (handle, list, run, schema, submit, subscribe — no runDetailed), and a real stubbed run's field set was compared against RouterRunResult's five attributes read off the Python SDK's default branch. output → data, serving_provider → servingProvider, dropped_params → droppedParams, replayed → replayed, request_id → requestId — all five present. The capability is reachable, via run; nothing is being dead-ended. Note that before this change the claim would have been false: replayed was the one field run did not carry.

The half this does not fix, measured. The vendored contract declares Idempotent-Replayed on 6 operation/status pairs across three operations. This change surfaces it on 1 of the 6 — runRouterModel 200, both content arms. The other five are all on paths deliberately out of scope; they are named individually under Residual.

pnpm check:sdk-parity is red at the current head, and that failure is pre-existing and not caused by this branch. The earlier RouterRunResult ExtractionError was fixed on main by #167. What fails now is newer upstream drift: the Python SDK's default branch gained Models.list / Models.schema, so parity/python-surface.json is stale. main's own CI fails the same job (run 37967760042, at 8096086). The fix is #201, which refreshes the snapshot and retires the discovery-methods-land-first-in-typescript lead. This branch touches neither the snapshot nor that entry, so it should go green once #201 lands and is merged in.

Judgment calls

  • Merged with main after #157 (creditsUsed) landed. That PR added a field to the same two interfaces and the same result literals. The merge keeps both fields side by side at every return site, in the router-route-contract.mjs reader and in the spec-contract pins.
  • replayed is now OPTIONAL on the published interfaces. This reverses the original choice to make it required. #157 set the precedent: 0.4.0 shipped RunJsonResult/RunBinaryResult without either field, so a required field would break a consumer's own hand-built result literal. replayed?: boolean follows that precedent. #157's internal BuiltRunResult now requires both replayed and creditsUsed, so tsc still names any SDK return site that leaves one out. A hand-built literal that omits it reads as undefined, which is falsy, so it reads as "not a replay".
  • Nothing was added to src/sdk/index.ts, on purpose. The plan asked for IDEMPOTENT_REPLAYED_HEADER and parseReplayed to be re-exported from the package root "mirroring how DROPPED_PARAMS_HEADER/parseDroppedParams are exported". They are not: git grep DROPPED_PARAMS_HEADER src/sdk/index.ts returns nothing, and neither is FALLBACK_PROVIDER_HEADER, nor (in #157) CREDITS_USED_HEADER. Every Router disclosure header constant and parser is deliberately module-internal — exported from models.ts for the tests and the sibling module, and kept off the published surface. Adding one and not its two siblings would be an odd asymmetry, and a package-root export is a permanent public-API commitment that is far cheaper to add later than to remove. So the precedent was followed over the instruction. Say the word and it is a three-line addition.
  • The new runSuccessHeaders reader returns {component, required} per header name. #157's runSuccessHeaderNames reader, now on main, sits beside it and returns lowercased names only. That is enough for the credits pin but not for this one. Both readers are kept, and they do not conflict. Collapsing them into one would be a tidy follow-up.

Residual

Not fixed by this change, and each one actionable on its own:

  • The thrown error path carries no replayed. finish() computes it before the !response.ok throw, but errorFromResponse does not receive it, so a replayed 400 content_policy_violation — which Router records and replays like any other answer it actually produced, and which some models meter — reaches the caller as an ordinary error with nothing saying it was not charged twice. The contract declares the header on runRouterModel's 400 and 422. This belongs with the error-path credits work rather than here, because both want the same plumbing through ComfyError.
  • submit() does not surface it. submitRouterModelRequest's 201 declares Idempotent-Replayed too: a replayed submit returns the ORIGINAL handle rather than queueing a second generation. RequestHandle is not a RunResult, so a replayed on the handle is a separate small piece of design (where does it live — a field, or a property of the submit call?) rather than a line of this diff.
  • The 422 declarations are untouched on all three operations (runRouterModel, submitRouterModelRequest, getRouterModelRequestResult), via the shared RouterModelValidationError response. Same category as the error path above. These plus the two items above are the 5 of 6 declared sites this change does not cover.
  • Nothing pins the queued result route's absence of the header. RequestHandle.get()'s TSDoc states replayed is always false there, and that was verified against the vendored contract by hand (above) — but no test fails the day a sync adds Idempotent-Replayed to getRouterModelRequestResult's 200. The code would start reporting it correctly; only the documentation and the "deduplicate by requestId" advice would go stale. An inverse pin in router-spec-contract.test.ts, shaped like the rot guards already in that file, would close it. Left out to keep this diff to what was asked for.
  • The Python SDK's header-name fix has landed, so the two SDKs now agree at runtime. (Corrected on the 2026-09-21 review pass — this bullet previously recorded the opposite.) On Comfy-Org/comfy-python-sdk's default branch, RouterRunResult.replayed now reads headers.get("Idempotent-Replayed") is not None (its models.py), the same bare spelling Router sends and the vendored contract declares; the open PR referenced here has since merged. So this change mirrors Python's shipped behaviour rather than an anticipated one, and a live comparison of the two SDKs should now match rather than diverge.
  • Not exported from the package root — see the third judgment call above. If the intent really was a public re-export, this is the one piece of the plan not delivered.
  • pnpm check:sdk-parity is still red, on base drift rather than this change. The RouterRunResult extractor failure this bullet used to describe was fixed by #167. The red job now is the stale list / schema snapshot described under Verification, fixed by #201. The parity allowlist entry added here is written so it stays correct both before and after parity/python-surface.json is refreshed. The new guard asserts on the TypeScript side only and deliberately does not assert that the Python snapshot still contains run_detailed.

Provenance

  • Authored by: agent-work loop
  • Verified:
    • At the current head (main merged in at 8096086): pnpm lint, pnpm format:check, pnpm typecheck, pnpm build and pnpm check:spec-drift (both checks) are clean.
    • pnpm test at the current head: 697 passed, 5 skipped (28 test files passed, 1 skipped).
    • CI at the current head: Test (node 22/24), build-check and both public-repo-hygiene jobs pass.
    • sdk-parity fails on stale-snapshot drift that main fails identically. The fix is #201 (see Verification).
    • Three break-and-restore checks were run at 79a07df and restored green (see Verification).
  • Deviations:
    • No package-root re-export was added (see the judgment call above).
    • replayed is optional rather than required, to match #157's source-compatibility precedent (see the judgment call above).

🔄 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-typescript-sdk/pull/164 **Author:** [@mattmillerai](https://github.com/mattmillerai) **Created:** 9/19/2026 **Status:** ✅ Merged **Merged:** 10/9/2026 **Merged by:** [@mattmillerai](https://github.com/mattmillerai) **Base:** `main` ← **Head:** `matt/be-15940-idempotent-replayed` --- ### 📝 Commits (3) - [`79a07df`](https://github.com/Comfy-Org/comfy-typescript-sdk/commit/79a07df2d154807db5835bc9edecd71001a3e828) feat(models): surface Idempotent-Replayed as RunResult.replayed - [`0b22b6c`](https://github.com/Comfy-Org/comfy-typescript-sdk/commit/0b22b6cf874a9228560421fc8c6febc18ac5035c) Merge origin/main into matt/be-15940-idempotent-replayed - [`c8a0e84`](https://github.com/Comfy-Org/comfy-typescript-sdk/commit/c8a0e8419afda66d247b8ccf2f06eac26773cd11) Merge remote-tracking branch 'origin/main' into matt/be-15940-idempotent-replayed ### 📊 Changes **9 files changed** (+287 additions, -7 deletions) <details> <summary>View changed files</summary> 📝 `CHANGELOG.md` (+13 -0) 📝 `scripts/router-route-contract.mjs` (+48 -0) 📝 `src/sdk/modelRequests.test.ts` (+26 -0) 📝 `src/sdk/modelRequests.ts` (+12 -1) 📝 `src/sdk/models.test.ts` (+88 -0) 📝 `src/sdk/models.ts` (+61 -3) 📝 `src/sdk/router-spec-contract.test.ts` (+30 -0) 📝 `src/sdk/surface-parity.test.ts` (+4 -1) 📝 `test/support/router-stub-server.ts` (+5 -2) </details> ### 📄 Description ## ELI-5 When you retry a model call under the same `Idempotency-Key`, Router does not run the model again — it hands you back the answer it already has, and does not charge you a second time. The answer looks *identical* to a fresh one: same body, same status, same headers. The only thing that says "this is a replay" is a response header, `Idempotent-Replayed`, and until now this SDK read it for nobody. So a caller summing up what its runs cost had no way to tell a replayed answer from a fresh charge, and would double-count it. Now every `comfy.models.run` result carries `replayed?: boolean` (always set by the SDK; optional only for source compatibility, like `creditsUsed`). `true` means "you already paid for this one — skip it". ## What changed - **`RunJsonResult.replayed` / `RunBinaryResult.replayed`** (`src/sdk/models.ts`), read in `finish()` next to `servingProvider`/`droppedParams` and set on all three return sites (binary, no-`Content-Type` binary, json). Derived from the header's **presence** via a new `parseReplayed`, not from its value — Router omits the header on a fresh run rather than sending `false`, and the vendored contract says so in as many words ("branch on its presence"). Same derivation the Python SDK's `RouterRunResult.replayed` uses (`headers.get(...) is not None`). - **`RequestHandle.get()`** (`src/sdk/modelRequests.ts`) reads the same header into the same field. It is `false` on every real response — see the verification below — and the `get()` TSDoc now says to deduplicate re-collection by `RequestHandle.requestId` rather than by `replayed`. Kept as a header read rather than a hard-coded `false` for the same "one union, both paths" reason the two alt-provider headers are read there. - **A spec pin** for `IDEMPOTENT_REPLAYED_HEADER` (`src/sdk/router-spec-contract.test.ts`, backed by a new `runSuccessHeaders` reader in `scripts/router-route-contract.mjs`): the run route's `200` must declare a header spelled exactly that, resolving to `RouterIdempotentReplayedHeader`, with `required: false`. All three matter, because this constant's drift is **silent in both directions** — a name wrong by one character reports `replayed: false` on every replay forever (indistinguishable from the fresh charge the field exists to tell a replay apart from), and a contract that made the header `required` would mean its arrival no longer marks anything and `parseReplayed` would have to read the value instead. The failure messages say which. - **Parity allowlist** (`src/sdk/surface-parity.test.ts`): a new `pythonMethodsSubsumedByTypescript` field on `Asymmetry`, with `["run_detailed", "runDetailed"]` under the existing `result-envelope` entry (its `why` extended rather than a new entry added). Those names are filtered off the **Python** side of the `models` name comparison only, and a new rot guard fails the day this SDK grows a `runDetailed` — the mirror image of the existing lead guards, which fire when *Python* catches up. - Tests for both arms, both paths, the collect loop, the presence-not-value rule, and the double-collection case; a `CHANGELOG.md` entry under Unreleased / Added. ## Verification `pnpm lint`, `pnpm format:check`, `pnpm typecheck`, `pnpm build`, `node scripts/check-spec-drift.mjs` and `node scripts/check-public-repo-hygiene.mjs` all clean; `pnpm test` is 649 passed / 5 skipped, 27 files. **Break-and-restore, all three restored green:** - Removed `replayed` from the binary return site in `finish()` → `tsc` fails `TS2322` naming the missing `replayed`. *(Run at `79a07df`, when the field was required on the interface. After the merge with `main` the same guarantee comes from `BuiltRunResult`, which `finish()` and `RequestHandle.get()` return and which requires `replayed`.)* - Set `idempotentReplayed = true` on the fresh-run test → `AssertionError: expected true to be false`. - Added a fake `runDetailed` to the method list the parity guard reads → it fails with its own message, ``comfy.models.runDetailed` now exists — delete the subsumed entry for `run_detailed``. **Against the artifacts, not just the diff.** Two claims in this change are about things outside it, so both were checked rather than assumed: - *"The queued result route never carries the header."* Read out of `spec/router-openapi.yaml`: `getRouterModelRequestResult`'s `200` declares `X-Comfy-Request-Id` and `X-Content-Type-Options` and nothing else. So `RequestHandle.get()`'s `replayed` genuinely reads `false`, and the TSDoc's "deduplicate by `requestId`" is the real advice rather than a hedge. - *"Python's `run_detailed` adds nothing `run` does not already return."* This is the one capability-denial-shaped claim in the diff, so it was falsified empirically rather than unit-tested: every public `comfy.models` method was enumerated live (`handle, list, run, schema, submit, subscribe` — no `runDetailed`), and a real stubbed run's field set was compared against `RouterRunResult`'s five attributes read off the Python SDK's default branch. `output → data`, `serving_provider → servingProvider`, `dropped_params → droppedParams`, `replayed → replayed`, `request_id → requestId` — all five **present**. The capability is reachable, via `run`; nothing is being dead-ended. Note that before this change the claim would have been false: `replayed` was the one field `run` did not carry. **The half this does not fix, measured.** The vendored contract declares `Idempotent-Replayed` on **6 operation/status pairs** across three operations. This change surfaces it on **1 of the 6** — `runRouterModel` `200`, both content arms. The other five are all on paths deliberately out of scope; they are named individually under Residual. `pnpm check:sdk-parity` is red at the current head, and **that failure is pre-existing and not caused by this branch**. The earlier `RouterRunResult` `ExtractionError` was fixed on `main` by #167. What fails now is newer upstream drift: the Python SDK's default branch gained `Models.list` / `Models.schema`, so `parity/python-surface.json` is stale. `main`'s own CI fails the same job (run 37967760042, at `8096086`). The fix is #201, which refreshes the snapshot and retires the `discovery-methods-land-first-in-typescript` lead. This branch touches neither the snapshot nor that entry, so it should go green once #201 lands and is merged in. ## Judgment calls - **Merged with `main` after #157 (`creditsUsed`) landed.** That PR added a field to the same two interfaces and the same result literals. The merge keeps both fields side by side at every return site, in the `router-route-contract.mjs` reader and in the spec-contract pins. - **`replayed` is now OPTIONAL on the published interfaces.** This reverses the original choice to make it required. #157 set the precedent: `0.4.0` shipped `RunJsonResult`/`RunBinaryResult` without either field, so a required field would break a consumer's own hand-built result literal. `replayed?: boolean` follows that precedent. #157's internal `BuiltRunResult` now requires both `replayed` and `creditsUsed`, so `tsc` still names any SDK return site that leaves one out. A hand-built literal that omits it reads as `undefined`, which is falsy, so it reads as "not a replay". - **Nothing was added to `src/sdk/index.ts`, on purpose.** The plan asked for `IDEMPOTENT_REPLAYED_HEADER` and `parseReplayed` to be re-exported from the package root "mirroring how `DROPPED_PARAMS_HEADER`/`parseDroppedParams` are exported". They are not: `git grep DROPPED_PARAMS_HEADER src/sdk/index.ts` returns nothing, and neither is `FALLBACK_PROVIDER_HEADER`, nor (in #157) `CREDITS_USED_HEADER`. Every Router *disclosure* header constant and parser is deliberately module-internal — exported from `models.ts` for the tests and the sibling module, and kept off the published surface. Adding one and not its two siblings would be an odd asymmetry, and a package-root export is a permanent public-API commitment that is far cheaper to add later than to remove. So the precedent was followed over the instruction. Say the word and it is a three-line addition. - The new `runSuccessHeaders` reader returns `{component, required}` per header name. #157's `runSuccessHeaderNames` reader, now on `main`, sits beside it and returns lowercased names only. That is enough for the credits pin but not for this one. Both readers are kept, and they do not conflict. Collapsing them into one would be a tidy follow-up. ## Residual Not fixed by this change, and each one actionable on its own: - **The thrown error path carries no `replayed`.** `finish()` computes it before the `!response.ok` throw, but `errorFromResponse` does not receive it, so a replayed `400 content_policy_violation` — which Router records and replays like any other answer it actually produced, and which some models meter — reaches the caller as an ordinary error with nothing saying it was not charged twice. The contract declares the header on `runRouterModel`'s `400` and `422`. This belongs with the error-path credits work rather than here, because both want the same plumbing through `ComfyError`. - **`submit()` does not surface it.** `submitRouterModelRequest`'s `201` declares `Idempotent-Replayed` too: a replayed submit returns the ORIGINAL handle rather than queueing a second generation. `RequestHandle` is not a `RunResult`, so a `replayed` on the handle is a separate small piece of design (where does it live — a field, or a property of the submit call?) rather than a line of this diff. - **The `422` declarations are untouched on all three operations** (`runRouterModel`, `submitRouterModelRequest`, `getRouterModelRequestResult`), via the shared `RouterModelValidationError` response. Same category as the error path above. These plus the two items above are the 5 of 6 declared sites this change does not cover. - **Nothing pins the queued result route's *absence* of the header.** `RequestHandle.get()`'s TSDoc states `replayed` is always `false` there, and that was verified against the vendored contract by hand (above) — but no test fails the day a sync adds `Idempotent-Replayed` to `getRouterModelRequestResult`'s `200`. The code would start reporting it correctly; only the documentation and the "deduplicate by `requestId`" advice would go stale. An inverse pin in `router-spec-contract.test.ts`, shaped like the rot guards already in that file, would close it. Left out to keep this diff to what was asked for. - **The Python SDK's header-name fix has landed, so the two SDKs now agree at runtime.** *(Corrected on the 2026-09-21 review pass — this bullet previously recorded the opposite.)* On `Comfy-Org/comfy-python-sdk`'s default branch, `RouterRunResult.replayed` now reads `headers.get("Idempotent-Replayed") is not None` (its `models.py`), the same bare spelling Router sends and the vendored contract declares; the open PR referenced here has since merged. So this change mirrors Python's *shipped* behaviour rather than an anticipated one, and a live comparison of the two SDKs should now match rather than diverge. - **Not exported from the package root** — see the third judgment call above. If the intent really was a public re-export, this is the one piece of the plan not delivered. - **`pnpm check:sdk-parity` is still red, on base drift rather than this change.** The `RouterRunResult` extractor failure this bullet used to describe was fixed by #167. The red job now is the stale `list` / `schema` snapshot described under Verification, fixed by #201. The parity allowlist entry added here is written so it stays correct both before and after `parity/python-surface.json` is refreshed. The new guard asserts on the TypeScript side only and deliberately does not assert that the Python snapshot still contains `run_detailed`. ## Provenance - **Authored by:** agent-work loop - **Verified:** - At the current head (`main` merged in at `8096086`): `pnpm lint`, `pnpm format:check`, `pnpm typecheck`, `pnpm build` and `pnpm check:spec-drift` (both checks) are clean. - `pnpm test` at the current head: 697 passed, 5 skipped (28 test files passed, 1 skipped). - CI at the current head: Test (node 22/24), build-check and both public-repo-hygiene jobs pass. - `sdk-parity` fails on stale-snapshot drift that `main` fails identically. The fix is #201 (see Verification). - Three break-and-restore checks were run at `79a07df` and restored green (see Verification). - **Deviations:** - No package-root re-export was added (see the judgment call above). - `replayed` is optional rather than required, to match #157's source-compatibility precedent (see the judgment call above). --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
bsakaguchi 2026-10-31 22:05:13 +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-typescript-sdk#165
No description provided.