> ## Documentation Index
> Fetch the complete documentation index at: https://docs.comfy.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Read one partner model's input schema as an OpenAPI document.

> The per-model input schema 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 without reading Comfy's prose docs. It mirrors fal's per-model schema endpoint, and it is the discovery mechanism the SDK quickstart depends on.
The document served is the SAME source the server validates a call against before the request reaches the provider. That is the property that makes it worth trusting: what Comfy publishes and what Comfy enforces are one document, not two copies that drift. Both read the schema through a single accessor (`routerschema.Source.InputSchema`), so they cannot diverge without deleting it.
The path prefix is the SAME canonical `{provider}/{model}[/{variant}]` model ID that `POST /v1/models/{provider}/{model}` invokes, with `/openapi.json` appended - a caller reads an ID out of the catalog, appends one literal segment, and gets that model's schema. `provider` and `model` are governed by the same `RouterProvider` / `RouterModel` parameters, with the same alphabet.
One constraint this puts on the still-unsettled `{variant}` segment: `openapi.json` is itself a legal value under the `RouterModel` alphabet, so whatever spelling addresses a variant must not make `/v1/models/{provider}/{model}/openapi.json` ambiguous with a variant literally named `openapi.json`. Reserving that one literal is the cheapest resolution, and it is noted here rather than resolved, because how the variant segment is addressed is not settled by this contract.
Only the model's INPUT is described. M1 returns the provider's native output unchanged, so there is no Comfy-owned output shape to describe and the document deliberately does not invent one.
A model whose schema has not been authored yet is served a MINIMAL PERMISSIVE document with `200`, NOT a `404`: the model exists, `GET /v1/models/{provider}/{model}` reports it and `POST` runs it, so 404 here would have two Router routes disagreeing about whether the same model exists. The permissive document says the true thing instead - this model takes a JSON object and Comfy has not yet narrowed which fields - and flags itself with `x-comfy-input-schema-authored: false` so a caller can tell "unconstrained" from "constrained to an open object". A `404` on this route means only what it means everywhere else in Router: `model_not_found`, the ID names nothing.
The response is cacheable. It carries a strong `ETag` over the document bytes and honours `If-None-Match` with a `304`, because an SDK re-fetches this document far more often than the document changes.
This operation is deliberately tagged `Comfy Router` and NOT `API Nodes`, for the reason given on the invocation route.



## OpenAPI

````yaml https://api.comfy.org/openapi get /v1/models/{provider}/{model}/openapi.json
openapi: 3.0.2
info:
  title: Comfy API
  version: '1.0'
servers:
  - url: https://api.comfy.org
security: []
tags:
  - description: Comfy Router's canonical, model-ID-addressed routes.
    name: Comfy Router
