List the models Comfy Router can run.
Comfy Router’s model catalog - one page of the canonical model IDs that POST /v1/models/{provider}/{model} accepts. An SDK calls this on cold start to discover what is runnable, and the model_not_found suggestions come from the same catalog, so an ID listed here that then 404s on invocation would be worse than either failure alone. That agreement is structural rather than a promise: an entry’s provider and model are the two path segments of the invocation route and reference the SAME schema components that route’s path parameters do, and id is those two segments joined by /.
Pagination is CURSOR-based, deliberately not offset-based. The catalog is a moving list - models are added, and embargoed, between calls - and an offset walk silently skips or repeats entries when the list changes underneath it. Pass a page’s next_cursor back as cursor to fetch the next page, and stop when has_more is false rather than when a page comes back short. A cursor is opaque: it is not an offset, not a model ID, and not stable across catalog rebuilds, so a cursor that is malformed or no longer valid is answered with a 400 from the Router error contract (error_type: invalid_input), never a 500.
A model that is deployed but NOT yet released is EXCLUDED from every page. This is a requirement of this route specifically, not something it inherits: PartnerModelEmbargoMiddleware gates /proxy/* only, and only methods that can carry a body, so a bodyless GET outside /proxy/ is outside the embargo gate on both axes. Confirming that a specific unreleased model exists is precisely the disclosure modelembargo was built to prevent, and a catalog is the most direct way to make that confirmation - so the handler must filter the embargo set out of the page itself. An excluded model is simply absent: the catalog does not mark it, does not reserve a slot for it, and has_more/limit describe the page AFTER exclusion, so the omission is not inferable from a short page either.
Per-model detail and the per-model input/output schemas are separate routes; this one carries only the identity of each model.
Authorizations
Bearer token authentication. Normally a Firebase or Cloud JWT. A 'comfyui-' prefixed API key is ALSO accepted here on operations served by the comfyFirebase auth middleware: the prefix classifies the value as an API key and it is validated exactly as if sent in X-API-Key (BE-9720, parity with ingest).
Query Parameters
Opaque pagination cursor. Pass a previous page's next_cursor to fetch the next page; omit it for the first page. See RouterPageCursor for why the value is opaque and why this route paginates by cursor rather than by offset.
A cursor that is malformed, over-long, or no longer valid is answered with a 400 carrying the Router error contract's RouterErrorResponse (error_type: invalid_input) - never a 500, and never a silent fallback to the first page, which would make a walk loop forever.
An OPAQUE cursor into a Router list. It is produced by the server and only ever round-tripped: it is not an offset, not a model ID, not ordered, and not stable across catalog rebuilds, so parsing one, incrementing one, or persisting one beyond the walk it came from are all outside the contract. Cursor rather than offset because the catalog is a moving list - an offset walk silently skips or repeats entries when entries are added or removed mid-walk, and a caller cannot tell that it happened.
maxLength bounds it because the value arrives in a query string and is fed to a decoder; a cursor that is malformed, truncated, over-long or no longer valid is a 400 from the Router error contract, never a 500. minLength: 1 is load-bearing rather than tidy: Echo's QueryParam returns "" for both ?cursor= and an omitted cursor, so without it the empty string is a schema-valid cursor indistinguishable from no cursor at all, and a handler would silently restart the walk at page one - the infinite loop RouterCatalogCursor explicitly forbids. An empty cursor is therefore a 400, not page one. The pattern fixes the alphabet at URL- and base64-safe characters so a control character, a CR/LF, or a space can never ride a query string into the decoder or into a 400's free-text detail. It constrains the SERVER, which is the only party that mints these; as with the model-ID segments it is a CONTRACT statement and not enforcement, since comfy-api installs no OpenAPI request validator - a handler must still validate the value it was handed before decoding it.
1 - 512^[A-Za-z0-9._~+/=-]+$"q7Fm2xTn9pLd4RsV"
Number of models to return in one page. Values above the declared maximum are outside the contract, but this route does not reject them: it serves the maximum instead, and the page size actually served is echoed back as limit on the response, so a clamp is always detectable by the caller. Treat the maximum as the real page stride - a client that asks for more and assumes it received more will miss rows. 0 and negative values are also accepted and select the default, which is why no minimum is declared: sub-1 is meaningful here, not invalid.
The cap of 100 is the one the node-listing endpoints already use (BE-8098): an uncapped page size on a list route is a trivially exploitable amplification, and this route is hit by SDKs on cold start.
x <= 100Response
OK - one page of the model catalog.
One page of the Router model catalog.
has_more is the ONLY correct stop condition for a walk: a short page is not one, because a page can be trimmed by an entry that disappeared between the cursor being minted and the page being served. next_cursor is present exactly when has_more is true, and omitted otherwise. limit echoes the page size actually served, which is what makes a clamped request detectable.
The models on this page, at most limit of them.
Whether another page exists beyond this one. Keep walking while this is true; do not infer the end of the catalog from a short or empty data.
The page size actually served. A requested limit above the maximum is CLAMPED down to the maximum rather than rejected, so this can be smaller than the value asked for - paginate with this number, not with the one you sent, or you will assume rows you never received.
Unlike the REQUEST parameter, this one declares a minimum: sub-1 is meaningful on the way in (it selects the default) but a page size actually served is always positive, and 0 is exactly what an unset Go field serializes to - so without the bound a handler that forgets to populate this still emits a conforming response, and limit: 0 beside has_more: true describes a walk that cannot advance.
1 <= x <= 10020
An OPAQUE cursor into a Router list. It is produced by the server and only ever round-tripped: it is not an offset, not a model ID, not ordered, and not stable across catalog rebuilds, so parsing one, incrementing one, or persisting one beyond the walk it came from are all outside the contract. Cursor rather than offset because the catalog is a moving list - an offset walk silently skips or repeats entries when entries are added or removed mid-walk, and a caller cannot tell that it happened.
maxLength bounds it because the value arrives in a query string and is fed to a decoder; a cursor that is malformed, truncated, over-long or no longer valid is a 400 from the Router error contract, never a 500. minLength: 1 is load-bearing rather than tidy: Echo's QueryParam returns "" for both ?cursor= and an omitted cursor, so without it the empty string is a schema-valid cursor indistinguishable from no cursor at all, and a handler would silently restart the walk at page one - the infinite loop RouterCatalogCursor explicitly forbids. An empty cursor is therefore a 400, not page one. The pattern fixes the alphabet at URL- and base64-safe characters so a control character, a CR/LF, or a space can never ride a query string into the decoder or into a 400's free-text detail. It constrains the SERVER, which is the only party that mints these; as with the model-ID segments it is a CONTRACT statement and not enforcement, since comfy-api installs no OpenAPI request validator - a handler must still validate the value it was handed before decoding it.
1 - 512^[A-Za-z0-9._~+/=-]+$"q7Fm2xTn9pLd4RsV"