mirror of
https://github.com/Comfy-Org/comfy-typescript-sdk.git
synced 2026-11-04 23:47:27 +00:00
[PR #138] [MERGED] feat(models): add comfy.models.schema() and comfy.models.list() for Router discovery #139
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#139
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/138
Author: @mattmillerai
Created: 9/10/2026
Status: ✅ Merged
Merged: 9/14/2026
Merged by: @mattmillerai
Base:
main← Head:matt/be-13257-models-schema-list📝 Commits (4)
5a25976feat(models): add comfy.models.schema() and comfy.models.list() for Router discovery2b8c472fix(models): address Cursor review findings on the discovery surface9336475Merge remote-tracking branch 'origin/main' into matt/be-13257-models-schema-lista80e738fix(models): green the CI gate and answer the CodeRabbit review (BE-13257)📊 Changes
12 files changed (+1761 additions, -30 deletions)
View changed files
📝
README.md(+80 -3)📝
parity/python-surface.json(+8 -2)📝
scripts/check-spec-drift.mjs(+47 -4)📝
scripts/router-route-contract.mjs(+89 -5)📝
src/sdk/index.ts(+12 -0)📝
src/sdk/modelRequests.test.ts(+5 -0)📝
src/sdk/modelRequests.ts(+12 -1)➕
src/sdk/models-discovery.test.ts(+720 -0)📝
src/sdk/models.ts(+517 -3)📝
src/sdk/router-spec-contract.test.ts(+165 -3)📝
src/sdk/surface-parity.test.ts(+9 -9)📝
test/support/router-stub-server.ts(+97 -0)📄 Description
ELI-5
Comfy Router can already tell you which models it runs and what arguments each one takes. This SDK could not ask it either question — you could only
run()a model whose ID and input shape you already knew. This addscomfy.models.list()(walk the catalog) andcomfy.models.schema(model)(fetch one model's published OpenAPI document), so discovering a model and running it are three calls on one namespace instead of two hand-rolledfetches plus a re-implementation of this SDK's auth, error mapping and request-id handling.What changed
comfy.models.schema(model, options?)GETs/v2/models/{provider}/{model}/openapi.jsonand resolves to{ unchanged: false, document, etag, requestId }. Pass anetagyou already hold and it goes out asIf-None-Match; a304resolves as an explicit{ unchanged: true, document: undefined, etag, requestId }— not a throw, not an empty document. The result is a discriminated union onunchanged, so reading.documentwithout narrowing is a compile error rather than a silentundefinedtreated as an empty schema.comfy.models.list(options?)returns a lazy handle that is async-iterable over models, followingnext_cursoruntilhas_moreis false. Nothing is sent until the iteration starts, and each iteration is a fresh walk.list().page()is the single-page form for a caller driving its own pagination ({ data, hasMore, nextCursor, limit, requestId }), withpage(overrides)merged over the optionslist()was given.Both methods go through
run()'s own credential gate, base-URL resolution,X-Comfy-Request-Idcapture andX-Comfy-Error-Typetable — the four things a hand-rolledfetchhad to re-implement. The credential check and the model-ID parse are now shared functions rather than copies, sorun()andschema()cannot diverge on which IDs they accept.The document is returned as data and is not validated here, and no validator joins the runtime dependencies. That is not only a scope call: these are OpenAPI 3.0.2 documents — JSON Schema draft-04 plus
nullable, which stock Ajv does not cover — so a consumer wantsajv-draft-04and its own decisions about it, and a browser-loadable package should not carry one.Route coverage is now a gate. Every operation
spec/router-openapi.yamldeclares maps either to acomfy.modelsmethod or to a written-down reason there is none, asserted in the unit suite and inpnpm check:spec-drift. This omission could sit unnoticed for as long as it did precisely because nothing in the repo looked at that file'spathsat all — the existing drift check reads onlyx-comfy-error-types, and its own header says so.Sweep, including the half not fixed. The vendored contract declares four operations. Before this change one of the four had an SDK method (
runRouterModel); after it, three of four do. The fourth,getRouterModel(per-model catalog detail), still has none — see Residual. On the cross-SDK side the Python SDK exposesrunand nothing else on bothModelsandAsyncModels, so 0 of these 2 methods exist there today; that lag is declared insurface-parity.test.ts's allowlist with a rot guard that fails the moment the Python snapshot grows either name.Acceptance criteria
schema(model, options?)calls the per-modelopenapi.jsonroute and resolves to the document plus itsETag— asserted on the wire path, the document and the tagetagis sent asIf-None-Match, and a304resolves as an explicit "unchanged" — plus a case where the304echoed no tag and the caller's own is keptlist(options?)calls/v2/modelsand iterates every model across pages; a three-page stub asserts every model is yielded exactly once, in cursor order, in three requestslist().page()) and the default iteration is over models, not pagesX-Comfy-Request-Id— asserted by test on both methods, not by inspectionschema()on an unknown ID raises the same classrun()raises against the stub's404— asserted asfromSchema.constructor === fromRun.constructor, so a future change to one path that misses the other failspackage.jsonand pins the dependency set to exactlyeventsource-parser,hash-wasm,zodrouter-spec-contract.test.ts, the routes sibling of the error-bucket check), in both directions: an operation with no entry fails, and a stale entry naming a removed operation failsModelsdocstring still says the package carries no copy of the schemas, and now says whereschema()gets them insteadbfl/flux-2-pro, the ID the vendored contract itself carries as itsRouterModelIdexample and the one the README already uses throughoutJudgment calls
Neither method retries.
run()retries because it holds a connection for a whole generation and losing it can cost a paid result; a catalog page or a schema document carries no idempotency key and costs nothing to re-ask for. Adding a retry policy to a read is a separate decision and I did not make it here.The discovery deadline is 30s, not
run()'s 20 minutes, and it applies per page on a walk rather than to the whole walk — a shared budget would make a large catalog fail for no reason but its size.A malformed catalog page raises rather than truncating. A page with no boolean
has_more, one claiminghas_more: truewith nonext_cursor, and one repeating a cursor it already served each raiseunexpected_response. Guessing "last page" there is exactly the silent-truncation failure these methods exist to prevent, and an SDK that hangs on a cursor loop is worse than one that raises.CatalogModeltypes only the identity (id,provider,model) and passes everything else through an index signature. The contract calls those three the minimum needed to invoke a model; hand-mirroring the rest of the catalog's fields here is the thing that goes stale silently, which is the same reasoning that keeps the schema documents out of this package.The ticket's design named
toRouterErroras the error mapping to reuse. Onmainmodels.rundoes not use it — it mapsX-Comfy-Error-Typethrough its own table inmodels.tsto theexceptions.tsclasses (NotFound,Unauthorized, …). I followedrun(), because the operative acceptance criterion is thatschema()raises the same classrun()raises, and the test asserts that identity rather than asserting two class names separately. See Residual for what this turned up.Unexercised artifacts
401without one, so everything here is verified against the vendored contract and a local stub, not against a deployed Router.ideogram/ideogram-v3in particular — named as the live 200 — was never called from here.nullablefinding they carry is taken from the ticket's own account of them rather than re-derived. It shapes only a documented recommendation (ajv-draft-04), not any behaviour in this diff.Residual
getRouterModel(GET /v2/models/{provider}/{model}) still has no SDK method. The vendored contract declares four operations; this change takes SDK coverage from one to three. The fourth is per-model catalog detail (RouterModelDetail) — distinct from the schema document, which isgetRouterModelInputSchemaand is covered here. The scoping work named only the catalog and the schema routes as "the two discovery endpoints", so this third one appears to have been missed rather than excluded. It is recorded explicitly in the newROUTE_COVERAGEmap with its reason, so it is now visible rather than invisible. Nothing blocks exposing it:list()already yields the identity fieldsrun()andschema()need, so this is additive, not a gap that breaks anything today. Worth its own ticket.toRouterErrorand the wholerouterErrors.*class hierarchy have no production caller.toRouterErroris exported, documented as what you catch fromcomfy.models.run, and guarded by a spec-drift test that pins fifteen classes to the vendored contract — but greppingsrc/for a non-test caller returns only its own definition.models.runraises theexceptions.tsclasses instead. This is pre-existing onmain, not introduced here, and it is why the design item saying "reusetoRouterError" and the acceptance criterion saying "the same classrun()raises" pull in opposite directions; I satisfied the criterion. Someone should decide which of the two error surfaces is the real one — an integrator following therouterErrorsdocstring today writes acatchthat never fires.Python SDK parity is not done and is out of scope by design, so
comfy_sdk.models.Models/AsyncModelsstill exposerunalone. The lag is declared in the parity allowlist with a rot guard, so the allowlist entry cannot outlive the lag — but until the Python side lands, the two SDKs'modelssurfaces genuinely differ.Live verification is missing, as above. Everything in this diff is proven against the vendored contract and a local stub. The one claim a stub cannot make is that a deployed Router's catalog page and schema document match the shapes asserted here.
Provenance
pnpm test: 519 passed, 0 failed (475 on the base branch, 44 new);pnpm typecheck,pnpm lint,pnpm format:check,pnpm build: clean;pnpm check:spec-drift: OK on both halves;node scripts/check-public-repo-hygiene.mjs: OKrun()'s actual table rather than the named-but-uncalledtoRouterError, and the single-page form hangs offlist()(list().page()) rather than being a third top-level method, which is what "two methods on the namespace" and "also expose the single-page form" together allow.Summary by CodeRabbit
New Features
Documentation
🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.