paths:
  /v1/models/{provider}/{model}/openapi.json:
    get:
      tags:
        - Comfy Router
      summary: Read one partner model's input schema as an OpenAPI document.
      description: >-
        The per-model input schema 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 without reading Comfy's prose
        docs. It mirrors fal's per-model schema endpoint, and it is the
        discovery mechanism the SDK quickstart depends on.

        The document served is the SAME source the server validates a call
        against before the request reaches the provider. That is the property
        that makes it worth trusting: what Comfy publishes and what Comfy
        enforces are one document, not two copies that drift. Both read the
        schema through a single accessor (`routerschema.Source.InputSchema`), so
        they cannot diverge without deleting it.

        The path prefix is the SAME canonical `{provider}/{model}[/{variant}]`
        model ID that `POST /v1/models/{provider}/{model}` invokes, with
        `/openapi.json` appended - a caller reads an ID out of the catalog,
        appends one literal segment, and gets that model's schema. `provider`
        and `model` are governed by the same `RouterProvider` / `RouterModel`
        parameters, with the same alphabet.

        One constraint this puts on the still-unsettled `{variant}` segment:
        `openapi.json` is itself a legal value under the `RouterModel` alphabet,
        so whatever spelling addresses a variant must not make
        `/v1/models/{provider}/{model}/openapi.json` ambiguous with a variant
        literally named `openapi.json`. Reserving that one literal is the
        cheapest resolution, and it is noted here rather than resolved, because
        how the variant segment is addressed is not settled by this contract.

        Only the model's INPUT is described. M1 returns the provider's native
        output unchanged, so there is no Comfy-owned output shape to describe
        and the document deliberately does not invent one.

        A model whose schema has not been authored yet is served a MINIMAL
        PERMISSIVE document with `200`, NOT a `404`: the model exists, `GET
        /v1/models/{provider}/{model}` reports it and `POST` runs it, so 404
        here would have two Router routes disagreeing about whether the same
        model exists. The permissive document says the true thing instead - this
        model takes a JSON object and Comfy has not yet narrowed which fields -
        and flags itself with `x-comfy-input-schema-authored: false` so a caller
        can tell "unconstrained" from "constrained to an open object". A `404`
        on this route means only what it means everywhere else in Router:
        `model_not_found`, the ID names nothing.

        The response is cacheable. It carries a strong `ETag` over the document
        bytes and honours `If-None-Match` with a `304`, because an SDK
        re-fetches this document far more often than the document changes.

        This operation is deliberately tagged `Comfy Router` and NOT `API
        Nodes`, for the reason given on the invocation route.
      operationId: GetRouterModelInputSchema
      parameters:
        - $ref: '#/components/parameters/RouterProvider'
        - $ref: '#/components/parameters/RouterModel'
        - description: >-
            The `ETag` a caller holds from an earlier `200`. When it matches the
            current document (RFC 9110 weak comparison; `*` matches any current
            document) the answer is a bodyless `304` carrying the same `ETag`,
            otherwise the full document.
          in: header
          name: If-None-Match
          schema:
            type: string
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RouterModelInputSchemaDocument'
          description: OK - the model's input schema, as a standalone OpenAPI document.
          headers:
            Cache-Control:
              $ref: '#/components/headers/RouterSchemaCacheControlHeader'
            ETag:
              $ref: '#/components/headers/RouterSchemaETagHeader'
            X-Comfy-Request-Id:
              $ref: '#/components/headers/RouterRequestIdHeader'
        '304':
          description: >-
            Not Modified - the document is unchanged since the `ETag` the caller
            sent in `If-None-Match`. No body is returned.
          headers:
            Cache-Control:
              $ref: '#/components/headers/RouterSchemaCacheControlHeader'
            ETag:
              $ref: '#/components/headers/RouterSchemaETagHeader'
            X-Comfy-Request-Id:
              $ref: '#/components/headers/RouterRequestIdHeader'
        '401':
          $ref: '#/components/responses/RouterRequestError'
        '403':
          $ref: '#/components/responses/RouterRequestError'
        '404':
          $ref: '#/components/responses/RouterRequestError'
        '500':
          $ref: '#/components/responses/RouterRequestError'
        '503':
          $ref: '#/components/responses/RouterRequestError'
      security:
        - BearerAuth: []
