> ## 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.

# Run a partner model synchronously by canonical model ID.

> Comfy Router's canonical, model-ID-addressed entry point. The request body is the partner model's OWN native JSON input and the success response is that model's OWN native JSON output: Router forwards both unchanged instead of imposing a Comfy-shaped envelope, so a caller can move between the partner's API and Router by changing the host. This is the SYNCHRONOUS path, mirroring `POST https://fal.run/{id}` - the response carries the finished result. A queued counterpart, `/v1/queue/models/{provider}/{model}`, is planned and would put fal's `fal.run` / `queue.fal.run` split onto a single host; it is not part of this contract yet.
The path addresses a model by the canonical `{provider}/{model}[/{variant}]` model ID. `provider` and `model` are its first two segments and are lowercase; how the optional `variant` segment is addressed is not settled by this contract.
This operation is deliberately tagged `Comfy Router` and NOT `API Nodes`. The `API Nodes` tag drives ComfyUI's Partner-Node Pydantic codegen, so reusing it here would couple the Router contract to that generator.



## OpenAPI

````yaml https://api.comfy.org/openapi post /v1/models/{provider}/{model}
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}:
    post:
      tags:
        - Comfy Router
      summary: Run a partner model synchronously by canonical model ID.
      description: >-
        Comfy Router's canonical, model-ID-addressed entry point. The request
        body is the partner model's OWN native JSON input and the success
        response is that model's OWN native JSON output: Router forwards both
        unchanged instead of imposing a Comfy-shaped envelope, so a caller can
        move between the partner's API and Router by changing the host. This is
        the SYNCHRONOUS path, mirroring `POST https://fal.run/{id}` - the
        response carries the finished result. A queued counterpart,
        `/v1/queue/models/{provider}/{model}`, is planned and would put fal's
        `fal.run` / `queue.fal.run` split onto a single host; it is not part of
        this contract yet.

        The path addresses a model by the canonical
        `{provider}/{model}[/{variant}]` model ID. `provider` and `model` are
        its first two segments and are lowercase; how the optional `variant`
        segment is addressed is not settled by this contract.

        This operation is deliberately tagged `Comfy Router` and NOT `API
        Nodes`. The `API Nodes` tag drives ComfyUI's Partner-Node Pydantic
        codegen, so reusing it here would couple the Router contract to that
        generator.
      operationId: RunRouterModel
      parameters:
        - $ref: '#/components/parameters/RouterProvider'
        - $ref: '#/components/parameters/RouterModel'
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RouterModelInput'
        description: >-
          The partner model's native JSON input, forwarded to the provider
          unchanged.
        required: true
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RouterModelOutput'
          description: OK - the partner model's native JSON output, returned unchanged.
          headers:
            X-Comfy-Request-Id:
              $ref: '#/components/headers/RouterRequestIdHeader'
        '403':
          $ref: '#/components/responses/RouterRequestError'
        '404':
          $ref: '#/components/responses/RouterRequestError'
        '422':
          $ref: '#/components/responses/RouterModelValidationError'
        '503':
          $ref: '#/components/responses/RouterRequestError'
        '504':
          $ref: '#/components/responses/RouterDeadlineExceeded'
      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:
    RouterModelInput:
      additionalProperties: true
      description: >-
        A partner model's native JSON input document, forwarded to the provider
        as-is. Its concrete shape is owned by the partner rather than by Comfy,
        so this is an open object: Router does not narrow, rename, or
        re-envelope the fields. It is a named component (never an inline
        anonymous object) because ComfyUI's spec-driven codegen needs a class to
        generate.

        Declaring an object here is a codegen requirement, not a licence to
        re-decode the payload: numeric fidelity is the handler's job, since
        decoding into a Go `map[string]any` rounds integers above 2^53 (partner
        seeds and IDs) to `float64`. Forward the raw bytes rather than
        round-tripping them through a generic map. Body size is likewise bounded
        at the handler - comfy-api caps comparable bodies with
        `http.MaxBytesReader` - not by this schema.
      type: object
    RouterModelOutput:
      additionalProperties: true
      description: >-
        A partner model's native JSON output document, returned to the caller
        as-is. Its concrete shape is owned by the partner rather than by Comfy,
        so this is an open object: Router does not narrow, rename, or
        re-envelope the fields. It is a named component (never an inline
        anonymous object) because ComfyUI's spec-driven codegen needs a class to
        generate.

        Declaring an object here is a codegen requirement, not a licence to
        re-decode the payload: numeric fidelity is the handler's job, since
        decoding into a Go `map[string]any` rounds integers above 2^53 (partner
        seeds and IDs) to `float64`. Forward the raw bytes rather than
        round-tripping them through a generic map. Body size is likewise bounded
        at the handler - comfy-api caps comparable bodies with
        `http.MaxBytesReader` - not by this schema.
      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
    RouterValidationErrorResponse:
      description: >-
        Router's model-level `422` body, in the fal/FastAPI form: the request
        was well-formed enough to reach the model and the model rejected its
        contents. Note it carries no `error_type` of its own - that is what
        `X-Comfy-Error-Type` on the response is for, so a client can read the
        coarse bucket off the header without first deciding which of the two
        Router error bodies it received.
      properties:
        detail:
          description: >-
            Every validation failure found on the request, one entry per
            offending field.
          items:
            $ref: '#/components/schemas/RouterValidationErrorDetail'
          type: array
      required:
        - detail
      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
    RouterValidationErrorDetail:
      description: >-
        One model-level validation failure, in the fal/FastAPI form. `type`
        carries the SPECIFIC provider reason - `value_error`, `missing`,
        `image_too_small`, `unsupported_audio_format`, `greater_than`,
        `file_too_large` and the rest - which is the granularity
        `RouterErrorType`'s coarse bucket cannot express. It is an open string
        and not an `enum` for the same reason: the provider vocabulary runs to
        roughly 48 values across two tiers and grows on the provider's release
        cycle, not ours, and an unmodelled value must reach the caller rather
        than fail deserialization.
      properties:
        ctx:
          $ref: '#/components/schemas/RouterValidationErrorContext'
        input:
          $ref: '#/components/schemas/RouterValidationErrorInput'
        loc:
          description: >-
            Path to the offending field, outermost segment first - for example
            `["body", "image_url"]`, or `["body", "images", 0]` where an integer
            indexes into an array.
          items:
            anyOf:
              - type: string
              - type: integer
          type: array
        msg:
          description: Human-readable description of this single failure.
          type: string
        type:
          description: >-
            Specific, machine-readable reason for this failure, passed through
            from the provider unchanged. This is the value a typed SDK exception
            hierarchy branches on; `error_type` on the response header is only
            its coarse bucket.
          example: image_too_small
          type: string
      required:
        - loc
        - msg
        - type
      type: object
    RouterValidationErrorContext:
      additionalProperties: true
      description: >-
        The violated bound for one `RouterValidationErrorDetail`, carried from
        the provider verbatim - for example `{"limit_value": 8}` alongside
        `greater_than`, `{"min_width": 512}` alongside `image_too_small`, or
        `{"max_size_bytes": 10485760}` alongside `file_too_large`. The key set
        is specific to the provider and the error type, so this is deliberately
        an open object: narrowing it to a fixed field list, or folding it into
        the `msg` string, is precisely how a ported integration compiles and
        then silently loses the branch that read the bound. Absent when the
        error type carries no bound.
      type: object
    RouterValidationErrorInput:
      description: >-
        The offending input value, echoed back verbatim so a caller can see what
        was rejected without re-deriving it from `loc`. Any JSON type - string,
        number, boolean, array, object or null - so this schema is deliberately
        left untyped rather than narrowed to an object. Absent when the provider
        does not echo the input back.
  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.

        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'
    RouterRetryAfterHeader:
      description: >-
        Seconds to wait before retrying the SAME request with the SAME
        `Idempotency-Key`. Present on a `deadline_exceeded` `504` only when
        Comfy holds a handle to a generation the provider is still running; the
        value is Router's own poll interval, which is the one honest number this
        route has for "ask again later". Absent when there is nothing to
        collect: an unkeyed call, or a bound that expired before the provider
        accepted anything.
      schema:
        example: 2
        minimum: 1
        type: integer
  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'
    RouterModelValidationError:
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/RouterValidationErrorResponse'
      description: >-
        The request reached the model and the model rejected its contents. The
        body is `RouterValidationErrorResponse`, the fal/FastAPI `detail[]`
        shape, so each offending field keeps its own specific `type` and `ctx`.
        `X-Comfy-Error-Type` carries the coarse bucket for the whole response.
      headers:
        X-Comfy-Error-Type:
          $ref: '#/components/headers/RouterErrorTypeHeader'
        X-Comfy-Request-Id:
          $ref: '#/components/headers/RouterRequestIdHeader'
    RouterDeadlineExceeded:
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/RouterErrorResponse'
      description: >-
        Comfy stopped holding the connection at its own configured bound
        (`deadline_exceeded`). The body and the two headers are exactly
        `RouterRequestError`'s; what this adds is the optional `Retry-After`,
        present when a retry with the same `Idempotency-Key` will collect the
        generation that is still running rather than dispatch a new one. See the
        `504` on `POST /v1/models/{provider}/{model}`.
      headers:
        Retry-After:
          $ref: '#/components/headers/RouterRetryAfterHeader'
        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

````