mirror of
https://github.com/Comfy-Org/comfy-typescript-sdk.git
synced 2026-11-04 23:47:27 +00:00
[PR #191] [MERGED] feat(models): carry refusalSubject on ComfyError so models.run names the refused input/output #193
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#193
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/191
Author: @mattmillerai
Created: 9/27/2026
Status: ✅ Merged
Merged: 10/9/2026
Merged by: @mattmillerai
Base:
be-17174-refusal-subject← Head:be-17424-run-refusal-subject📝 Commits (4)
47c8022feat(models): carry refusalSubject on ComfyError so models.run names the refused input/outputd9d6595Merge be-17174-refusal-subject: read a repeated identical refusal subject392075dfix(models): guard the error-class lookup to own keys, and say what refusalSubject's doc can promise9f9a1e9Merge origin/be-17174-refusal-subject into be-17424-run-refusal-subject📊 Changes
7 files changed (+210 additions, -13 deletions)
View changed files
📝
CHANGELOG.md(+6 -2)📝
src/sdk/exceptions.test.ts(+9 -0)📝
src/sdk/exceptions.ts(+25 -0)📝
src/sdk/models.test.ts(+140 -0)📝
src/sdk/models.ts(+17 -3)📝
src/sdk/routerErrors.ts(+10 -6)📝
test/support/router-stub-server.ts(+3 -2)📄 Description
STACKED — merging lands on
be-17174-refusal-subject(owned by @mattmillerai, PR #187), NOTmain. This builds on #187'sREFUSAL_SUBJECT_HEADERand subject normalizer; merge #187 first, then this retargets tomain.ELI-5
When a model refuses a request on content-policy grounds, Router can say which part it refused (for example, "the audio output"). The queued
submit/subscribe/handlecalls already pass that on (#187).comfy.models.rundid not: its error had nowhere to put it. After this change the errormodels.runthrows carries it aserr.refusalSubject, so a caller can change just that part and try again.What changed
ComfyError.refusalSubject: string | null(andrefusalSubject?onComfyErrorOptions), on the base class next toretryAfter/idempotencyKey, defaulting tonull. It's on the base becausemodels.runraises a refusal as a bareComfyError(BY_ERROR_TYPEhas nocontent_policy_violationentry). The value passes through unnarrowed, the same ascode.errorFromResponse(src/sdk/models.ts) readsX-Comfy-Refusal-Subjectfirst and falls back to the body'srefusal_subject. That is the same ordertoRouterErroruses, and the value goes through the same normalizer: trimmed, andnullif blank, not a string, or comma-joined from two different subjects (a repeated header). After #187's latest commit, a header that repeats the same subject still names it. Like #187 and the Python SDK, it isn't gated oncode.readRefusalSubjectis now exported fromsrc/sdk/routerErrors.ts, alongsideparseRetryAfter, whichmodels.tsalready imports from there.RouterError.refusalSubjectdocstring and #187's CHANGELOG bullet no longer saymodels.runlacks the subject. There is a newUnreleased / Addedentry, and one sentence on theRunOptions.retrydocstring wherecontent_policy_violationis listed.ScriptedResponse.headersacceptsstring[], which sends a repeated header line.errorFromResponselooks up the error class by own property only (Object.hasOwn, astoRouterErrordoes). Before, a server bucket ofconstructormademodels.runthrow aStringwrapper instead of aComfyError, droppingrefusalSubject, andtoString/__proto__raised a bareTypeError. That lookup predates this PR, but it would have dropped the subject this PR adds, so it is fixed here.Judgment call: where the normalizer lives
The plan offered two options: export the normalizer from
routerErrors.ts, or move it to a small internal module that both files import. I tried the internal module first, and it brokerouterErrors.test.ts's ES5 down-level suite. That suite transpilesrouterErrors.tsin isolation and fails if the modulerequires anything, on purpose, because it proves theinstanceofchain survives a consumer's ES5 build. So the helper had to stay insiderouterErrors.ts, and I exported it the wayparseRetryAfteris exported. The side effect: it becomes reachable asrouterErrors.readRefusalSubject(and via@comfyorg/sdk/errors), since that module is re-exported whole. The surface-parity check compares error classes,modelsmethods anderror_typemappings, not helper functions, so parity is unaffected.Tests
A new
describe("refusalSubject on models.run")block inmodels.test.ts. Each case drivescomfy.models.run(MODEL, {}, { retry: FAST })against a scripted 400content_policy_violationand assertscodeplus:"output_audio""input_text"nullnullnull. This sends two real header lines, not a pre-joined string. I checked separately that Node'swriteHeadwith an array value reachesfetchasHeaders.get(...) === "input_image, output_text".output_audiotwice) →"output_audio". This matches #187's normalizer change.3) →nullinput_3d_mesh) → passed throughdetail[]validation shape →nullmodels.schema()andmodels.list().page()failures →refusalSubject === nullconstructor/toString/__proto__with a subject header → still aComfyErrorcarryingcodeandrefusalSubject. This fails without the own-property guard.In
exceptions.test.ts,new ComfyError("x").refusalSubject === nulland the option round-trips.Mutation check: with
refusalSubjecthard-coded tonullinerrorFromResponse, the five cases that expect a value fail. (That count predates the same-subject case added since.) The null-expecting cases pass either way by design.Residual
X-Comfy-Refusal-Subject/refusal_subjecthasn't shipped to production yet, so production responses don't carry the header. The stub-server tests above are the only verification. Once the producer is live, run a realcontent_policy_violationthroughcomfy.models.runand confirmerr.refusalSubjectis populated.REFUSAL_SUBJECTSand the header name are still not pinned tospec/router-openapi.yaml. The vendored spec doesn't carry them yet. #187's rot guard inrouter-spec-contract.test.tsflags the sync that brings them in; that sync PR should pin them. Out of scope here.models.runstill raises theComfyErrorfamily rather thanRouterError. Switching it is separate backlog work and not attempted here.Provenance
cdb627a)pnpm lint,pnpm format:check,pnpm typecheckcleanpnpm check:spec-driftOK (both checks)pnpm test: 812 passed, 5 skipped;pnpm buildokpnpm check:sdk-parityOK against the Python SDK'smainrouterErrors.ts(reachable asrouterErrors.readRefusalSubject); the internal-module option broke the module's import-free ES5 test (see "Judgment call")BY_ERROR_TYPE[code]lookup was switched to an own-property one, and therefusalSubjectdoc says what Router sends rather than claiming discovery paths are alwaysnullCHANGELOG.md, theRouterError.refusalSubjectdoc andexceptions.test.tskept both sides (#187's queued-surface wording andstampIdempotencyKeytests, plus this branch'smodels.runsubject)🤖 Generated with Claude Code
🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.