components:
  parameters:
    RouterProvider:
      description: >-
        Lowercase provider segment of the canonical
        `{provider}/{model}[/{variant}]` model ID - the partner whose model is
        being run.

        The schema is `RouterProviderSegment`, the SAME component a catalog
        entry's `provider` field references, so an ID `GET /v1/models` lists
        cannot drift from the ids this route accepts. Its `pattern` is a
        CONTRACT statement, not enforcement: comfy-api installs no OpenAPI
        request validator and oapi-codegen binds path parameters as plain
        strings, so the handler must re-validate this segment itself before
        using it to select a provider or compose an upstream URL.
      in: path
      name: provider
      required: true
      schema:
        $ref: '#/components/schemas/RouterProviderSegment'
    RouterModel:
      description: >-
        Lowercase model segment of the canonical
        `{provider}/{model}[/{variant}]` model ID - the model to run within that
        provider.

        As with `provider`, the schema is the shared `RouterModelSegment`
        component and its `pattern` documents the contract rather than enforcing
        it - see `RouterProvider`.
      in: path
      name: model
      required: true
      schema:
        $ref: '#/components/schemas/RouterModelSegment'
  schemas:
    RouterModelInputSchemaDocument:
      additionalProperties: true
      description: >-
        A standalone OpenAPI document describing ONE Comfy Router model's input
        - the body `POST /v1/models/{provider}/{model}` accepts for that model.
        It is what `GET /v1/models/{provider}/{model}/openapi.json` returns.

        It is a full OpenAPI document rather than a bare JSON Schema because
        that is what the tools this endpoint exists for consume: an SDK
        generator takes an OpenAPI document, and fal's per-model schema endpoint
        - the surface this one mirrors - returns one too. The document is
        STANDALONE: every schema component its request body references travels
        with it under its own `components.schemas`, so nothing in it points at a
        section the caller does not have.

        The shape is left open here on purpose. Its concrete contents are an
        OpenAPI document, and restating the OpenAPI meta-schema inside this spec
        would be a second copy of a specification Comfy does not own - the exact
        publish-versus-enforce drift this endpoint exists to prevent, one level
        up. It is a named component (never an inline anonymous object) because
        ComfyUI's spec-driven codegen needs a class to generate.

        Two Comfy extensions are carried at the document root and are part of
        this contract. `x-comfy-router-model-id` repeats the canonical model ID,
        so a document saved to disk still names the model it describes.
        `x-comfy-input-schema-authored` is a boolean reporting whether the
        embedded schema was authored for this model (`true`) or is the
        permissive fallback served until one is (`false`); the two are
        indistinguishable from the schema alone, and a caller that cannot tell
        them apart would read "any JSON object" as a narrowed contract.
      type: object
    RouterProviderSegment:
      description: >-
        Lowercase `provider` segment of the canonical
        `{provider}/{model}[/{variant}]` model ID - the partner whose model is
        being addressed. The invocation route's `provider` path parameter and a
        catalog entry's `provider` field both reference this one schema, which
        is what keeps the listed IDs and the accepted IDs from drifting apart.

        The `pattern` is a CONTRACT statement, not enforcement: comfy-api
        installs no OpenAPI request validator and oapi-codegen binds path
        parameters as plain strings, so a handler must re-validate the segment
        itself before using it to select a provider or compose an upstream URL.
        The alphabet deliberately admits no `/`, no percent-encoding, and no
        repeated separator, so no accepted value can contain a `.` or `..` path
        segment.
      example: fal-ai
      maxLength: 64
      pattern: ^[a-z0-9]+([._-][a-z0-9]+)*$
      type: string
    RouterModelSegment:
      description: >-
        Lowercase `model` segment of the canonical
        `{provider}/{model}[/{variant}]` model ID - the model to run within that
        provider. Shared by the invocation route's `model` path parameter and a
        catalog entry's `model` field, for the same no-drift reason as
        `RouterProviderSegment`.

        As with the provider segment, the `pattern` documents the contract and
        does not enforce it. Dots are permitted inside the segment because
        partner model IDs use them for versions (`flux-1.1-pro`), but a repeated
        separator is not, so `..` cannot appear.
      example: flux-pro
      maxLength: 128
      pattern: ^[a-z0-9]+([._-][a-z0-9]+)*$
      type: string
    RouterErrorResponse:
      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 FastAPI `detail[]`
        array into this `detail` string would destroy the per-field granularity
        an SDK branches on.
      properties:
        detail:
          description: >-
            Human-readable description of the failure, safe to surface to an end
            user. Not machine-parsed - branch on `error_type` instead.
          type: string
        error_type:
          $ref: '#/components/schemas/RouterErrorType'
      required:
        - detail
        - error_type
      type: object
    RouterErrorType:
      description: >-
        Coarse, machine-readable bucket for a Router failure, mirrored on the
        `X-Comfy-Error-Type` response header so a caller can branch without
        parsing the body. The set is closed at fifteen values: the six
        request-level buckets `invalid_input`, `content_policy_violation`,
        `provider_error`, `provider_timeout`, `insufficient_credits` and
        `model_not_found`, plus the transport-level `unauthorized`, `forbidden`,
        `concurrency_limit_exceeded`, `client_disconnected`, `internal_error`,
        `deadline_exceeded`, `not_enabled`, `service_unavailable` and
        `rate_limited`.

        RETRY SEMANTICS. `not_enabled` is TERMINAL - the same request will be
        refused the same way, so a client must not retry it - while
        `service_unavailable` is TRANSIENT and SHOULD be retried with backoff.
        That opposition is the whole reason they are two buckets rather than
        one: a retry policy keys off the status/`error_type` pair, and a single
        shared bucket would make retry wrong in one direction or the other.

        `not_enabled` and `forbidden` both return `403` and, like the `504` pair
        below, the difference between them is the one the status cannot carry.
        `forbidden` means the credential is valid but is not entitled to this
        model or operation - a decision about the caller. `not_enabled` means
        Comfy Router is not switched on for the caller yet - a state of the
        product rollout, not a judgement about them, and one that changes
        without the caller doing anything. `404` would have claimed the model
        does not exist when it does, and a `5xx` would have blamed the server
        for a deliberate state, so neither was available as a way to separate
        the two by status instead.

        HOW OFTEN `not_enabled` IS SEEN CHANGES OVER TIME, AND IT NEVER STOPS
        BEING VALID. While Comfy Router is rolling out, most callers are not on
        the ramp yet and this is the ordinary answer for them. Once Router is
        fully rolled out it becomes rare. It does not become impossible: the
        deploy-time switch that turns Router off for an environment is a
        permanent operational lever, so `not_enabled` is still the correct
        answer the day it is pulled. It is therefore a value Comfy expects to
        STOP EMITTING often rather than one that is ever withdrawn - a published
        bucket is never removed, because removing it would delete the generated
        exception class an SDK built from it and break any client branching on
        it. `service_unavailable` has no such arc at all: a dependency can
        always fault.

        `deadline_exceeded` and `provider_timeout` both return `504` and the
        difference between them is which SIDE ran out of time, which is not
        cosmetic: `provider_timeout` means the upstream model provider ran out
        of time, while `deadline_exceeded` means COMFY stopped holding the
        connection at its own configured bound. A client that collapses the two
        onto the status alone loses that distinction, and with it the difference
        between "the partner is failing" and "the call is longer than the
        connection Comfy will hold".

        NEITHER BUCKET IS A STATEMENT ABOUT THE CHARGE. Comfy's charges settle
        on COMPLETION: a generation the provider completed is billed whether or
        not the caller was still connected to receive the response, and a
        generation that failed or never completed is not billed. Reaching
        `deadline_exceeded` or `client_disconnected` therefore does not tell a
        caller they were not charged.

        It is deliberately a plain string rather than an `enum`: the set is
        expected to grow -- it already has, and `file_download_error`,
        `cancelled` and `queue_timeout` are named as further additions -- and a
        generated client that hard-rejects an unrecognized bucket would fail
        hardest exactly when something has already gone wrong. Treat an unknown
        value as `internal_error`. Bucketing loses no granularity - the specific
        provider-level reason survives in `RouterValidationErrorDetail.type` and
        its `ctx`.
      example: invalid_input
      type: string
      x-comfy-error-types:
        - meaning: >-
            The request was rejected before it reached the model - a malformed
            body, a malformed or expired pagination cursor, or an input the
            model's own schema does not accept.
          tier: request
          value: invalid_input
        - meaning: >-
            The provider refused the request on content-policy grounds. The
            refusal is deterministic: re-sending the same input will be refused
            again.
          tier: request
          value: content_policy_violation
        - meaning: >-
            The partner provider reported a failure of its own, or returned a
            response Router could not interpret as a result.
          tier: request
          value: provider_error
        - 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 share `504` and
            are separated because they name different causes: this one says the
            partner failed, that one says Comfy stopped holding the connection.
          tier: request
          value: provider_timeout
        - meaning: The calling workspace does not have enough credits to run the model.
          tier: request
          value: insufficient_credits
        - meaning: >-
            The `{provider}/{model}` ID names no model Router can run; an
            unknown provider lands here too. `detail` carries up to three
            suggestions drawn from the models the caller is entitled to see.
          tier: request
          value: model_not_found
        - meaning: The request carried no usable credential.
          tier: transport
          value: unauthorized
        - meaning: >-
            The credential is valid but is not entitled to this model or this
            operation.
          tier: transport
          value: forbidden
        - meaning: >-
            The workspace already has as many calls in flight as it is allowed;
            retry once one of them finishes.
          tier: transport
          value: concurrency_limit_exceeded
        - 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.
          tier: transport
          value: client_disconnected
        - 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.
          tier: transport
          value: internal_error
        - meaning: >-
            Comfy stopped holding the connection at its own configured bound
            before an answer arrived. It shares `504` with `provider_timeout`
            and 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 SAME `Idempotency-Key`:
            when the provider had already accepted the generation, the retry
            collects that generation rather than dispatching another, and a
            `Retry-After` on the `504` says when to ask.
          tier: transport
          value: deadline_exceeded
        - 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 shares `403` with `forbidden` and is NOT the
            same thing, because `forbidden` is 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.
          tier: transport
          value: not_enabled
        - 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
            from `internal_error` - which is a `500` and means Router itself
            failed - so a client can tell "come back shortly" from "this call is
            not going to work".
          tier: transport
          value: service_unavailable
        - meaning: >-
            The caller has spent an allowance measured over a WINDOW and must
            wait for that window to roll. It shares `429` with
            `concurrency_limit_exceeded` and 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. `detail` names the window.
          tier: transport
          value: rate_limited
  headers:
    RouterSchemaCacheControlHeader:
      description: >-
        Freshness directives for the served schema document. `private` because
        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-revalidate` so a stale copy is revalidated against the
        `ETag` rather than served on.
      schema:
        example: private, max-age=300, must-revalidate
        type: string
    RouterSchemaETagHeader:
      description: >-
        Strong entity tag over the served document's bytes, for `GET
        /v1/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 as `If-None-Match` to get a `304` instead of the
        document.

        It is STRONG (no `W/` prefix) and it is a digest of the exact bytes
        served, so two processes serving the same schema issue the same tag - a
        tag that changed on restart would make the cache useless.
        `If-None-Match` is compared by the weak-comparison rule RFC 9110
        mandates, so a cache that stored the tag weakly still matches.
      required: true
      schema:
        example: '"6b8c1f2e0a9d4c3b5e7f8a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f"'
        type: string
    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.

        It is minted by the server and is never read from a request header of
        the same name: a caller-controlled id would let two unrelated calls
        collide in the audit trail, which would make the join actively
        misleading rather than merely absent. Sending this header on a request
        has no effect.
      required: true
      schema:
        example: 6f1a1a6e-6a53-4a5f-9d3a-2b3b0a1f9c21
        format: uuid
        type: string
    RouterErrorTypeHeader:
      description: >-
        Coarse, machine-readable bucket for the failure, set by Router on every
        error response. It carries the same value as
        `RouterErrorResponse.error_type`, and on the `422` it is the ONLY
        machine-readable bucket, because that body is the fal/FastAPI `detail[]`
        shape and has no `error_type` field 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'
  responses:
    RouterRequestError:
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/RouterErrorResponse'
      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
        `RouterErrorResponse` and the bucket is repeated on
        `X-Comfy-Error-Type`.
      headers:
        X-Comfy-Error-Type:
          $ref: '#/components/headers/RouterErrorTypeHeader'
        X-Comfy-Request-Id:
          $ref: '#/components/headers/RouterRequestIdHeader'
  securitySchemes:
    BearerAuth:
      bearerFormat: JWT
      description: |
        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).
      scheme: bearer
      type: http

````