From ab41469a3a7bb3ac2679eee2cdf1ae2e9f8dd5e1 Mon Sep 17 00:00:00 2001 From: mattmillerai <7741082+mattmillerai@users.noreply.github.com> Date: Tue, 29 Sep 2026 05:50:50 +0000 Subject: [PATCH] chore: sync vendored Comfy Router spec from cloud@8215ce6 --- spec/router-openapi.yaml | 86 +++++++++++++++++++++++++++++++++------- 1 file changed, 72 insertions(+), 14 deletions(-) diff --git a/spec/router-openapi.yaml b/spec/router-openapi.yaml index ee5dd54..bb4c4f9 100644 --- a/spec/router-openapi.yaml +++ b/spec/router-openapi.yaml @@ -95,7 +95,7 @@ paths: - $ref: '#/components/parameters/FallbackProvider' requestBody: required: true - 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. + description: 'The partner model''s native JSON input. Without `model_provider` the request runs native dispatch on the model''s default provider and this body is the model''s own native schema, forwarded unchanged (`strict_mode` is meaningless there and changes nothing). 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. With `model_provider` selecting an alternate provider and `strict_mode=true` no translation runs: the body must already be that alternate provider''s own real schema, not this model''s native one (see `strict_mode`), and is forwarded unchanged. The body''s own fields also select which operation Router runs on a model that supports more than one: for an editable image model, including an input image switches it from text-to-image to the image-to-image (edit) operation; for a Seedance video model, a first-frame image selects image-to-video and a reference image or clip selects reference-to-video. Each conditioned operation is metered on its own rate, not the base text-to-image or text-to-video rate.' content: application/json: schema: @@ -151,7 +151,7 @@ paths: '502': $ref: '#/components/responses/RouterProviderError' '503': - $ref: '#/components/responses/RouterRequestError' + $ref: '#/components/responses/RouterRequestUnavailable' '504': $ref: '#/components/responses/RouterDeadlineExceeded' /v2/models/{provider}/{model}/openapi.json: @@ -219,10 +219,12 @@ paths: parameters: - $ref: '#/components/parameters/RouterProvider' - $ref: '#/components/parameters/RouterModel' + - $ref: '#/components/parameters/ModelProvider' + - $ref: '#/components/parameters/StrictMode' - $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. + description: 'The partner model''s native JSON input, identical to the body the synchronous route accepts for this model, and it selects the operation and is metered the same way: the body''s own fields choose which operation Router runs on a model that supports more than one (an input image switches an editable image model to image-to-image; a Seedance first-frame image selects image-to-video and a reference image or clip selects reference-to-video), and each conditioned operation is metered on its own rate, not the base text-to-image or text-to-video rate. The same provider-selection contract applies at dispatch: without `model_provider`, or with `model_provider` and `strict_mode=false` (the default), the body is the model''s native document and is 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 - a non-strict alternate-provider body is additionally translated into that provider''s real schema at dispatch. With `strict_mode=true` the body must already be the alternate provider''s own schema and is forwarded unchanged: native-schema validation is skipped, exactly as on the synchronous route (see `model_provider` and `strict_mode`).' content: application/json: schema: @@ -248,7 +250,7 @@ paths: '402': $ref: '#/components/responses/RouterRequestError' '403': - $ref: '#/components/responses/RouterRequestError' + $ref: '#/components/responses/RouterQueueSubmitForbidden' '404': $ref: '#/components/responses/RouterRequestError' '409': @@ -256,7 +258,7 @@ paths: '422': $ref: '#/components/responses/RouterModelValidationError' '503': - $ref: '#/components/responses/RouterRequestError' + $ref: '#/components/responses/RouterRequestUnavailable' /v2/models/{provider}/{model}/requests/{request_id}: get: summary: Collect the result of one submitted request. @@ -273,7 +275,7 @@ paths: - $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.' + description: 'OK - the result stored 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, and under the same provider-selection contract the submit accepted: 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 it is those bytes under the partner''s own `Content-Type`. Today no such model can be queued (the submit refuses it), so this branch is declared for the SDK contract ahead of the server serving it. 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' @@ -414,12 +416,18 @@ components: description: Human-readable description of the failure, safe to surface to an end user. Not machine-parsed - branch on `error_type` instead. error_type: $ref: '#/components/schemas/RouterErrorType' + upstream_detail: + type: string + description: A bounded, sanitized reason the model provider gave for rejecting the request, present only when `error_type` is `invalid_input` and `X-Comfy-Upstream-Status` is a provider `4xx` or `2xx` - i.e. the provider refused the request as malformed and said why. Usually that status is a `4xx`; it is a `2xx` for a provider that reports a rejected generation inside a success envelope (a BytePlus failed-task poll is HTTP `200` with the reason in its body). Absent on every other failure, including provider `5xx`, transport failures, content-policy refusals and any refusal Router raised about itself. It mirrors the `X-Comfy-Upstream-Detail` header. + refusal_subject: + type: string + description: 'Which input or output a content-policy refusal was about, as a Router-level closed vocabulary: `input`, `output`, `input_text`, `input_image`, `input_video`, `input_audio`, `output_text`, `output_image`, `output_video`, `output_audio`. The bare `input` / `output` values name the side when the provider did not name a modality. Present only when `error_type` is `content_policy_violation` and the provider named the refused subject with a machine-readable code; absent otherwise. Never provider text. Named today for BytePlus, Runway, BFL, Gemini, Veo, Vertex, xAI and Wan refusals; a provider whose refusal does not say which side it was about leaves it absent. It mirrors the `X-Comfy-Refusal-Subject` header.' required: - detail - error_type RouterErrorType: type: string - 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 eighteen 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`, `rate_limited`, `cancelled`, `queue_timeout` and `request_not_found`. Closed describes the set as documented today, not a bound that holds forever: the set is expected to grow, which is why this is deliberately a plain string and not an `enum`, so a client must treat an unrecognised value as `internal_error` rather than switch exhaustively over the list above and break on the next addition.' + 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 nineteen 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`, `rate_limited`, `cancelled`, `queue_timeout`, `request_not_found` and `queue_backlog_full`. Closed describes the set as documented today, not a bound that holds forever: the set is expected to grow, which is why this is deliberately a plain string and not an `enum`, so a client must treat an unrecognised value as `internal_error` rather than switch exhaustively over the list above and break on the next addition.' example: invalid_input x-comfy-error-types: - value: invalid_input @@ -460,7 +468,7 @@ components: 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. The one exception to "about the caller" is the queued submit, which also answers `not_enabled` for a model whose partner answers a generation directly as bytes: that model cannot yet be queued, so it is the model and not the caller that is refused, nothing is queued or charged, and the synchronous route `POST /v2/models/{provider}/{model}` runs it instead.' - value: service_unavailable tier: transport meaning: 'A service Comfy Router depends on is temporarily unavailable and the caller did nothing wrong. Retry it with backoff: it is the one bucket here whose condition clears on its own, without the caller changing the request and without a concurrency slot freeing, which is what distinguishes it from the other retryable answers (`concurrency_limit_exceeded`, `deadline_exceeded`). It is separate 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".' @@ -476,6 +484,9 @@ components: - 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. + - value: queue_backlog_full + tier: transport + meaning: 'The caller already has too many queued requests waiting to run, so this submit was refused. It shares `429` with `concurrency_limit_exceeded` and is not the same thing: that one is the synchronous route''s answer for too many calls in flight at once, whereas the queue accepts a submit at that limit and parks it, and this bucket is the separate bound on how many a caller may leave waiting so that parking cannot mean enqueuing without end. It clears as the caller''s own queued requests finish, so retry once some of them complete.' RouterModelBilling: type: object description: Per-model billing facts a caller needs before invoking - not prices. Usage and cost figures never appear here. @@ -759,7 +770,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: 'This request cannot be served as sent, and on `POST /v2/models/{provider}/{model}` three unrelated conditions answer this status. The first, which every route declaring this status can raise: the `Idempotency-Key` on this request is already held, and this request cannot be answered from its record. Two buckets share the status there 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 for the key 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 second and third conditions are raised only by `POST /v2/models/{provider}/{model}`, are not about the `Idempotency-Key` at all, and are both reachable on a request that carries no key. Router checks the third before the second, so a request that would trip both is refused for the third: an explicit `model_provider` naming an alternate provider on a request whose body selects a multipart operation (an edit, for example gpt-image''s `image` field), when that provider''s translator for this model cannot itself serve the operation - the swap is refused rather than silently billing a plain generation for the edit the caller actually asked for. A leg whose translator does carry the media is not refused and proceeds normally, so this refusal is per-leg rather than blanket. This one is not raised by the automatic on-failure retry `fallback_provider` controls, which is simply skipped (inert, not refused) on any multipart body, whether or not a leg could have served it - see the `model_provider` parameter. Its `detail` begins "this request''s `image` selects the edit operation". The second: `model_provider` named an alternate provider on a request that had already resolved a bring-your-own-key credential for the provider in the path. The three are mutually exclusive - the multipart case is decided from the request body and the named leg''s own translator, before BYOK is even considered - and the BYOK case is exclusive with the key case because that credential was resolved for the path''s provider while every alternate leg dispatches on Comfy''s own key for its own provider, so the call is refused before anything is dispatched and nothing is charged. Both the second and third carry `invalid_input`, the same bucket as the terminal key case above, so `X-Comfy-Error-Type` does not separate any of the three and `detail` is what a client branches on: the BYOK case begins `this request resolved a BYOK credential`, and a new key does not help - drop `model_provider`, or send the request without the BYOK credential. See the `model_provider` parameter, which also records that `fallback_provider` is inert rather than refused on a BYOK request. The body is `RouterErrorResponse` and the bucket is repeated on `X-Comfy-Error-Type`.' headers: X-Comfy-Error-Type: $ref: '#/components/headers/RouterErrorTypeHeader' @@ -772,7 +783,7 @@ components: schema: $ref: '#/components/schemas/RouterErrorResponse' RouterModelValidationError: - description: The request's contents were rejected against the model's schema. The body is `RouterValidationErrorResponse`, the 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. + description: The request's contents were rejected against the model's schema. The body is `RouterValidationErrorResponse`, the 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. A JSON request body that is not an object at all (an array, a string, a number, `null`) is refused here too, as `dict_type` at `["body"]`. headers: X-Comfy-Error-Type: $ref: '#/components/headers/RouterErrorTypeHeader' @@ -797,6 +808,17 @@ components: application/json: schema: $ref: '#/components/schemas/RouterErrorResponse' + RouterQueueSubmitForbidden: + 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 `not_enabled` also answers a model whose partner answers a generation directly as bytes: such a model cannot yet be queued, so the submit refuses it and nothing is queued or charged. For that refusal it is the model and not the account that is refused: run such a model on the synchronous route, `POST /v2/models/{provider}/{model}`, instead. The same redirect is given, in `detail`, to a bring-your-own-key request whose credential resolved but that the queue cannot carry. A credential that carries no Comfy workspace is refused `not_enabled` here too, with a `detail` naming the credential, while the synchronous route accepts it. Any other `not_enabled` or `forbidden` here is refused by the synchronous route the same way, so read `detail` rather than the bucket before re-sending elsewhere.' + headers: + X-Comfy-Error-Type: + $ref: '#/components/headers/RouterErrorTypeHeader' + X-Comfy-Request-Id: + $ref: '#/components/headers/RouterRequestIdHeader' + content: + application/json: + schema: + $ref: '#/components/schemas/RouterErrorResponse' RouterRequestError: description: A Router request-level failure - the request never reached the model, or failed for a reason the model itself did not report. The body is `RouterErrorResponse` and the bucket is repeated on `X-Comfy-Error-Type`. headers: @@ -808,6 +830,19 @@ components: application/json: schema: $ref: '#/components/schemas/RouterErrorResponse' + RouterRequestUnavailable: + 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`. A `503` that was refused for capacity - the in-flight request-body budget was full - carries `Retry-After` naming when to re-send the identical request; a `503` raised because a dependency faulted does not, because none of those rails knows when it will recover. + headers: + X-Comfy-Error-Type: + $ref: '#/components/headers/RouterErrorTypeHeader' + X-Comfy-Request-Id: + $ref: '#/components/headers/RouterRequestIdHeader' + Retry-After: + $ref: '#/components/headers/RouterCapacityRetryAfterHeader' + content: + application/json: + 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`.' headers: @@ -817,6 +852,10 @@ components: $ref: '#/components/headers/RouterRequestIdHeader' X-Comfy-Upstream-Status: $ref: '#/components/headers/RouterUpstreamStatusHeader' + X-Comfy-Upstream-Detail: + $ref: '#/components/headers/RouterUpstreamDetailHeader' + X-Comfy-Refusal-Subject: + $ref: '#/components/headers/RouterRefusalSubjectHeader' Idempotent-Replayed: $ref: '#/components/headers/RouterIdempotentReplayedHeader' content: @@ -828,14 +867,14 @@ components: 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.' + 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. Fallback also turns itself off, regardless of this parameter, on a request that resolved a bring-your-own-key credential, on a request whose body selects a multipart operation, and under `strict_mode=true` - each binds the call to one specific provider and cannot be faithfully replayed against another: the alternate leg would dispatch on Comfy''s own key rather than on the caller''s credential, no alternate translator is guaranteed to carry the multipart media field, and a strict body is already shaped for one provider rather than for the native contract. Each of those is inert, not refused - no retry is attempted and the first attempt''s own failure reaches the caller unchanged. Only an explicit `model_provider` is refused, and see that parameter for the `409`s it answers with; note that fallback''s multipart disable is unconditional, while `model_provider`''s multipart `409` applies only to a leg whose translator cannot serve the operation.' 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.' + description: 'Selects an alternate provider to serve this model, instead of its current default. Omitting it runs native dispatch on the model''s own default provider; `default`, `comfy`, and `comfyui` are aliases for that same native behavior and name no override, because Comfy Router is never itself a serving backend. The alternate providers Router can retarget a model onto are `fal`, `wavespeed`, `runware`, and `higgsfield`; which of them a given model supports is reported by `GET /v2/models/{provider}/{model}`. When an alternate provider is selected, the native request body is translated into that provider''s real schema unless `strict_mode=true`; see `strict_mode` and `fallback_provider`. Its refusals are checked in a fixed order, and an earlier one answers whether or not a later one would. A value that is not a registered provider at all is refused `400` with `error_type: invalid_input`. Then a request whose body selects a multipart operation (an edit - for example gpt-image''s `image` field) is refused `409`, also with `error_type: invalid_input`, whose `detail` begins "this request''s `image` selects the edit operation" - but only when the named provider''s translator for this model cannot itself serve that operation; a leg whose translator does carry the media is not refused and proceeds normally, so this is a per-leg refusal rather than a blanket one. Then a request that has already resolved a bring-your-own-key credential for the provider in the path is refused `409`, also with `error_type: invalid_input`, whose `detail` begins `this request resolved a BYOK credential` - see the `409` on this route, where that `detail` is the only thing separating this case, and the multipart case above, from the `Idempotency-Key` one. That BYOK check runs whether or not the named provider has a leg for this model. Then the named provider''s own gate refuses `403` with `error_type: not_enabled` when that provider is not turned on for you, and `503` with `error_type: service_unavailable` when the gate cannot be evaluated - a flag-evaluation failure, or a missing or nil gate entry. Only past all of those is a real provider that does not serve this model refused `400` with `error_type: invalid_input`, the same answer as a value that is not a registered provider at all - both mean the `model_provider` value cannot serve this model, and that `400`''s `detail` is human-readable and not a contract, so read `GET /v2/models/{provider}/{model}` to learn which providers a model does support rather than parsing it; past that `400`, this workspace''s partner-provider policy for the named vendor is evaluated too and can refuse `403` or `503` of its own. Neither this parameter nor `fallback_provider` is available on a BYOK request, and the two are unavailable in different ways: the credential was resolved for the provider named in the path while every alternate leg dispatches on Comfy''s own key, so an explicit `model_provider` is refused with that `409`, and `fallback_provider` is inert rather than refused - no retry against an alternate provider is attempted and the first attempt''s own failure is what the caller receives. The same split applies to a multipart body: only an explicit `model_provider` reaches the `409` above, and only for a leg whose translator cannot serve the operation, while the automatic on-failure retry `fallback_provider` controls is simply skipped (inert, not refused) for any multipart body. Router defines no `provider_not_available` or `validation_error` `error_type`: these conditions fold onto `invalid_input`. The `error_type` set can still grow, so treat any value you do not recognize as `internal_error` rather than switching exhaustively.' schema: type: string RouterCatalogCursor: @@ -889,7 +928,7 @@ components: 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.' + 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 `X-Comfy-Router-Dropped-Params` is never sent, and provider fallback is disabled for the call because a body shaped for one provider cannot be replayed against another (see `fallback_provider`).' schema: type: boolean default: false @@ -918,6 +957,13 @@ components: format: int64 minimum: 0 example: 400 + RouterCapacityRetryAfterHeader: + description: Seconds to wait before re-sending the same request, unchanged. It is present only when Router refused the request body for capacity - the in-flight request-body budget was full - and a capacity refusal submitted nothing and charged nothing, so re-sending the identical call once the interval has passed is the whole remedy. There is no `Idempotency-Key` to collect under and no queued request to poll for; this is the one `Retry-After` on Router that means simply "send it again later". It is absent on the dependency-fault `503`s that share this status - a credential rail, an idempotency store or a policy decision point that could not answer - because none of them knows when it will, and a wrong number is worse than none against a dependency that is already failing. + required: false + schema: + type: integer + minimum: 1 + example: 4 RouterCreditsUsedHeader: description: 'What this run cost, in Comfy credits, priced from the same rate card the charge itself is billed against - so a caller needs no price table of its own, and on a run that reached the provider through more than one billed call it is their sum rather than the last one. It reports a price, not a settled ledger entry. On a model Comfy bills while your request is still open, the value is published only once the usage event was accepted by billing; on a model that submits and then polls, it is the price the asynchronous worker will bill, recorded before that charge settles - so a run whose billing later fails can still have carried this header. Treat it as what you will be charged, not as proof you were, and reconcile against the usage and billing API rather than against this header alone. Absent whenever no cost was reported: a request billed against your own provider key, a partner whose response does not carry every dimension its price is computed from, a usage event that matched no billable metric, or a charge that did not reach billing - so a missing header means "not reported" and must not be read as "free", and a report that sums absent headers as zero will not reconcile. A run that was rated and genuinely cost nothing reads `0`, which is a reported cost rather than a missing one, so branch on whether the header is present rather than on whether its value is non-zero. Coverage is partial today and widening, so do not assume the header is present for every model. On a response that also carries `Idempotent-Replayed: true` this restates what the original run cost, and that run is charged once however many times you retry the key - so do not add the header up across retries of one `Idempotency-Key`. Absent on an error response: it is written only on the path that returns a result, which a refused call never reaches.' required: false @@ -925,7 +971,7 @@ components: type: string example: '12.5' 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. + 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. The disclosure is bounded three ways, so that it stays a fixed size rather than one that grows with the request body — some native arrays (`input.media` on the Wan video family) are deliberately uncapped, and entries quote caller-supplied values: at most 64 entries, each at most 300 bytes plus an elision mark, summing to at most 4096 bytes of the encoded header value. Whichever bound binds first wins. Shortening is never silent: an entry cut to the per-entry bound ends in `…`, and when entries are left out entirely a final summarising entry states how many. Treat that count as a lower bound on the fields affected rather than a tally of them: it counts disclosure entries, and a translator may collapse many dropped elements into one entry (the Wan video family''s media groups do exactly that). Entries are also sanitised before publication — a media URL is stripped of its query string, which is what carries its credential — so an entry names the field it dropped rather than reproducing the value verbatim.' required: false schema: type: string @@ -961,6 +1007,12 @@ components: type: integer minimum: 1 example: 3 + RouterRefusalSubjectHeader: + description: 'Which input or output a content-policy refusal was about, as a Router-level closed vocabulary: `input`, `output`, `input_text`, `input_image`, `input_video`, `input_audio`, `output_text`, `output_image`, `output_video`, `output_audio`. Present only when `error_type` is `content_policy_violation` and the provider named the refused subject with a machine-readable code; absent otherwise - so branch on its presence. Never provider text. Mirrors `RouterErrorResponse.refusal_subject`.' + required: false + schema: + type: string + example: output_audio RouterRequestIdHeader: description: Server-generated identifier for this call, present on every Router response - success, 4xx and 5xx alike, because an error response is exactly when a user needs an id to quote in a support request. The same value is written into the call's usage/audit event, which is what lets a complaint about a charge be joined to the charge itself instead of searched for by timestamp. required: true @@ -987,6 +1039,12 @@ components: schema: type: string example: '"6b8c1f2e0a9d4c3b5e7f8a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f"' + RouterUpstreamDetailHeader: + description: A bounded, sanitized reason the model provider gave for rejecting the request. Present only when `error_type` is `invalid_input` and `X-Comfy-Upstream-Status` is a provider `4xx` or `2xx` - usually a `4xx`, and a `2xx` for a provider that reports a rejected generation inside a success envelope; absent otherwise - so branch on its presence. Mirrors `RouterErrorResponse.upstream_detail`. + required: false + schema: + type: string + example: expected the height to be at least 300px, but received a 445x283px image instead 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`.' required: false