From 3a8fea1fc6ad590520dccd82784f9dcdf78e046f Mon Sep 17 00:00:00 2001 From: mattmillerai <7741082+mattmillerai@users.noreply.github.com> Date: Fri, 18 Sep 2026 01:05:27 +0000 Subject: [PATCH] chore: sync vendored Comfy Router spec from cloud@8759533 --- spec/router-openapi.yaml | 430 +++++++++++++++++++++++++++++++++++---- 1 file changed, 395 insertions(+), 35 deletions(-) diff --git a/spec/router-openapi.yaml b/spec/router-openapi.yaml index dc66cf9..5fb0cb7 100644 --- a/spec/router-openapi.yaml +++ b/spec/router-openapi.yaml @@ -18,7 +18,7 @@ paths: /v2/models: get: summary: List the models Comfy Router can run. - description: 'Comfy Router''s model catalog - one page of the canonical model IDs that `POST /v2/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 `/`.' + description: 'Comfy Router''s model catalog - one page of the canonical model IDs that `POST /v2/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 `/`.' operationId: listRouterModels tags: - Comfy Router @@ -79,7 +79,7 @@ paths: $ref: '#/components/responses/RouterRequestError' post: 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: the response carries the finished result.' + 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: the response carries the finished result.' operationId: runRouterModel tags: - Comfy Router @@ -90,21 +90,28 @@ paths: - $ref: '#/components/parameters/RouterProvider' - $ref: '#/components/parameters/RouterModel' - $ref: '#/components/parameters/RouterIdempotencyKey' + - $ref: '#/components/parameters/ModelProvider' + - $ref: '#/components/parameters/StrictMode' + - $ref: '#/components/parameters/FallbackProvider' requestBody: required: true - description: The partner model's native JSON input, forwarded to the provider unchanged. + description: The partner model's native JSON input. Without `model_provider`, or with `strict_mode=true`, forwarded to the provider unchanged - under `strict_mode=true` the body must already be the alternate provider's own real schema, not this model's native one (see `strict_mode`). With `model_provider` selecting an alternate provider and `strict_mode=false` (the default), the body is translated into that provider's real schema before it is sent - any native field that cannot be expressed exactly is dropped and disclosed via the response's `X-Comfy-Router-Dropped-Params` header, never silently. content: application/json: schema: $ref: '#/components/schemas/RouterModelInput' responses: '200': - description: 'OK - the partner model''s native output, returned unchanged, under the partner''s OWN media type. For most models that is JSON (`RouterModelOutput`); for a model whose partner answers a generation directly as bytes - the ElevenLabs audio models are the first in the catalog - it is those bytes, and the response carries the partner''s own `Content-Type` (`audio/mpeg`, `audio/wav`, ...) rather than `application/json`. A client MUST branch on the response `Content-Type` and must not assume a JSON document; the per-model contract is published at `GET /v2/models/{provider}/{model}/openapi.json`. This response carries `X-Content-Type-Options: nosniff`, so a partner media type is taken at its word and never sniffed into something else. When this response was replayed from the record held against an `Idempotency-Key` rather than produced by running the model again, it carries `Idempotent-Replayed: true` and is not charged a second time.' + description: 'OK - without `model_provider`, or with `model_provider` and `strict_mode=false` (the default, translated back into this model''s native contract when possible, falling back to the alternate provider''s own raw response on a translation failure - logged, never silent), the shape is this model''s own native output; with `strict_mode=true` it is the alternate provider''s response returned unchanged. For most models that is JSON (`RouterModelOutput`); for a model whose partner answers a generation directly as bytes - the ElevenLabs audio models are the first in the catalog - it is those bytes, and the response carries the partner''s own `Content-Type` (`audio/mpeg`, `audio/wav`, ...) rather than `application/json`. A client must branch on the response `Content-Type` and must not assume a JSON document; the per-model contract is published at `GET /v2/models/{provider}/{model}/openapi.json`. This response carries `X-Content-Type-Options: nosniff`, so a partner media type is taken at its word and never sniffed into something else. When this response was replayed from the record held against an `Idempotency-Key` rather than produced by running the model again, it carries `Idempotent-Replayed: true` and is not charged a second time.' headers: X-Comfy-Request-Id: $ref: '#/components/headers/RouterRequestIdHeader' X-Content-Type-Options: $ref: '#/components/headers/RouterNoSniffHeader' + X-Comfy-Router-Fallback-Provider: + $ref: '#/components/headers/RouterFallbackProviderHeader' + X-Comfy-Router-Dropped-Params: + $ref: '#/components/headers/RouterDroppedParamsHeader' Idempotent-Replayed: $ref: '#/components/headers/RouterIdempotentReplayedHeader' X-Committed-Spend-Limit: @@ -146,7 +153,7 @@ paths: /v2/models/{provider}/{model}/openapi.json: get: 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. + 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 tags: - Comfy Router @@ -164,7 +171,7 @@ paths: type: string responses: '200': - description: OK - the model's input AND output schemas, as a standalone OpenAPI document. + description: OK - the model's input and output schemas, as a standalone OpenAPI document. headers: X-Comfy-Request-Id: $ref: '#/components/headers/RouterRequestIdHeader' @@ -195,11 +202,204 @@ paths: $ref: '#/components/responses/RouterRequestError' '503': $ref: '#/components/responses/RouterRequestError' + /v2/models/{provider}/{model}/requests: + post: + summary: Submit a partner model run to the queue and return immediately. + description: Comfy Router's queued delivery mode. The request body is the same partner-native JSON input `POST /v2/models/{provider}/{model}` accepts for this model - one body shape, one per-model schema, two delivery modes - but this route does not hold the connection for the result. It admits the run, answers `201` with a handle, and the caller collects the result later through the three reads below. + operationId: submitRouterModelRequest + tags: + - Comfy Router + security: + - BearerAuth: [] + - ApiKeyAuth: [] + parameters: + - $ref: '#/components/parameters/RouterProvider' + - $ref: '#/components/parameters/RouterModel' + - $ref: '#/components/parameters/RouterIdempotencyKey' + requestBody: + required: true + description: The partner model's native JSON input, identical to the body the synchronous route accepts for this model. Validated against the model's own input schema before the run is admitted, so a body the model would reject is a `422` here rather than a queued request that fails minutes later. + content: + application/json: + schema: + $ref: '#/components/schemas/RouterModelInput' + responses: + '201': + description: 'Created - the run was admitted to the queue. The body is the handle: the `request_id`, `status: IN_QUEUE`, a `queue_position` snapshot, and the `status_url`, `response_url` and `cancel_url` that address the rest of this request''s lifetime. It is deliberately `201` and not `202`: a queued request is a resource this call created and the three URLs address it, whereas the `202` on the result read below means "not ready yet, ask again" and creates nothing.' + headers: + X-Comfy-Request-Id: + $ref: '#/components/headers/RouterRequestIdHeader' + Idempotent-Replayed: + $ref: '#/components/headers/RouterIdempotentReplayedHeader' + content: + application/json: + schema: + $ref: '#/components/schemas/RouterQueueSubmitResponse' + '401': + $ref: '#/components/responses/RouterRequestError' + '400': + $ref: '#/components/responses/RouterRequestError' + '413': + $ref: '#/components/responses/RouterRequestError' + '402': + $ref: '#/components/responses/RouterRequestError' + '403': + $ref: '#/components/responses/RouterRequestError' + '404': + $ref: '#/components/responses/RouterRequestError' + '409': + $ref: '#/components/responses/RouterIdempotencyConflict' + '422': + $ref: '#/components/responses/RouterModelValidationError' + '503': + $ref: '#/components/responses/RouterRequestError' + /v2/models/{provider}/{model}/requests/{request_id}: + get: + summary: Collect the result of one submitted request. + description: The collect endpoint. On a request that has finished successfully it returns the partner model's own native output, byte for byte what the synchronous route's `200` carries for the same model and the same input - so the two delivery modes produce one result shape and a caller can move between them without a second parser. + operationId: getRouterModelRequestResult + tags: + - Comfy Router + security: + - BearerAuth: [] + - ApiKeyAuth: [] + parameters: + - $ref: '#/components/parameters/RouterProvider' + - $ref: '#/components/parameters/RouterModel' + - $ref: '#/components/parameters/RouterQueueRequestId' + responses: + '200': + description: 'OK - the partner model''s native output for a request that produced one - a request that completed successfully, or a terminal one that carries both a recorded charge and a stored result - returned unchanged under the partner''s own media type, exactly as the synchronous route''s `200` returns it. For most models that is JSON (`RouterModelOutput`); for a model whose partner answers a generation directly as bytes it is those bytes under the partner''s own `Content-Type`. A client must branch on the response `Content-Type` and must not assume a JSON document; the per-model contract is published at `GET /v2/models/{provider}/{model}/openapi.json`. This response carries `X-Content-Type-Options: nosniff`, so a partner media type is taken at its word and never sniffed into something else.' + headers: + X-Comfy-Request-Id: + $ref: '#/components/headers/RouterRequestIdHeader' + X-Content-Type-Options: + $ref: '#/components/headers/RouterNoSniffHeader' + content: + application/json: + schema: + $ref: '#/components/schemas/RouterModelOutput' + '*/*': + schema: + type: string + format: binary + '202': + description: Accepted - the request has not finished. The body is the same `RouterQueueStatusResponse` the status read returns, so this route can be polled on its own, and `Retry-After` hints when to ask again. + headers: + X-Comfy-Request-Id: + $ref: '#/components/headers/RouterRequestIdHeader' + Retry-After: + $ref: '#/components/headers/RouterQueuePollAfterHeader' + content: + application/json: + schema: + $ref: '#/components/schemas/RouterQueueStatusResponse' + '401': + $ref: '#/components/responses/RouterRequestError' + '403': + $ref: '#/components/responses/RouterRequestError' + '404': + $ref: '#/components/responses/RouterRequestError' + '410': + $ref: '#/components/responses/RouterRequestError' + '503': + $ref: '#/components/responses/RouterRequestError' + '409': + $ref: '#/components/responses/RouterRequestError' + '504': + $ref: '#/components/responses/RouterRequestError' + '422': + $ref: '#/components/responses/RouterModelValidationError' + default: + $ref: '#/components/responses/RouterRequestError' + /v2/models/{provider}/{model}/requests/{request_id}/cancel: + put: + summary: Ask for one submitted request to be cancelled. + description: 'Asks Comfy to stop a request that has not finished. It is a request, not a guarantee, and the `202` says exactly that: `CANCELLATION_REQUESTED` means the ask was accepted, not that the run has stopped. A run already on the wire at a partner may complete anyway - and a partner generation that completes is charged, whether or not anyone collected it - so a caller who needs to know what actually happened reads the status endpoint afterwards, where a cancellation that took effect is `COMPLETED` carrying an `error_type` like every other terminal outcome.' + operationId: cancelRouterModelRequest + tags: + - Comfy Router + security: + - BearerAuth: [] + - ApiKeyAuth: [] + parameters: + - $ref: '#/components/parameters/RouterProvider' + - $ref: '#/components/parameters/RouterModel' + - $ref: '#/components/parameters/RouterQueueRequestId' + responses: + '202': + description: Accepted - `CANCELLATION_REQUESTED`. The ask was accepted for a request that had not yet reached a terminal state. It is not a statement that the run has stopped; read the status endpoint to learn what it did. + headers: + X-Comfy-Request-Id: + $ref: '#/components/headers/RouterRequestIdHeader' + content: + application/json: + schema: + $ref: '#/components/schemas/RouterQueueCancelResponse' + '409': + description: 'Conflict - `ALREADY_COMPLETED`. The request had already reached a terminal state, so there was nothing to cancel. It is terminal for the ask: retrying it will return the same answer. Whether that terminal state was a success, a failure or an earlier cancellation is not carried here - the status endpoint answers that.' + headers: + X-Comfy-Request-Id: + $ref: '#/components/headers/RouterRequestIdHeader' + content: + application/json: + schema: + $ref: '#/components/schemas/RouterQueueCancelResponse' + '400': + $ref: '#/components/responses/RouterRequestError' + '401': + $ref: '#/components/responses/RouterRequestError' + '403': + $ref: '#/components/responses/RouterRequestError' + '404': + $ref: '#/components/responses/RouterRequestError' + '503': + $ref: '#/components/responses/RouterRequestError' + default: + $ref: '#/components/responses/RouterRequestError' + /v2/models/{provider}/{model}/requests/{request_id}/status: + get: + summary: Read the queue state of one submitted request. + description: The poll endpoint. It answers with the request's current state and never with the result, so a client can watch a long generation without transferring its output on every poll - the result is collected once, from the read below, when this says `COMPLETED`. + operationId: getRouterModelRequestStatus + tags: + - Comfy Router + security: + - BearerAuth: [] + - ApiKeyAuth: [] + parameters: + - $ref: '#/components/parameters/RouterProvider' + - $ref: '#/components/parameters/RouterModel' + - $ref: '#/components/parameters/RouterQueueRequestId' + responses: + '200': + description: OK - the request's current queue state. `status` is one of the three states; `queue_position` is present while the request is still `IN_QUEUE`; `error_type` is present only on a `COMPLETED` request that failed or was cancelled. + headers: + X-Comfy-Request-Id: + $ref: '#/components/headers/RouterRequestIdHeader' + Retry-After: + $ref: '#/components/headers/RouterQueuePollAfterHeader' + content: + application/json: + schema: + $ref: '#/components/schemas/RouterQueueStatusResponse' + '401': + $ref: '#/components/responses/RouterRequestError' + '403': + $ref: '#/components/responses/RouterRequestError' + '404': + $ref: '#/components/responses/RouterRequestError' + '410': + $ref: '#/components/responses/RouterRequestError' + '503': + $ref: '#/components/responses/RouterRequestError' + default: + $ref: '#/components/responses/RouterRequestError' components: schemas: RouterChargesOnPolicyRejection: type: string - description: Whether a call this model REFUSES on content-policy grounds is nevertheless charged to the caller. Providers differ, the difference is invisible at call time, and a user who sees an error and a charge for the same call has no way to have known - so it is stated per model, before the call, rather than left to per-provider folklore. + description: Whether a call this model refuses on content-policy grounds is nevertheless charged to the caller. Providers differ, the difference is invisible at call time, and a user who sees an error and a charge for the same call has no way to have known - so it is stated per model, before the call, rather than left to per-provider folklore. example: unknown RouterErrorResponse: type: object @@ -220,7 +420,7 @@ components: 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-Key` that 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 with `409` in the key cases and with `400`/`422` in 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. + 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-Key` that 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 with `409` in the key cases and with `400`/`422` in 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.' @@ -229,13 +429,13 @@ components: 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 share `504` and are separated because they name different causes: this one says the partner failed, that one says Comfy stopped holding the connection.' + 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.' - 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 lands here too. `detail` carries up to three suggestions drawn from the models the caller is entitled to see. + 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. This includes a catalogued ID the provider does not currently serve for Comfy; the response then carries no suggestions. - value: unauthorized tier: transport meaning: The request carried no usable credential. @@ -244,28 +444,37 @@ components: 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 `409` rather than the `429` above: another call is already in flight for the `Idempotency-Key` this request presented. Re-send the SAME key after `Retry-After` seconds to collect that call''s result.' + 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 `409` rather than the `429` above: another call is already in flight for the `Idempotency-Key` this request presented. Re-send the same key after `Retry-After` seconds 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. + 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 `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.' + 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.' - 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 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.' + 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.' - 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 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".' - 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 `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.' + 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.' + - 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_QUEUE` was 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 not `client_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 sees `200` with this in the body, because the request ended and so the read succeeded. Collecting the result of such a request answers `409` in 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 not `410`, which on that route means the result aged out of its retention window, and not a `5xx`, 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: a `deadline_exceeded` generation may be running and billable, whereas this one provably never started. It is returned under `504`, which it shares with `provider_timeout` and `deadline_exceeded` because 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 what `error_type` carries. 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_id` names no request of the caller's under this model. It is the second of the two conditions the queued reads' `404` covers; the first is the `{provider}/{model}` ID resolving to no partner model, which is `model_not_found` and 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 is `410`, not this. RouterModelBilling: type: object - description: Per-model billing FACTS a caller needs before invoking - not prices. Usage and cost figures never appear here. + description: Per-model billing facts a caller needs before invoking - not prices. Usage and cost figures never appear here. properties: charges_on_policy_rejection: $ref: '#/components/schemas/RouterChargesOnPolicyRejection' @@ -279,14 +488,14 @@ components: - $ref: '#/components/schemas/RouterModelDetailFields' RouterModelDetailFields: type: object - description: 'The half of `RouterModelDetail` the catalog listing does NOT carry: per-model fields worth one lookup but not worth repeating on every entry of a paginated catalog page.' + description: 'The half of `RouterModelDetail` the catalog listing does not carry: per-model fields worth one lookup but not worth repeating on every entry of a paginated catalog page.' properties: input_schema_url: type: string format: uri pattern: ^https:// maxLength: 2048 - description: 'Pointer to this model''s input schema document - the description of the body `POST /v2/models/{provider}/{model}` accepts for this model. Only the POINTER is part of this contract: the document it addresses is authored separately. Absent when no schema has been authored for the model.' + description: 'Pointer to this model''s input schema document - the description of the body `POST /v2/models/{provider}/{model}` accepts for this model. Only the pointer is part of this contract: the document it addresses is authored separately. Absent when no schema has been authored for the model.' RouterModelId: type: string description: A canonical Comfy Router model ID, `{provider}/{model}` - exactly the value that addresses the model on `POST /v2/models/{provider}/{model}`, so a caller can interpolate it into that path without re-deriving it from anything. Its `pattern` is `RouterProviderSegment` and `RouterModelSegment` joined by a single `/`, and `maxLength` is their sum plus that separator. @@ -299,7 +508,7 @@ components: additionalProperties: true RouterModelInputSchemaDocument: type: object - description: A standalone OpenAPI document describing ONE Comfy Router model's input AND output - the body `POST /v2/models/{provider}/{model}` accepts for that model, under the operation's `requestBody`, and the body it returns, under that operation's `200` content. 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. + description: A standalone OpenAPI document describing one Comfy Router model's input and output - the body `POST /v2/models/{provider}/{model}` accepts for that model, under the operation's `requestBody`, and the body it returns, under that operation's `200` content. 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 RouterModelListEntry: type: object @@ -334,7 +543,7 @@ components: $ref: '#/components/schemas/RouterPageCursor' limit: type: integer - description: 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. + description: 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. minimum: 1 maximum: 100 example: 20 @@ -344,7 +553,7 @@ components: - limit RouterModelOutput: type: object - 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. For the concrete shape ONE model returns, read that model''s own document at `GET /v2/models/{provider}/{model}/openapi.json`, whose `200` carries the per-model output schema when Comfy has described it.' + 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. For the concrete shape one model returns, read that model''s own document at `GET /v2/models/{provider}/{model}/openapi.json`, whose `200` carries the per-model output schema when Comfy has described it.' additionalProperties: true RouterModelSegment: type: string @@ -354,7 +563,7 @@ components: example: flux-2-pro RouterPageCursor: type: string - description: '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.' + description: '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.' pattern: ^[A-Za-z0-9._~+/=-]+$ minLength: 1 maxLength: 512 @@ -365,13 +574,117 @@ components: pattern: ^[a-z0-9]+([._-][a-z0-9]+)*$ maxLength: 64 example: bfl + RouterQueueCancelResponse: + type: object + description: The answer to a cancellation ask on the two statuses that describe a request this route resolved - the `202` and the `400`. One body shape across both rather than a success envelope plus an error envelope, because both are the same statement - what cancelling found - and a client that has to parse a different type per status code gains nothing from the split. + properties: + request_id: + $ref: '#/components/schemas/RouterQueueRequestId' + status: + $ref: '#/components/schemas/RouterQueueCancelStatus' + required: + - request_id + - status + RouterQueueCancelStatus: + type: string + description: What a cancellation ask found, for the two outcomes that describe a request this route actually resolved. Both are mirrored by the HTTP status, so a client may branch on either. + enum: + - CANCELLATION_REQUESTED + - ALREADY_COMPLETED + example: CANCELLATION_REQUESTED + RouterQueuePosition: + type: integer + minimum: 0 + description: How many requests are ahead of this one in the queue, at the instant the response was composed. Zero means this request is at the front. + example: 3 + RouterQueueRequestId: + type: string + format: uuid + x-go-type: string + pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ + maxLength: 36 + description: Identifier of one queued Router request - the handle a caller polls, cancels and collects a result by. + example: 6f1a1a6e-6a53-4a5f-9d3a-2b3b0a1f9c21 + RouterQueueStatus: + type: string + description: The state of a queued Router request. It has exactly three values, and unlike `RouterErrorType` this one is a closed `enum`, because the two schemas are closed in opposite directions on purpose. `RouterErrorType` classifies failures and its set is expected to grow, so a generated client that hard-rejected an unrecognised bucket would fail hardest exactly when something had already gone wrong. This one is a lifecycle, and a lifecycle with a fourth state added later is a breaking change to every polling loop written against it whether it is declared as an enum or not - so it is declared as one, and the constraint is stated where a client can see it. + enum: + - IN_QUEUE + - IN_PROGRESS + - COMPLETED + example: IN_QUEUE + RouterQueueStatusFields: + type: object + description: 'The half of `RouterQueueStatusResponse` that is not the URL block: one queued request''s identity, its current state, and - when that state is terminal and the run did not succeed - the coarse bucket saying why.' + properties: + request_id: + $ref: '#/components/schemas/RouterQueueRequestId' + status: + $ref: '#/components/schemas/RouterQueueStatus' + queue_position: + $ref: '#/components/schemas/RouterQueuePosition' + error_type: + allOf: + - $ref: '#/components/schemas/RouterErrorType' + description: Present only on a `COMPLETED` request that did not succeed, carrying the same coarse bucket the result read puts on `X-Comfy-Error-Type` when it returns that failure. It is what distinguishes a terminal request that succeeded from one that failed or was cancelled - there is no separate terminal status for either - and it is absent on success rather than null, so branch on its presence. + required: + - request_id + - status + RouterQueueStatusResponse: + type: object + description: One queued request's current state, composed with the same three URLs the submission returned. + allOf: + - $ref: '#/components/schemas/RouterQueueUrls' + - $ref: '#/components/schemas/RouterQueueStatusFields' + RouterQueueSubmitFields: + type: object + description: 'The half of `RouterQueueSubmitResponse` that is not the URL block: the new request''s identity and its state at the instant it was admitted.' + properties: + request_id: + $ref: '#/components/schemas/RouterQueueRequestId' + status: + $ref: '#/components/schemas/RouterQueueStatus' + queue_position: + $ref: '#/components/schemas/RouterQueuePosition' + required: + - request_id + - status + RouterQueueSubmitResponse: + type: object + description: 'The handle returned when a run is admitted to the queue: the request''s identity and state, composed with the three URLs that address the rest of its lifetime.' + allOf: + - $ref: '#/components/schemas/RouterQueueUrls' + - $ref: '#/components/schemas/RouterQueueSubmitFields' + RouterQueueUrls: + type: object + description: The three URLs that address the rest of one queued request's lifetime, returned on every response that carries a live handle so a client never composes a queue URL itself. + properties: + status_url: + type: string + format: uri + description: Absolute URL of this request's status read. + example: https://api.comfy.org/v2/models/bfl/flux-pro-1.1/requests/6f1a1a6e-6a53-4a5f-9d3a-2b3b0a1f9c21/status + response_url: + type: string + format: uri + description: Absolute URL this request's result is collected from. + example: https://api.comfy.org/v2/models/bfl/flux-pro-1.1/requests/6f1a1a6e-6a53-4a5f-9d3a-2b3b0a1f9c21 + cancel_url: + type: string + format: uri + description: Absolute URL a cancellation is asked for at. + example: https://api.comfy.org/v2/models/bfl/flux-pro-1.1/requests/6f1a1a6e-6a53-4a5f-9d3a-2b3b0a1f9c21/cancel + required: + - status_url + - response_url + - cancel_url RouterValidationErrorContext: type: object 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.' additionalProperties: true RouterValidationErrorDetail: type: object - description: 'One model-level validation failure, in the 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.' + description: 'One model-level validation failure, in the 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: loc: type: array @@ -427,7 +740,7 @@ components: schema: $ref: '#/components/schemas/RouterErrorResponse' RouterDeadlineExceeded: - 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 /v2/models/{provider}/{model}`. The status is SHARED with `provider_timeout` - the partner not answering in time, rather than Comfy''s own bound expiring - which is why `X-Comfy-Upstream-Status` is declared here too: present, it carries the partner''s own status and the bound that expired was theirs; absent, the bound was Comfy''s.' + 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 /v2/models/{provider}/{model}`. The status is shared with `provider_timeout` - the partner not answering in time, rather than Comfy''s own bound expiring - which is why `X-Comfy-Upstream-Status` is declared here too: present, it carries the partner''s own status and the bound that expired was theirs; absent, the bound was Comfy''s.' headers: X-Comfy-Error-Type: $ref: '#/components/headers/RouterErrorTypeHeader' @@ -442,7 +755,7 @@ components: schema: $ref: '#/components/schemas/RouterErrorResponse' RouterIdempotencyConflict: - description: 'The `Idempotency-Key` on this request is already held, and this request cannot be answered from its record. Two conditions share the status and `X-Comfy-Error-Type` is what separates them, because they are acted on in opposite ways. `concurrency_limit_exceeded` means the original call for this key is still running: wait `Retry-After` seconds and re-send THE SAME key, which collects that call''s result rather than starting a second one. `invalid_input` means the key cannot serve this request at all - it was already used for a different request (the method, the path and query, or the body differ from the original), or the original completed (and, if it succeeded, was charged) and Router holds no copy of its response it can still stand behind - for example it was too large to store, or it names an asset Comfy does not host and so cannot promise still resolves, which on a direct-return model is replayed for a few minutes after the original call and refused after that - or the copy it holds is content-encoded in a way this request did not accept - and the answer is always a NEW key, never a re-send of this one. There is no `Retry-After` on any of these, because waiting changes nothing. `detail` says which case it is; the different-request case says nothing about how the call that does own the key turned out. The body is `RouterErrorResponse` and the bucket is repeated on `X-Comfy-Error-Type`.' + description: 'The `Idempotency-Key` on this request is already held, and this request cannot be answered from its record. Two conditions share the status and `X-Comfy-Error-Type` is what separates them, because they are acted on in opposite ways. `concurrency_limit_exceeded` means the original call for this key is still running: wait `Retry-After` seconds and re-send the same key, which collects that call''s result rather than starting a second one. `invalid_input` means the key cannot serve this request at all - it was already used for a different request (the method, the path and query, or the body differ from the original), or the original completed (and, if it succeeded, was charged) and Router holds no copy of its response it can still stand behind - for example it was too large to store, or it names an asset Comfy does not host and so cannot promise still resolves, which on a direct-return model is replayed for a few minutes after the original call and refused after that - or the copy it holds is content-encoded in a way this request did not accept - and the answer is always a new key, never a re-send of this one. There is no `Retry-After` on any of these, because waiting changes nothing. `detail` says which case it is; the different-request case says nothing about how the call that does own the key turned out. The body is `RouterErrorResponse` and the bucket is repeated on `X-Comfy-Error-Type`.' headers: X-Comfy-Error-Type: $ref: '#/components/headers/RouterErrorTypeHeader' @@ -492,7 +805,7 @@ components: schema: $ref: '#/components/schemas/RouterErrorResponse' RouterRunRequestError: - 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`. On this route the status is ALSO how the partner''s own refusal of a call that really ran is returned - the `content_policy_violation` some models meter - and that answer is recorded against an `Idempotency-Key` and served to a same-key retry, so unlike the catalog reads'' shared error this response can arrive carrying `Idempotent-Replayed: true`.' + 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`. On this route the status is also how the partner''s own refusal of a call that really ran is returned - the `content_policy_violation` some models meter - and that answer is recorded against an `Idempotency-Key` and served to a same-key retry, so unlike the catalog reads'' shared error this response can arrive carrying `Idempotent-Replayed: true`.' headers: X-Comfy-Error-Type: $ref: '#/components/headers/RouterErrorTypeHeader' @@ -507,6 +820,20 @@ components: schema: $ref: '#/components/schemas/RouterErrorResponse' parameters: + FallbackProvider: + name: fallback_provider + in: query + required: false + description: 'Controls whether Router retries this call against the model''s other registered provider when the first attempt fails for a reason attributable to Router''s own side or to the specific provider tried - never for a reason attributable to the request itself (an unretried failure is refused exactly as it always was). Omitted, or any value other than `false`, turns fallback on (the default) and Router uses the one alternate the model has today. `false` turns fallback off: a failure is refused, never retried. A successful fallback response carries the `X-Comfy-Router-Fallback-Provider` header, naming the provider that served it; a fallback attempt that itself also fails does not carry the header, and no case retries a generation that may already have been submitted to a provider.' + schema: + type: string + ModelProvider: + name: model_provider + in: query + required: false + description: 'Selects an alternate provider for this model, instead of its current default. Omitted, or `default`, is byte-for-byte today''s behavior. A value naming a real provider that does not serve this model is refused `404` with `error_type: model_not_found` - the same bucket an unknown model ID itself uses; an unrecognized value (not a real, registered provider at all) is refused `400` with `error_type: invalid_input`. Router''s error_type set is closed (see the Router error contract note above); it carries neither a `provider_not_available` nor a `validation_error` bucket.' + schema: + type: string RouterCatalogCursor: name: cursor in: query @@ -527,7 +854,7 @@ components: name: Idempotency-Key in: header required: false - description: 'Caller-generated key that makes retrying ONE logical call safe. A call that reached the caller with an answer is recorded against its key for 24 hours, and a retry carrying the same key is answered from that record instead of dispatching - and charging - the provider a second time, marked `Idempotent-Replayed: true`. Keys are scoped to the workspace your credential carries, or to your user when it carries none - so the keyspace is SHARED by every member of a workspace rather than private to one caller. Make a key unique across the whole workspace, not just within your own client: a second member who reuses a key string is answered from the first member''s record, or refused `409` if the request differs. Because the scope follows the CREDENTIAL and not the person, a credential that carries no workspace at all scopes to your user id instead - so retrying one logical call under a different credential can land in a different namespace, where it is dispatched and charged again. Retry with the credential you started with. A keyed request with no authenticated caller is refused `401`. The guarantee is a BILLING one: a key is charged at most once. It is not a promise that a key is dispatched at most once, and it does not make a lost call resumable. Some answers are RECORDED but not replayable for the full 24 hours, and the billing guarantee is the half that always holds: the key stays consumed - the retry never re-runs and never re-charges - but it is answered `409 invalid_input` instead of being served the original body. That happens whenever Comfy does not hold a copy of the response it can still stand behind; a response past the replay size cap and a result addressed by an asset URL Comfy does not host are the two you are most likely to meet. The second is the one worth planning for, because it looks like an ordinary success: which models answer with a Comfy-hosted asset link, how long one stays valid, and what a result carries when an individual asset could not be copied are stated in one place, under Result assets in the API reference, and this paragraph does not restate them. On a model that returns its result on the original call, an answer still holding a partner''s own asset link is replayed for a few minutes - which is where a dropped connection puts an SDK''s automatic same-key re-send, and while the partner''s link is certainly still alive - and refused after that rather than replayed dead. So a prompt retry of a partially re-hosted result behaves exactly like any other replay, and only a later one meets the `409`. That short window is deliberately NOT offered on a model that submits and is polled, because there the partner may have minted the URL long before your call collected it and its remaining life is unknowable - and those models do not need it: a call cut off mid-generation keeps its key holding the generation, so the same-key retry collects the ORIGINAL result rather than a recorded copy of it. A response past the size cap has no window either and is refused from the start. The action on any of these `409 invalid_input` refusals is the same: use a new key. Only an answer a provider actually produced is recorded, though. A refusal Router raises on its own BEFORE dispatching anything - not enabled for you yet (`403`), unknown model (`404`), not entitled to the model (`403`), a body the model''s schema rejects or that names a different model than the path (`422`), a malformed request (`400 invalid_input`) - dispatched nothing and charged nothing, so it RELEASES the key: re-send the SAME key once you are on the rollout ramp or have corrected the request and it runs for real, rather than replaying the refusal or colliding with it as a `409`. That turns on whether a provider was reached, NEVER on the status, so a `400 content_policy_violation` - the partner''s own answer to a call that ran, which some models meter - is recorded and replayed like any other answer. Releasing a refusal that dispatched nothing frees nothing chargeable, so it does not weaken the at-most-once billing guarantee above.' + description: 'Caller-generated key that makes retrying one logical call safe. A call that reached the caller with an answer is recorded against its key for 24 hours, and a retry carrying the same key is answered from that record instead of dispatching - and charging - the provider a second time, marked `Idempotent-Replayed: true`. Keys are scoped to the workspace your credential carries, or to your user when it carries none - so the keyspace is shared by every member of a workspace rather than private to one caller. Make a key unique across the whole workspace, not just within your own client: a second member who reuses a key string is answered from the first member''s record, or refused `409` if the request differs. Because the scope follows the credential and not the person, a credential that carries no workspace at all scopes to your user id instead - so retrying one logical call under a different credential can land in a different namespace, where it is dispatched and charged again. Retry with the credential you started with. A keyed request with no authenticated caller is refused `401`. The guarantee is a billing one: a key is charged at most once. It is not a promise that a key is dispatched at most once, and it does not make a lost call resumable. Some answers are recorded but not replayable for the full 24 hours, and the billing guarantee is the half that always holds: the key stays consumed - the retry never re-runs and never re-charges - but it is answered `409 invalid_input` instead of being served the original body. That happens whenever Comfy does not hold a copy of the response it can still stand behind; a response past the replay size cap and a result addressed by an asset URL Comfy does not host are the two you are most likely to meet. The second is the one worth planning for, because it looks like an ordinary success: which models answer with a Comfy-hosted asset link, how long one stays valid, and what a result carries when an individual asset could not be copied are stated in one place, under Result assets in the API reference, and this paragraph does not restate them. On a model that returns its result on the original call, an answer still holding a partner''s own asset link is replayed for a few minutes - which is where a dropped connection puts an SDK''s automatic same-key re-send, and while the partner''s link is certainly still alive - and refused after that rather than replayed dead. So a prompt retry of a partially re-hosted result behaves exactly like any other replay, and only a later one meets the `409`. That short window is deliberately not offered on a model that submits and is polled, because there the partner may have minted the URL long before your call collected it and its remaining life is unknowable - and those models do not need it: a call cut off mid-generation keeps its key holding the generation, so the same-key retry collects the original result rather than a recorded copy of it. A response past the size cap has no window either and is refused from the start. The action on any of these `409 invalid_input` refusals is the same: use a new key. Only an answer a provider actually produced is recorded, though. A refusal Router raises on its own before dispatching anything - not enabled for you yet (`403`), unknown model (`404`), not entitled to the model (`403`), a body the model''s schema rejects or that names a different model than the path (`422`), a malformed request (`400 invalid_input`) - dispatched nothing and charged nothing, so it releases the key: re-send the same key once you are on the rollout ramp or have corrected the request and it runs for real, rather than replaying the refusal or colliding with it as a `409`. That turns on whether a provider was reached, never on the status, so a `400 content_policy_violation` - the partner''s own answer to a call that ran, which some models meter - is recorded and replayed like any other answer. Releasing a refusal that dispatched nothing frees nothing chargeable, so it does not weaken the at-most-once billing guarantee above.' schema: type: string minLength: 1 @@ -547,9 +874,24 @@ components: description: Lowercase provider segment of the canonical `{provider}/{model}` model ID - the partner whose model is being run. schema: $ref: '#/components/schemas/RouterProviderSegment' + RouterQueueRequestId: + name: request_id + in: path + required: true + description: 'The queued request to address - the `request_id` the submission returned in its body. It is not the submission''s `X-Comfy-Request-Id`: that header carries the id of one HTTP call and addresses nothing, as the `RouterQueueRequestId` schema spells out.' + schema: + $ref: '#/components/schemas/RouterQueueRequestId' + StrictMode: + name: strict_mode + in: query + required: false + description: 'Only meaningful together with `model_provider`. `false` (the default): the request body must be this model''s own native contract, translated to the alternate provider''s real schema - any native field that cannot be expressed exactly is dropped and disclosed via the response''s `X-Comfy-Router-Dropped-Params` header, never silently. `true`: the body must already be the alternate provider''s own real schema, passed through unmodified in both directions - no translation, so the header is never sent.' + schema: + type: boolean + default: false headers: CommittedSpendCurrentHeader: - description: The USD cents the caller currently has committed to calls still in flight. On a `429` this EXCLUDES the refused call, whose commitment was rolled back before the refusal was sent; on an admitted response it INCLUDES the call being answered. Present alongside `X-Committed-Spend-Limit`. + description: The USD cents the caller currently has committed to calls still in flight. On a `429` this excludes the refused call, whose commitment was rolled back before the refusal was sent; on an admitted response it includes the call being answered. Present alongside `X-Committed-Spend-Limit`. required: false schema: type: integer @@ -557,7 +899,7 @@ components: minimum: 0 example: 9600 CommittedSpendLimitHeader: - description: 'The ceiling, in USD cents, on the partner spend the caller may have committed to calls still in flight - money held from the moment a call is admitted and released when that call finishes. It is not a budget, a balance, or any running total of what the caller has spent to date: settling an invoice frees no room under it, and letting an in-flight call finish does. Contrast `X-Concurrency-Limit`, which bounds those same in-flight calls counted as a NUMBER OF CALLS rather than priced. How the ceiling is SIZED is a separate question from what it measures, and it is not tier-independent: the ceiling moves with the account''s lifetime paid spend, off the same thresholds the concurrent-call tier uses, so paying more raises it - see [partner-node concurrency limits](https://docs.comfy.org/tutorials/partner-nodes/concurrency-limits) for that ladder and for the concurrent-call bound that shares this `429`. Present on BOTH outcomes of an enforcing committed-spend gate - the `429` it raises and the success it admits - and absent while the gate is not enforcing, when it declines to decide and lets the call through, or on a `429` raised by the concurrent-call pool instead (a committed-spend `429` carries this trio and drops `X-Concurrency-*`).' + description: 'The ceiling, in USD cents, on the partner spend the caller may have committed to calls still in flight - money held from the moment a call is admitted and released when that call finishes. It is not a budget, a balance, or any running total of what the caller has spent to date: settling an invoice frees no room under it, and letting an in-flight call finish does. Contrast `X-Concurrency-Limit`, which bounds those same in-flight calls counted as a number of calls rather than priced. How the ceiling is sized is a separate question from what it measures, and it is not tier-independent: the ceiling moves with the account''s lifetime paid spend, off the same thresholds the concurrent-call tier uses, so paying more raises it - see [partner-node concurrency limits](https://docs.comfy.org/tutorials/partner-nodes/concurrency-limits) for that ladder and for the concurrent-call bound that shares this `429`. Present on both outcomes of an enforcing committed-spend gate - the `429` it raises and the success it admits - and absent while the gate is not enforcing, when it declines to decide and lets the call through, or on a `429` raised by the concurrent-call pool instead (a committed-spend `429` carries this trio and drops `X-Concurrency-*`).' required: false schema: type: integer @@ -572,13 +914,24 @@ components: format: int64 minimum: 0 example: 400 + RouterDroppedParamsHeader: + description: One JSON-encoded string holding an array of strings - decode it with a JSON parser rather than splitting it on commas, because it is a single string on the wire, not a comma-separated OpenAPI array, and each entry is a sentence carrying commas of its own - present whenever a translation produced this call's request body and could not express one or more native fields exactly on the provider that served it, naming each dropped field and why, whether the caller asked for that translation with `model_provider` (`strict_mode=false`, the default) or an automatic `fallback_provider` retry ran it. Absent when no translation ran, when translation ran but dropped nothing, and on an error response. On a fallback retry it names what that retry's own translation (into the provider that actually served the call) dropped, never the primary attempt's. + required: false + schema: + type: string + example: '["moderation (fal applies its own, non-configurable safety filtering)"]' 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 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. + 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 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' + RouterFallbackProviderHeader: + description: Present, naming the provider, only when `fallback_provider` actually retried this call against a second provider and that retry succeeded - the provider that ultimately served the call, never one that was attempted and also failed. Absent when the primary attempt itself succeeded, and absent on an error response. See `fallback_provider` for the retry policy this discloses. + required: false + schema: + type: string RouterIdempotentReplayedHeader: - description: Present and `true` when this response was served from an `Idempotency-Key`'s record rather than by running the model again. It carries the original call's status, body and content type, and it is not billed a second time - the charge settled when the original completed. The header is ABSENT on a fresh run rather than sent as `false`, so branch on its presence. + description: Present and `true` when this response was served from an `Idempotency-Key`'s record rather than by running the model again. It carries the original call's status, body and content type, and it is not billed a second time - the charge settled when the original completed. The header is absent on a fresh run rather than sent as `false`, so branch on its presence. required: false schema: type: boolean @@ -591,15 +944,22 @@ components: enum: - nosniff example: nosniff + RouterQueuePollAfterHeader: + description: Seconds to wait before polling this queued request again. It is Router's own estimate of when asking again is worth the round trip, and it moves with how far the request has actually got - a request at the back of the queue is told to wait longer than one already running. + required: false + schema: + type: integer + minimum: 1 + example: 3 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. + 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 RouterRetryAfterHeader: - description: 'Seconds to wait before retrying the SAME request with the SAME `Idempotency-Key`. It is set on the two answers such a retry can actually collect from: a `409` carrying `error_type: concurrency_limit_exceeded`, where the original call for that key is still running, and a `deadline_exceeded` `504`, where Comfy stopped holding the connection but still holds a handle to a generation the provider is running. In both cases the value is the interval Router itself would wait before asking again, which is the one honest number this route has for "ask again later". Absent when there is nothing to collect: an unkeyed call, a bound that expired before the provider accepted anything, or a `409` that refuses the key outright instead of asking the caller to wait.' + description: 'Seconds to wait before retrying the same request with the same `Idempotency-Key`. It is set on the two answers such a retry can actually collect from: a `409` carrying `error_type: concurrency_limit_exceeded`, where the original call for that key is still running, and a `deadline_exceeded` `504`, where Comfy stopped holding the connection but still holds a handle to a generation the provider is running. In both cases the value is the interval Router itself would wait before asking again, which is the one honest number this route has for "ask again later". Absent when there is nothing to collect: an unkeyed call, a bound that expired before the provider accepted anything, or a `409` that refuses the key outright instead of asking the caller to wait.' required: false schema: type: integer @@ -618,7 +978,7 @@ components: type: string example: '"6b8c1f2e0a9d4c3b5e7f8a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f"' RouterUpstreamStatusHeader: - description: 'The model provider''s OWN HTTP status for this call. Present only when the failure came FROM the provider, and ABSENT whenever Comfy Router refused the call itself - so branch on its presence: present means the request left Comfy, reached the provider, and the provider''s answer is what produced this response''s `error_type`.' + description: 'The model provider''s own HTTP status for this call. Present only when the failure came from the provider, and absent whenever Comfy Router refused the call itself - so branch on its presence: present means the request left Comfy, reached the provider, and the provider''s answer is what produced this response''s `error_type`.' required: false schema: type: integer @@ -635,4 +995,4 @@ components: 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.' + 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.'