文档 /
接口 /
Read one partner model's input and output schemas as an OpenAPI document.
Read one partner model's input and output schemas as an OpenAPI document.
The per-model input and output schemas for a single Comfy Router model, served as a standalone OpenAPI document, so a caller - an SDK, a codegen tool, or an agent - can discover a model's arguments, a…
接口
read-one-partner-models-input-and-output-schemas-as-an-openapi-document 更新于 2026-11-13
Read one partner model's input and output schemas as an OpenAPI document.
The per-model input and output schemas for a single Comfy Router model, served as a standalone OpenAPI document, so a caller - an SDK, a codegen tool, or an agent - can discover a model's arguments, and the shape of what it returns, without reading Comfy's prose docs. It is the discovery mechanism the SDK quickstart depends on.
OpenAPI
````yaml /router-openapi.yaml get /v2/models/{provider}/{model}/openapi.json openapi: 3.0.2 info: title: Comfy Router description: >- Comfy Router's public contract: the model catalog and the model-ID-addressed invocation routes, with the error buckets they return. Projected from the canonical Comfy API contract. version: '1.0' servers:
- url: https://api.comfy.org security: [] tags:
- name: Comfy Router description: Comfy Router's canonical, model-ID-addressed routes. paths: /v2/models/{provider}/{model}/openapi.json: get: tags:
- Comfy Router summary: >- Read one partner model's input and output schemas as an OpenAPI document. description: >- The per-model input and output schemas for a single Comfy Router model, served as a standalone OpenAPI document, so a caller - an SDK, a codegen tool, or an agent - can discover a model's arguments, and the shape of what it returns, without reading Comfy's prose docs. It is the discovery mechanism the SDK quickstart depends on. operationId: getRouterModelInputSchema parameters:
- $ref: '#/components/parameters/RouterProvider'
- $ref: '#/components/parameters/RouterModel'
- in: header name: If-None-Match required: false description: >- The
ETaga caller holds from an earlier200. When it matches the current document (RFC 9110 weak comparison;*matches any current document) the answer is a bodyless304carrying the sameETag, otherwise the full document. schema: type: string responses: '200': description: >- OK - the model's input and output schemas, as a standalone OpenAPI document. headers: X-Comfy-Request-Id: $ref: '#/components/headers/RouterRequestIdHeader' ETag: $ref: '#/components/headers/RouterSchemaETagHeader' Cache-Control: $ref: '#/components/headers/RouterSchemaCacheControlHeader' content: application/json: schema: $ref: '#/components/schemas/RouterModelInputSchemaDocument' '304': description: >- Not Modified - the document is unchanged since theETagthe caller sent inIf-None-Match. No body is returned. headers: X-Comfy-Request-Id: $ref: '#/components/headers/RouterRequestIdHeader' ETag: $ref: '#/components/headers/RouterSchemaETagHeader' Cache-Control: $ref: '#/components/headers/RouterSchemaCacheControlHeader' '401': $ref: '#/components/responses/RouterRequestError' '403': $ref: '#/components/responses/RouterRequestError' '404': $ref: '#/components/responses/RouterRequestError' '429': $ref: '#/components/responses/RouterRateLimited' '500': $ref: '#/components/responses/RouterRequestError' '503': $ref: '#/components/responses/RouterRequestError' security: - BearerAuth: []
- ApiKeyAuth: [] components: parameters: RouterProvider: name: provider in: path required: true description: >- Lowercase provider segment of the canonical
{provider}/{model}model ID - the partner whose model is being run. schema: $ref: '#/components/schemas/RouterProviderSegment' RouterModel: name: model in: path required: true description: >- Lowercase model segment of the canonical{provider}/{model}model ID - the model to run within that provider. schema: $ref: '#/components/schemas/RouterModelSegment' headers: RouterRequestIdHeader: description: >- Server-generated identifier for this call, present on every Router response - success, 4xx and 5xx alike, because an error response is exactly when a user needs an id to quote in a support request. The same value is written into the call's usage/audit event, which is what lets a complaint about a charge be joined to the charge itself instead of searched for by timestamp. required: true schema: type: string format: uuid example: 6f1a1a6e-6a53-4a5f-9d3a-2b3b0a1f9c21 RouterSchemaETagHeader: description: >- Strong entity tag over the served document's bytes, for `GET /v2/models/{provider}/{model}/openapi.json`. A per-model schema changes rarely and an SDK re-fetches it often, so a caller should store this value and send it back asIf-None-Matchto get a304instead of the document. required: true schema: type: string example: '"6b8c1f2e0a9d4c3b5e7f8a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f"' RouterSchemaCacheControlHeader: description: >- Freshness directives for the served schema document.privatebecause the route is authenticated - the document itself is not caller-specific, but a shared cache must not hold a response to an authenticated request - and
must-revalidateso a stale copy is revalidated against theETagrather than served on. required: false schema: type: string example: private, max-age=300, must-revalidate RouterErrorTypeHeader: description: >- Coarse, machine-readable bucket for the failure, set by Router on every error response. It carries the same value asRouterErrorResponse.error_type, and on the422it is the only machine-readable bucket, because that body is the FastAPIdetail[]shape and has noerror_typefield of its own. A client can therefore branch on this header alone, before deciding which of the two Router error bodies it received. required: true schema: $ref: '#/components/schemas/RouterErrorType' RouterRateLimitRetryAfterHeader: description: >- Seconds to wait before sending another request after a429carryingerror_type: rate_limited: the time until the caller's per-minute request allowance has refilled enough to admit one more request. It is sent on everyrate_limitedrefusal and on no other answer, so it is absent on a429whoseX-Comfy-Error-Typeisconcurrency_limit_exceededorqueue_backlog_full. Nothing the caller does drains it early; a request sent sooner is refused again and spends nothing, but gains nothing either. required: false schema: type: integer minimum: 1 example: 2 schemas: RouterModelInputSchemaDocument: type: object description: >- A standalone OpenAPI document describing one Comfy Router model's input and output - the bodyPOST /v2/models/{provider}/{model}accepts for that model, under the operation'srequestBody, and the body it returns, under that operation's200content. It is what `GET /v2/models/{provider}/{model}/openapi.json` returns. The component keeps its historical name, which predates the output half; the shape it describes is the whole document, not the input alone. additionalProperties: true RouterProviderSegment: type: string description: >- Lowercaseprovidersegment of the canonical{provider}/{model}model ID - the partner whose model is being addressed. The invocation route'sproviderpath parameter and a catalog entry'sproviderfield both reference this one schema, which is what keeps the listed IDs and the accepted IDs from drifting apart. pattern: ^[a-z0-9]+([._-][a-z0-9]+)*$ maxLength: 64 example: bfl RouterModelSegment: type: string description: >- Lowercasemodelsegment of the canonical{provider}/{model}model ID - the model to run within that provider. Shared by the invocation route's
modelpath parameter and a catalog entry'smodelfield, for the same no-drift reason asRouterProviderSegment. pattern: ^[a-z0-9]+([._-][a-z0-9]+)*$ maxLength: 128 example: flux-2-pro RouterErrorResponse: type: object description: >- Router's request-level error body: what is returned when the request never reached the model, or failed for a reason the model itself did not report - auth, quota, an unknown model ID, or provider transport. A model-level validation failure has its own shape,RouterValidationErrorResponse, because flattening a FastAPIdetail[]array into thisdetailstring would destroy the per-field granularity an SDK branches on. properties: detail: type: string description: >- Human-readable description of the failure, safe to surface to an end user. Not machine-parsed - branch onerror_typeinstead. Formodel_not_foundon a retired model,detailreads `<id> was retired on <YYYY-MM-DD>; use <successor>` when a successor is recorded and visible to the caller, or just `<id> was retired on <YYYY-MM-DD>` when it is not, instead of the usual suggestion list. A successor named here is always a live catalog ID the call can be re-pointed at. A model whose sunset date is still in the future is not retired: it stays listed byGET /v2/modelsand stays runnable until that date. Like the rest ofdetail, this retirement wording is for display and carries no compatibility guarantee. error_type: $ref: '#/components/schemas/RouterErrorType' upstream_detail: type: string description: >- A bounded, sanitized reason for aninvalid_inputrejection. On the synchronous route it is the reason the model provider gave, present only whenerror_typeisinvalid_inputandX-Comfy-Upstream-Statusis a provider4xxor2xx- i.e. the provider refused the request as malformed and said why. Usually that status is a4xx; it is a2xxfor a provider that reports a rejected generation inside a success envelope (a BytePlus failed-task poll is HTTP200with the reason in its body). There it is absent on every other failure, including provider5xx, transport failures, content-policy refusals and any refusal Router raised about itself, and it mirrors theX-Comfy-Upstream-Detailheader. The queued collect (`GET /v2/models/{provider}/{model}/requests/{request_id}`) of a run that endedinvalid_inputcarries the same reason in this field on its first collect, which then deletes it from the request: a later collect normally answers the sameerror_typeanddetailwithout it, so read it from the first collect. That read sends neither header. The queued collect also uses this field for Router's own reason when Router refused the request before sending it to the provider and can state why in one sentence, which the synchronous route answers indetail; a refusal that names individual fields carries none. refusal_subject: type: string description: >- Which input or output a content-policy refusal was about, as a Router-level closed vocabulary:input,output,input_text,input_image,input_video,input_audio,output_text,output_image,output_video,output_audio. The bareinput/outputvalues name the side when the provider did not name a modality. Present only whenerror_typeiscontent_policy_violationand the provider named the refused subject with a machine-readable code; absent otherwise. Never provider text. Named today for BytePlus, Runway, BFL, Gemini, Veo, Vertex, xAI and Wan refusals, and for the MiniMax Router's Nano Banana legs' Gemini refusals; a provider whose refusal does not say which side it was about leaves it absent. It mirrors theX-Comfy-Refusal-Subjectheader. denied_reason: type: string description: >- Billing's policy branch for a partner-node spend denial -no_fundswhen the billed wallet is empty,unspecifiedwhen the call was refused without a funds reading saying so. Present only whenerror_typeisinsufficient_creditsand the deployment reports wallet facts; absent otherwise. A machine-readable code, not display text: showdetail, branch on this. billed_workspace: $ref: '#/components/schemas/RouterBilledWorkspace' credential_kind: type: string description: >- How the refused request authenticated -personal_api_key,team_api_key,personal_sessionorteam_session. Answers "which credential do I switch to bill the other wallet?". It is not a permission and grants nothing. Present only alongsidedenied_reason. required: - detail
- error_type RouterErrorType: type: string description: >- Coarse, machine-readable bucket for a Router failure, mirrored on the
X-Comfy-Error-Typeresponse header so a caller can branch without parsing the body. The set is closed at twenty values: the six request-level bucketsinvalid_input,content_policy_violation,provider_error,provider_timeout,insufficient_creditsandmodel_not_found, plus the transport-levelunauthorized,forbidden,concurrency_limit_exceeded,client_disconnected,internal_error,deadline_exceeded,not_enabled,service_unavailable,rate_limited,cancelled,queue_timeout,request_not_found,queue_backlog_fullandresult_expired. Closed describes the set as documented today, not a bound that holds forever: the set is expected to grow, which is why this is deliberately a plain string and not anenum, so a client must treat an unrecognised value asinternal_errorrather than switch exhaustively over the list above and break on the next addition. example: invalid_input x-comfy-error-types: - value: invalid_input tier: request meaning: >- The request was rejected before it reached the model - a malformed body, a malformed or expired pagination cursor, an input the model's own schema does not accept, or an
Idempotency-Keythat cannot serve this request (already used for a different request - the method, the path and query, or the body differ - or already consumed by a call whose response cannot be replayed). Sent with409in the key cases and with400/422in the others; the status says which, and the key cases are the ones answered by using a new key rather than by editing the request. - value: content_policy_violation tier: request meaning: >- The provider refused the request on content-policy grounds. The refusal is deterministic: re-sending the same input will be refused again.
- value: provider_error tier: request meaning: >- The partner provider reported a failure of its own, or returned a response Router could not interpret as a result.
- value: provider_timeout tier: request meaning: >- The partner provider did not answer within its deadline. This bucket is the provider timing out and never Router's own server deadline, which is reported as
deadline_exceeded- the two share504and are separated because they name different causes: this one says the partner failed, that one says Comfy stopped holding the connection. - value: insufficient_credits tier: request meaning: The calling workspace does not have enough credits to run the model.
- value: model_not_found tier: request meaning: >- The
{provider}/{model}ID names no model Router can run: an unknown provider, an unenrolled id, one the catalog has retired past its sunset date, or a catalogued ID the provider does not currently serve for Comfy. A retired id'sdetailreads `<id> was retired on <YYYY-MM-DD>; use <successor>` when a successor is recorded and not withheld, or just<id> was retired on <YYYY-MM-DD>when it is not, instead of a suggestion list; the successor named for a retired id is always a live catalog ID you can re-point the call at, and a successor is dropped fromdetaileither because the table records none, or because the recorded one is currently withheld behind an embargo the caller cannot see. A model whose sunset is still in the future is not withdrawn: it stays listed byGET /v2/modelsand stays runnable until the date, so a listing taken today remains the answer to what runs today. Otherwisedetailcarries up to three suggestions drawn from the models the caller is entitled to see, except for a catalogued ID the provider does not currently serve for Comfy, whose response carries no suggestions. - value: unauthorized tier: transport meaning: The request carried no usable credential.
- value: forbidden tier: transport meaning: >- The credential is valid but is not entitled to this model or this operation.
- value: concurrency_limit_exceeded tier: transport meaning: >- The workspace already has as many calls in flight as it is allowed; retry once one of them finishes. It carries one further condition on the run route, on a
409rather than the429above: another call is already in flight for theIdempotency-Keythis request presented. Re-send the same key afterRetry-Afterseconds to collect that call's result. - value: client_disconnected tier: transport meaning: >- The caller closed the connection before Router could return a result. It is logged rather than delivered - there is no socket left to write it to - and it is an attribution, not a billing outcome: a provider generation that completed is billed regardless of whether the caller received the response.
- value: internal_error tier: transport meaning: >- Router itself failed. It is also the value a client should treat any unrecognized bucket as, so a later addition to the set does not break a client generated before it.
- value: deadline_exceeded tier: transport meaning: >- Comfy stopped holding the connection at its own configured bound before an answer arrived. It shares
504withprovider_timeoutand the pair says which side ran out of time; this one is Comfy's own bound, so nothing about the request was rejected and the same request may be retried. It says nothing about the charge: a provider generation that completed is billed regardless of whether the caller received the response. Retry it with the sameIdempotency-Key: when the provider had already accepted the generation, the retry collects that generation rather than dispatching another, and aRetry-Afteron the504says when to ask. - value: not_enabled tier: transport meaning: >- Comfy Router is not switched on for this caller yet. Nothing about the request is wrong and the model exists, which is why this is not
model_not_found; it shares403withforbiddenand is not the same thing, becauseforbiddenis an entitlement decision about the caller while this is a state of the rollout. It is terminal: do not retry, and do not treat it as an outage. The one exception to "about the caller" is the queued submit, which also answersnot_enabledfor a model whose partner answers a generation directly as bytes: that model cannot yet be queued, so it is the model and not the caller that is refused, nothing is queued or charged, and the synchronous routePOST /v2/models/{provider}/{model}runs it instead. - value: service_unavailable tier: transport meaning: >- A service Comfy Router depends on is temporarily unavailable and the caller did nothing wrong. Retry it with backoff: it is the one bucket here whose condition clears on its own, without the caller changing the request and without a concurrency slot freeing, which is what distinguishes it from the other retryable answers (
concurrency_limit_exceeded,deadline_exceeded). It is separate frominternal_error- which is a500and means Router itself failed - so a client can tell "come back shortly" from "this call is not going to work". - value: rate_limited tier: transport meaning: >- The caller has spent an allowance measured over a window and must wait for that window to roll. It shares
429withconcurrency_limit_exceededand is not the same thing: that one clears the moment one of the caller's own in-flight calls finishes, so retrying in seconds is right, whereas nothing the caller does drains this one early.detailnames the window, andRetry-Afterthe seconds to wait. - value: cancelled tier: transport meaning: >- A queued request was withdrawn — through the cancel route, or by an operator — before it produced a result; it is terminal, and it is not by itself a statement about the charge. Cancelling stops Comfy waiting and it does not always stop the partner working, so a request cancelled while it was still
IN_QUEUEwas never dispatched and cannot be charged, whereas one cancelled after it was admitted may still be charged — a partner generation that completes is charged whether or not anyone collected it, which is why the cancel route calls the ask a request rather than a guarantee. It is notclient_disconnected: that one says nobody is listening any more while a generation may still be running and billable, whereas this says the request itself was withdrawn. A caller polling the status read sees200with this in the body, because the request ended and so the read succeeded. Collecting the result of such a request answers409in Router's error envelope: that read also worked, and what it found is a request whose terminal state - the caller's own decision - leaves nothing to return. It is deliberately not410, which on that route means the result aged out of its retention window, and not a5xx, which would report the caller's own cancellation as a Router fault and read to a polling client as worth retrying. - value: queue_timeout tier: transport meaning: >- A queued request waited past its queue timeout without ever being admitted. Terminal, unbilled, and it never took a concurrency slot — the job never reached a provider. Deliberately not
deadline_exceeded, which is the synchronous route's connection bound: adeadline_exceededgeneration may be running and billable, whereas this one provably never started. It is returned under504, which it shares withprovider_timeoutanddeadline_exceededbecause a status can only say that a clock ran out; which clock - the partner's, Comfy's connection bound, or the queue's admission bound - is whaterror_typecarries. The request itself is terminal, so submit a new one rather than re-reading this one. - value: request_not_found tier: transport meaning: >- The
request_idnames no request of the caller's under this model. It is the second of the two conditions the queued reads'404covers; the first is the{provider}/{model}ID resolving to no partner model, which ismodel_not_foundand carries fuzzy model suggestions. It also covers the right-id / wrong-model URL the path shape refuses, and it is deliberately indistinguishable from a request in another workspace, so a probe with a guessed id learns nothing. A request that has merely aged out of its retention window is410, not this. - value: queue_backlog_full tier: transport meaning: >- The caller already has too many queued requests waiting to run, so this submit was refused. It shares
429withconcurrency_limit_exceededand is not the same thing: that one is the synchronous route's answer for too many calls in flight at once, whereas the queue accepts a submit at that limit and parks it, and this bucket is the separate bound on how many a caller may leave waiting so that parking cannot mean enqueuing without end. It clears as the caller's own queued requests finish, so retry once some of them complete. - value: result_expired tier: transport meaning: >- The request existed and finished, but its result is no longer stored, so the collect has nothing to return. It is returned under
410and is permanent for that id: re-reading will answer the same way, so do not retry it. It is deliberately notrequest_not_found, which would tell you the id was never valid and invite re-submitting a generation that has already run. Running the model again means submitting a new request. For a workspace under a zero-data-retention agreement, a queued request's result is not kept after it is delivered: it is removed shortly after the first collect, and an uncollected result is removed after a grace period once the request finishes, so store the result on your side when you first collect it. RouterBilledWorkspace: type: object description: >- The wallet a refused call would have charged, onRouterErrorResponse.billed_workspace. Answers "which wallet is empty?" - it is not necessarily the workspace the caller's UI currently has selected, and a team member's personal credential names their personal wallet here even while a funded team is open. Present only alongside
denied_reason. properties: id: type: string description: The Cloud workspace id. Absent on the legacy rail. type: type: string description: >-personalorteam. An open string: treat any other value as personal. rail: type: string description: >-legacywhen the call bills the caller's pre-workspace Metronome customer rather than a Cloud workspace; that customer has no workspace id. Absent for a Cloud workspace. required: - type responses: RouterRequestError: description: >- A Router request-level failure - the request never reached the model, or failed for a reason the model itself did not report. The body is
RouterErrorResponseand the bucket is repeated onX-Comfy-Error-Type. headers: X-Comfy-Error-Type: $ref: '#/components/headers/RouterErrorTypeHeader' X-Comfy-Request-Id: $ref: '#/components/headers/RouterRequestIdHeader' content: application/json: schema: $ref: '#/components/schemas/RouterErrorResponse' RouterRateLimited: description: >- The caller has spent their per-caller request allowance, which refills continuously over a one-minute window, and the request was refused before Router did anything with it. The bucket is alwaysrate_limited: wait the number of seconds inRetry-Afterbefore sending the next request. Nothing was charged, and a request sent sooner is refused again. The body isRouterErrorResponseand the bucket is repeated onX-Comfy-Error-Type. headers: X-Comfy-Error-Type: $ref: '#/components/headers/RouterErrorTypeHeader' X-Comfy-Request-Id: $ref: '#/components/headers/RouterRequestIdHeader' Retry-After: $ref: '#/components/headers/RouterRateLimitRetryAfterHeader' content: application/json: schema: $ref: '#/components/schemas/RouterErrorResponse' securitySchemes: BearerAuth: type: http scheme: bearer bearerFormat: JWT description: >- Bearer token authentication. Normally a Firebase or Cloud JWT. A 'comfyui-' prefixed API key is also accepted in this header: the prefix classifies the value as an API key and it is validated exactly as if it had been sent in X-API-Key. ApiKeyAuth: type: apiKey in: header name: X-API-Key description: >- API key authentication. Send the key in the X-API-Key header; keys are prefixed with 'comfyui-' and are generated from user account settings. The same 'comfyui-' key is also accepted in Authorization: Bearer (see BearerAuth), and when both headers carry a key, X-API-Key wins.
````