mirror of
https://github.com/Comfy-Org/comfy-typescript-sdk.git
synced 2026-11-04 23:47:27 +00:00
[PR #187] feat: expose refusalSubject on RouterError and ComfyError from X-Comfy-Refusal-Subject #186
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#186
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/187
Author: @mattmillerai
Created: 9/25/2026
Status: 🔄 Open
Base:
main← Head:be-17174-refusal-subject📝 Commits (10+)
0cff534feat(routerErrors): expose refusalSubject on RouterError from X-Comfy-Refusal-Subjectee4fb2afix(routerErrors): normalize refusalSubject, null when absent, and guard its unpinned wire names916e75dfix(routerErrors): read a repeated identical refusal subject, and tighten its testsd7c4706Merge origin/main into be-17174-refusal-subject64eb3afMerge origin/main into be-17174-refusal-subjectcdb627adocs(routerErrors): say status()/events() report a refused completion as dataa349941feat(models): carry refusalSubject on ComfyError so models.run names the refused input/output (#191)ffde40dMerge origin/main into be-17174-refusal-subject6ee1919fix(parity): refresh the Python snapshot and retire the QueueBacklogFull leadfa834c0test(router-spec): narrow the parsed Router spec instead of asserting its shape📊 Changes
11 files changed (+594 additions, -19 deletions)
View changed files
📝
CHANGELOG.md(+19 -0)📝
parity/python-surface.json(+6 -3)📝
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/router-spec-contract.test.ts(+107 -0)📝
src/sdk/routerErrors.test.ts(+156 -0)📝
src/sdk/routerErrors.ts(+112 -0)📝
src/sdk/surface-parity.test.ts(+0 -11)📝
test/support/router-stub-server.ts(+3 -2)📄 Description
What
A
routerErrors.ContentPolicyViolation, and theComfyErrorthatcomfy.models.runthrows forcontent_policy_violation, now say which input or output the provider refused, so a caller can change that one part and retry rather than give up:routerErrors.*is raised by the queued surface:submit, and a refused completion fromsubscribeorRequestHandle.get()(status()andevents()report a completion as data onQueueUpdateand do not throw).comfy.models.runraises aComfyError(code: "content_policy_violation"), which now carries the same value onComfyError.refusalSubject(merged in from the stacked #191).RouterErrorOptions.refusalSubject?: string | nullandRouterError.refusalSubject: string | null.nullmeans "not named", matchingrequestId,httpStatusandretryAfter. It sits on the base class, likeretryAfter, so an error with a bucket this release doesn't recognize still keeps it.toRouterErrorreadsX-Comfy-Refusal-Subjectfirst, then the body'srefusal_subject. Both values are trimmed. A blank value names no subject and falls through, the same way an emptyX-Comfy-Error-Typedoes. So does a value that joins two different subjects with a comma (Headers.getjoins a repeated header that way). A repeat of the same subject (input_image, input_image) reads as that subject. A non-string body value is ignored.errorFromCompletiongoes through the same builder, so a queued completion body that carriesrefusal_subjectalso sets it.REFUSAL_SUBJECT_HEADER,REFUSAL_SUBJECTS(the ten values the Router contract documents:input,output,input_text,input_image,input_video,input_audio,output_text,output_image,output_video,output_audio) andtype RefusalSubject. The property itself is not narrowed to that union. This follows the same policy aserrorType: a newer server may send a value this release has never seen, so it passes through unchanged, and the doc tells callers to treat an unknown value as unspecified.ComfyErrorOptions.refusalSubject?: string | nullandComfyError.refusalSubject: string | null, on the base class.models.ts'serrorFromResponsereads it header first, body second, through the samereadRefusalSubjectnormalizertoRouterErroruses (exported fromrouterErrors.ts, asparseRetryAfteris). The read is not gated oncode.errorFromResponsenow looks up the error class withObject.hasOwn(BY_ERROR_TYPE, code), astoRouterErroralready does. Before, anX-Comfy-Error-Typeofconstructorthrew aStringwrapper instead of aComfyError, andtoString/__proto__raised a bareTypeError.Tests
routerErrors.test.ts(newrefusalSubjectblock) covers:null(both from the factory and from direct construction)detail[]validation shape never sets itmodels.test.tscoversmodels.runagainst the Router stub server: header, body fallback, header precedence, absent →null, and a server-supplied bucket named after anObject.prototypekey still yields aComfyError.exceptions.test.tscovers the constructor default and pass-through.router-spec-contract.test.tspinsREFUSAL_SUBJECT_HEADER, the body'srefusal_subjectandREFUSAL_SUBJECTSagainstspec/router-openapi.yaml(see Residual).Judgment calls / deviations
surface-parity.test.tscompares class, method anderror_typenames, and the Python surface snapshot doesn't capture instance attributes. So the new property creates no parity difference, and nothing retires an entry once the Python twin lands. An earlier revision added an entry with only awhy. Review dropped it, and the CHANGELOG now records the lead instead. I did not build attribute extraction into the parity machinery, because that would be a design change the ticket doesn't ask for.X-Comfy-Upstream-Status". This SDK never reads that header, so the read goes intotoRouterError, the one place every Router error response is built.RouterRefusalSubjectHeader/RouterErrorResponse.refusal_subjectdescriptions and declares noenum, so the spec pin parses the backticked list after "closed vocabulary:" in both descriptions.Residual
RouterErrorResponse.refusal_subjectandRouterRefusalSubjectHeader, which fired the rot guard this branch carried, as intended. Merging main replaced that guard with a real pin insrc/sdk/router-spec-contract.test.ts. It checks that every response referencingRouterRefusalSubjectHeadernames itREFUSAL_SUBJECT_HEADER, thatRouterErrorResponsedeclares a stringrefusal_subject, and thatREFUSAL_SUBJECTSmatches the closed vocabulary in both descriptions, in order.comfy-python-sdkhas norefusal_subjectyet. No parity entry tracks it (see above).Response/Headersobjects in unit tests). No realcontent_policy_violationcarrying the header was triggered: that needs a provider that emits a machine-readable refusal code, and a real paid generation.Provenance
6538606(this branch merged with main at5f65833, which cut 0.5.0; this PR's changelog entries now sit in the new Unreleased section):pnpm lint,pnpm format:check,pnpm typecheckandpnpm check:spec-driftpass locally.pnpm test: 818 passed, 5 skipped.REFUSAL_SUBJECTSis altered; it narrows the parsed spec at runtime instead of asserting its shape.pnpm check:sdk-paritypassed against the refreshed Python snapshot (locally atfa834c0; thesdk-parityCI job passed at6538606).6538606is green: Test (node 22 and 24), build-check, sdk-parity and public-repo-hygiene all pass.REFUSAL_SUBJECTSpin parses the contract's prose, because the contract declares noenum.parity/python-surface.jsonand retires theQueueBacklogFulllead now that the Python SDK has it, whichsdk-parityneeded in order to pass.RouterError: #191'sComfyError.refusalSubjectand theObject.hasOwnlookup guard were merged into this branch.🤖 Generated with Claude Code
Summary by CodeRabbit
models.runcontent-policy errors now expose a refusal subject, using the response header first and the response body as a fallback. The value isnullwhen no usable subject is provided.models.runcontent-policy errors.🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.