ComfyPilotComfyPilot
文档 / 接口 / Price a model call before running it, without running it.

Price a model call before running it, without running it.

Returns a pre-flight estimate, in US dollars, of what running this body on this model will cost, priced at Comfy's standard list price (a model billed the cost its vendor reports is estimated from the…

接口 price-a-model-call-before-running-it-without-running-it 更新于 2026-11-13

查看官方原文

Price a model call before running it, without running it.

Returns a pre-flight estimate, in US dollars, of what running this body on this model will cost, priced at Comfy's standard list price (a model billed the cost its vendor reports is estimated from the vendor's published price list instead). It does not reflect contract or customer-specific rates: a workspace with negotiated pricing is quoted the list price and charged its contract rate, and source: exact then means exact at list price, not that workspace's charge. It is an estimate, not the final charge: the settled charge in your workspace's usage and billing records is what you pay. The body is validated against the model's input schema first, as a default run (no strict_mode, no reject_unknown_fields) validates it, so a body that fails the model's schema is the same 422 here as on the run; nothing is dispatched to a provider and nothing is charged. The answer is not a price lock: it is priced at the rates in force at pricing_as_of, and a run is charged at the rates in force when it is rated. If estimates are switched off for a deployment the route answers 403 with error_type: not_enabled; a model Router cannot run yet answers the same 403 with the run route's own detail, so read detail to tell the two apart. The body is priced as Router would dispatch it: translated for the model_provider leg it names and retargeted to the operation it runs on, in the same order as the estimate on the queued submit's 201. That estimate is not a pre-flight price, though: it arrives only after the run is admitted and queued, and that run is charged unless it is cancelled before dispatch.

OpenAPI

````yaml /router-openapi.yaml post /v2/models/{provider}/{model}/estimate openapi: 3.0.2 info: title: Comfy Router description: >- Comfy Router's public contract: the model catalog and the model-ID-addressed invocation routes, with the error buckets they return. Projected from the canonical Comfy API contract. version: '1.0' servers:

  • url: https://api.comfy.org security: [] tags:
  • name: Comfy Router description: Comfy Router's canonical, model-ID-addressed routes. paths: /v2/models/{provider}/{model}/estimate: post: tags:
  • Comfy Router summary: Price a model call before running it, without running it. description: >- Returns a pre-flight estimate, in US dollars, of what running this body on this model will cost, priced at Comfy's standard list price (a model billed the cost its vendor reports is estimated from the vendor's published price list instead). It does not reflect contract or customer-specific rates: a workspace with negotiated pricing is quoted the list price and charged its contract rate, and source: exact then means exact at list price, not that workspace's charge. It is an estimate, not the final charge: the settled charge in your workspace's usage and billing records is what you pay. The body is validated against the model's input schema first, as a default run (no strict_mode, no reject_unknown_fields) validates it, so a body that fails the model's schema is the same 422 here as on the run; nothing is dispatched to a provider and nothing is charged. The answer is not a price lock: it is priced at the rates in force at pricing_as_of, and a run is charged at the rates in force when it is rated. If estimates are switched off for a deployment the route answers 403 with error_type: not_enabled; a model Router cannot run yet answers the same 403 with the run route's own detail, so read detail to tell the two apart. The body is priced as Router would dispatch it: translated for the model_provider leg it names and retargeted to the operation it runs on, in the same order as the estimate on the queued submit's 201. That estimate is not a pre-flight price, though: it arrives only after the run is admitted and queued, and that run is charged unless it is cancelled before dispatch. operationId: estimateRouterModelCost parameters:
  • $ref: '#/components/parameters/RouterProvider'
  • $ref: '#/components/parameters/RouterModel'
  • $ref: '#/components/parameters/ModelProvider' requestBody: required: true description: >- The partner model's native JSON input, identical to the body the synchronous route accepts for this model. It is validated against the model's own input schema as a default run validates it (this route takes no strict_mode or reject_unknown_fields), and its own fields (the output count, the operation an input image selects) decide what is priced. content: application/json: schema: $ref: '#/components/schemas/RouterModelInput' responses: '200': description: >- The quote. Read source before reading any amount: only exact and free carry amount (free as "0"), and an unknown quote carries a reason instead of a figure. headers: X-Comfy-Request-Id: $ref: '#/components/headers/RouterRequestIdHeader' content: application/json: schema: $ref: '#/components/schemas/RouterCostEstimate' '400': $ref: '#/components/responses/RouterRequestError' '401': $ref: '#/components/responses/RouterRequestError' '403': $ref: '#/components/responses/RouterRequestError' '404': $ref: '#/components/responses/RouterRequestError' '413': $ref: '#/components/responses/RouterRequestError' '422': $ref: '#/components/responses/RouterEstimateValidationError' '429': $ref: '#/components/responses/RouterRateLimited' '503': $ref: '#/components/responses/RouterRequestUnavailable' '504': $ref: '#/components/responses/RouterRequestError' security:
  • BearerAuth: []
  • ApiKeyAuth: [] components: parameters: RouterProvider: name: provider in: path required: true description: >- Lowercase provider segment of the canonical {provider}/{model} model ID - the partner whose model is being run. schema: $ref: '#/components/schemas/RouterProviderSegment' RouterModel: name: model in: path required: true description: >- Lowercase model segment of the canonical {provider}/{model} model ID - the model to run within that provider. schema: $ref: '#/components/schemas/RouterModelSegment' ModelProvider: name: model_provider in: query required: false 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 workspace configured for direct provider calls only refuses any non-default model_provider with 403 and error_type: forbidden, and Router never runs its fallback_provider retry for it; a resubmit carrying the Idempotency-Key of a request already accepted is answered as a replay of that request rather than refused. 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. Then, on a Seedance 2.x model, a body carrying an asset:// reference in any content[] image, video or audio url is refused 400 with error_type: invalid_input: those ids are BytePlus virtual-library assets no other provider can serve, so send the media itself as an http(s) URL or a data: URI to use model_provider. Past those 400s, 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 schemas: RouterModelInput: type: object description: >- A partner model's native JSON input document, forwarded to the provider as-is. Its concrete shape is owned by the partner rather than by Comfy, so this is an open object: Router does not narrow, rename, or re-envelope the fields. It is a named component (never an inline anonymous object) because ComfyUI's spec-driven codegen needs a class to generate. additionalProperties: true RouterCostEstimate: type: object description: >- A pre-flight estimate of what this call will cost, in US dollars, priced before dispatch at Comfy's standard list price, or — for a model billed the cost its vendor reports — from the vendor's published price list. For that vendor-reported case, source: exact is returned only for a small opted-in set of models, and only when the published price list fully determines the figure (every price-driving field was stated, or left at a default that cannot change it); every other vendor-reported quote is estimated, including some the list would fully determine. Either way the run is still billed the amount the vendor reports after it completes, never the quoted figure verbatim. It does not reflect contract or customer-specific rates: a workspace with negotiated pricing is quoted the list price and charged its contract rate, and `source: exact` then means exact at list price, not that workspace's charge. It is not the final charge: the settled charge in your workspace's usage and billing records is what you pay. X-Comfy-Credits-Used, reported after the run, is a rounded post-hoc figure, not a receipt. source says how much to trust the number: exact carries amount; free carries amount "0" (the rate card prices this request at zero); estimated carries min_amount and max_amount and no amount; unknown carries neither and a reason. Never read an absent amount as free. Not a price lock: see pricing_as_of. required:
  • source
  • currency
  • provider
  • model
  • pricing_as_of properties: source: type: string description: >- exact | estimated | free | unknown; read any other value as unknown. exact and estimated both label an estimate; neither is the final charge. free means the rate card prices this request at zero, so amount is "0". unknown carries no figure. currency: type: string description: Always USD. amount: type: string description: >- The estimated cost in dollars as a decimal string, e.g. "0.04", with up to 8 decimals and no exponent. A chargeable sub-cent price is never rendered as "0"; only a free quote reads "0". Present only when source is exact or free. Not the final charge: the settled charge in your workspace's usage and billing records is. min_amount: type: string description: >- The low end of the estimate in dollars as a decimal string. It is an estimate, not a guaranteed floor: a call can be billed less, for example when the provider returns less than the request asked for (such as no images), when the estimate is a typical price learned from past calls, or when the price does not rise steadily with the amount requested. Present only when source is estimated. max_amount: type: string description: >- The high end of the estimate in dollars as a decimal string. It is an estimate, not a guaranteed cap: a call can be billed more, for example when the provider returns more than was asked (such as more images than requested), when the estimate is learned from past calls, or when the price does not rise steadily with the amount requested. Present only when source is estimated. amount_cents: type: number format: double description: >- The same figure in US cents, unrounded, for arithmetic. Present with amount. min_amount_cents: type: number format: double description: 'min_amount in US cents, unrounded. Present with min_amount.' max_amount_cents: type: number format: double description: 'max_amount in US cents. Present with max_amount.' credits: type: number format: double description: >- amount in Comfy credits using the platform conversion ($1 = 211 credits), rounded to 2 decimals - the figure X-Comfy-Credits-Used is expected to carry for an exact quote. That header is a rounded post-hoc figure, not a receipt; the settled charge in your workspace's usage and billing records is final. provider: type: string description: >- The provider leg this quote is for. On a queued submit it is the leg that will run. model: $ref: '#/components/schemas/RouterModelId' pricing_as_of: type: string format: date-time description: >- When the rate card this quote was priced from was fetched. The estimate is not a price lock: the run is charged at the rates in force when it is rated, not at this instant. An estimated quote taken from the vendor's published price list (a model billed the cost the vendor reports) carries the date that list was read instead. A quote whose billed quantity is predicted from a published vendor schedule and then priced from the rate card carries the older of that schedule's read date and the card's fetch time, for an exact answer and an estimated one alike (a model billed the credits its vendor reports is the estimated case). Whenever no rate card was read (reason: pricing_unavailable, byok, or a not_quotable call that cannot be priced from any card), this is when the quote was answered. reason: type: string description: >- Present only when source is unknown: not_quotable (Router cannot price this call before it runs - its price depends on what the provider reports back, or the body does not bound a billed quantity), unpriced_model (the rate card does not price this request), byok (queued submit only: the run uses your own provider key, so Comfy bills nothing for it), or pricing_unavailable (queued submit only: the rate card could not be read when the request was admitted; this is transient, and the run is still charged as usual). Read any other value as not_quotable. RouterProviderSegment: type: string description: >- Lowercase provider segment of the canonical {provider}/{model} model ID - the partner whose model is being addressed. The invocation route's provider path parameter and a catalog entry's provider field both reference this one schema, which is what keeps the listed IDs and the accepted IDs from drifting apart. pattern: ^[a-z0-9]+([._-][a-z0-9]+)*$ maxLength: 64 example: bfl RouterModelSegment: type: string description: >- Lowercase model segment of the canonical {provider}/{model} model ID
  • the model to run within that provider. Shared by the invocation route's model path parameter and a catalog entry's model field, for the same no-drift reason as RouterProviderSegment. pattern: ^[a-z0-9]+([._-][a-z0-9]+)*$ maxLength: 128 example: flux-2-pro 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. pattern: ^[a-z0-9]+([._-][a-z0-9]+)/[a-z0-9]+([._-][a-z0-9]+)$ maxLength: 193 example: bfl/flux-2-pro RouterErrorResponse: type: object description: >- Router's request-level error body: what is returned when the request never reached the model, or failed for a reason the model itself did not report - auth, quota, an unknown model ID, or provider transport. A model-level validation failure has its own shape, RouterValidationErrorResponse, because flattening a FastAPI detail[] array into this detail string would destroy the per-field granularity an SDK branches on. properties: detail: type: string description: >- Human-readable description of the failure, safe to surface to an end user. Not machine-parsed - branch on error_type instead. For model_not_found on a retired model, detail reads `<id> was retired on <YYYY-MM-DD>; use <successor>` when a successor is recorded and visible to the caller, or just `<id> was retired on <YYYY-MM-DD>` when it is not, instead of the usual suggestion list. A successor named here is always a live catalog ID the call can be re-pointed at. A model whose sunset date is still in the future is not retired: it stays listed by GET /v2/models and stays runnable until that date. Like the rest of detail, this retirement wording is for display and carries no compatibility guarantee. error_type: $ref: '#/components/schemas/RouterErrorType' upstream_detail: type: string description: >- A bounded, sanitized reason for an invalid_input rejection. On the synchronous route it is the reason the model provider gave, 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). There it is absent on every other failure, including provider 5xx, transport failures, content-policy refusals and any refusal Router raised about itself, and it mirrors the X-Comfy-Upstream-Detail header. The queued collect (`GET /v2/models/{provider}/{model}/requests/{request_id}`) of a run that ended invalid_input carries the same reason in this field on its first collect, which then deletes it from the request: a later collect normally answers the same error_type and detail without it, so read it from the first collect. That read sends neither header. The queued collect also uses this field for Router's own reason when Router refused the request before sending it to the provider and can state why in one sentence, which the synchronous route answers in detail; a refusal that names individual fields carries none. refusal_subject: type: string description: >- Which input or output a content-policy refusal was about, as a Router-level closed vocabulary: input, output, input_text, input_image, input_video, input_audio, output_text, output_image, output_video, output_audio. The 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, and for the MiniMax Router's Nano Banana legs' Gemini refusals; a provider whose refusal does not say which side it was about leaves it absent. It mirrors the X-Comfy-Refusal-Subject header. denied_reason: type: string description: >- Billing's policy branch for a partner-node spend denial - no_funds when the billed wallet is empty, unspecified when the call was refused without a funds reading saying so. Present only when error_type is insufficient_credits and the deployment reports wallet facts; absent otherwise. A machine-readable code, not display text: show detail, branch on this. billed_workspace: $ref: '#/components/schemas/RouterBilledWorkspace' credential_kind: type: string description: >- How the refused request authenticated - personal_api_key, team_api_key, personal_session or team_session. Answers "which credential do I switch to bill the other wallet?". It is not a permission and grants nothing. Present only alongside denied_reason. required:
  • detail
  • error_type RouterValidationErrorResponse: type: object description: >- Router's model-level 422 body, in the FastAPI form: the request was well-formed enough to reach the model and the model rejected its contents. Note it carries no error_type of its own - that is what X-Comfy-Error-Type on the response is for, so a client can read the coarse bucket off the header without first deciding which of the two Router error bodies it received. properties: detail: type: array description: >- Every validation failure found on the request, one entry per offending field. items: $ref: '#/components/schemas/RouterValidationErrorDetail' required:
  • detail 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 twenty 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, queue_backlog_full and result_expired. Closed describes the set as documented today, not a bound that holds forever: the set is expected to grow, which is why this is deliberately a plain string and not 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 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.
  • value: content_policy_violation tier: request meaning: >- The provider refused the request on content-policy grounds. The refusal is deterministic: re-sending the same input will be refused again.
  • value: provider_error tier: request meaning: >- The partner provider reported a failure of its own, or returned a response Router could not interpret as a result.
  • value: provider_timeout tier: request meaning: >- The partner provider did not answer within its deadline. This bucket is the provider timing out and never Router's own server deadline, which is reported as deadline_exceeded - the two 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, an unenrolled id, one the catalog has retired past its sunset date, or a catalogued ID the provider does not currently serve for Comfy. A retired id's detail reads `<id> was retired on <YYYY-MM-DD>; use <successor>` when a successor is recorded and not withheld, or just <id> was retired on <YYYY-MM-DD> when it is not, instead of a suggestion list; the successor named for a retired id is always a live catalog ID you can re-point the call at, and a successor is dropped from detail either because the table records none, or because the recorded one is currently withheld behind an embargo the caller cannot see. A model whose sunset is still in the future is not withdrawn: it stays listed by GET /v2/models and stays runnable until the date, so a listing taken today remains the answer to what runs today. Otherwise detail carries up to three suggestions drawn from the models the caller is entitled to see, except for a catalogued ID the provider does not currently serve for Comfy, whose response carries no suggestions.
  • value: unauthorized tier: transport meaning: The request carried no usable credential.
  • value: forbidden tier: transport meaning: >- The credential is valid but is not entitled to this model or this operation.
  • value: concurrency_limit_exceeded tier: transport meaning: >- The workspace already has as many calls in flight as it is allowed; retry once one of them finishes. It carries one further condition on the run route, on a 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.
  • 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.
  • 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. 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".
  • 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, and Retry-After the seconds to wait.
  • value: cancelled tier: transport meaning: >- A queued request was withdrawn — through the cancel route, or by an operator — before it produced a result; it is terminal, and it is not by itself a statement about the charge. Cancelling stops Comfy waiting and it does not always stop the partner working, so a request cancelled while it was still IN_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.
  • 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.
  • value: result_expired tier: transport meaning: >- The request existed and finished, but its result is no longer stored, so the collect has nothing to return. It is returned under 410 and is permanent for that id: re-reading will answer the same way, so do not retry it. It is deliberately not request_not_found, which would tell you the id was never valid and invite re-submitting a generation that has already run. Running the model again means submitting a new request. For a workspace under a zero-data-retention agreement, a queued request's result is not kept after it is delivered: it is removed shortly after the first collect, and an uncollected result is removed after a grace period once the request finishes, so store the result on your side when you first collect it. RouterBilledWorkspace: type: object description: >- The wallet a refused call would have charged, on RouterErrorResponse.billed_workspace. Answers "which wallet is empty?"
  • it is not necessarily the workspace the caller's UI currently has selected, and a team member's personal credential names their personal wallet here even while a funded team is open. Present only alongside denied_reason. properties: id: type: string description: The Cloud workspace id. Absent on the legacy rail. type: type: string description: >- personal or team. An open string: treat any other value as personal. rail: type: string description: >- legacy when the call bills the caller's pre-workspace Metronome customer rather than a Cloud workspace; that customer has no workspace id. Absent for a Cloud workspace. required:
  • type 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. properties: loc: type: array description: >- Path to the offending field, outermost segment first - for example ["body", "image_url"], or ["body", "images", 0] where an integer indexes into an array. items: anyOf:
  • type: string
  • type: integer msg: type: string description: Human-readable description of this single failure. type: type: string description: >- Specific, machine-readable reason for this failure, passed through from the provider unchanged. This is the value a typed SDK exception hierarchy branches on; error_type on the response header is only its coarse bucket. example: image_too_small ctx: $ref: '#/components/schemas/RouterValidationErrorContext' input: $ref: '#/components/schemas/RouterValidationErrorInput' required:
  • loc
  • msg
  • type 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 RouterValidationErrorInput: description: >- The offending input value, echoed back verbatim so a caller can see what was rejected without re-deriving it from loc. Any JSON type - string, number, boolean, array, object or null - so this schema is deliberately left untyped rather than narrowed to an object. Absent when the provider does not echo the input back. headers: RouterRequestIdHeader: description: >- Server-generated identifier for this call, present on every Router response - success, 4xx and 5xx alike, because an error response is exactly when a user needs an id to quote in a support request. The same value is written into the call's usage/audit event, which is what lets a complaint about a charge be joined to the charge itself instead of searched for by timestamp. required: true schema: type: string format: uuid example: 6f1a1a6e-6a53-4a5f-9d3a-2b3b0a1f9c21 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. required: true schema: $ref: '#/components/schemas/RouterErrorType' RouterRateLimitRetryAfterHeader: description: >- Seconds to wait before sending another request after a 429 carrying error_type: rate_limited: the time until the caller's per-minute request allowance has refilled enough to admit one more request. It is sent on every rate_limited refusal and on no other answer, so it is absent on a 429 whose X-Comfy-Error-Type is concurrency_limit_exceeded or queue_backlog_full. Nothing the caller does drains it early; a request sent sooner is refused again and spends nothing, but gains nothing either. required: false schema: type: integer minimum: 1 example: 2 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, or no slot was free to scan an oversized body - 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 503s 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 responses: RouterRequestError: description: >- A Router request-level failure - the request never reached the model, or failed for a reason the model itself did not report. The body is RouterErrorResponse and the bucket is repeated on X-Comfy-Error-Type. headers: X-Comfy-Error-Type: $ref: '#/components/headers/RouterErrorTypeHeader' X-Comfy-Request-Id: $ref: '#/components/headers/RouterRequestIdHeader' content: application/json: schema: $ref: '#/components/schemas/RouterErrorResponse' RouterEstimateValidationError: description: >- The estimate's body was rejected against the model's schema, or named a different model than the path. The body is RouterValidationErrorResponse, the same FastAPI detail[] shape the run route's 422 carries for the same body. Nothing was priced. An estimate reserves no Idempotency-Key, so there is nothing to release or replay: correct the body and re-send. headers: X-Comfy-Error-Type: $ref: '#/components/headers/RouterErrorTypeHeader' X-Comfy-Request-Id: $ref: '#/components/headers/RouterRequestIdHeader' content: application/json: schema: $ref: '#/components/schemas/RouterValidationErrorResponse' RouterRateLimited: description: >- The caller has spent their per-caller request allowance, which refills continuously over a one-minute window, and the request was refused before Router did anything with it. The bucket is always rate_limited: wait the number of seconds in Retry-After before sending the next request. Nothing was charged, and a request sent sooner is refused again. The body is RouterErrorResponse and the bucket is repeated on X-Comfy-Error-Type. headers: X-Comfy-Error-Type: $ref: '#/components/headers/RouterErrorTypeHeader' X-Comfy-Request-Id: $ref: '#/components/headers/RouterRequestIdHeader' Retry-After: $ref: '#/components/headers/RouterRateLimitRetryAfterHeader' content: 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, or no slot was free to scan an oversized body - 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' securitySchemes: BearerAuth: type: http scheme: bearer bearerFormat: JWT description: >- Bearer token authentication. Normally a Firebase or Cloud JWT. A 'comfyui-' prefixed API key is also accepted in this header: the prefix classifies the value as an API key and it is validated exactly as if it had been sent in X-API-Key. ApiKeyAuth: type: apiKey in: header name: X-API-Key description: >- API key authentication. Send the key in the X-API-Key header; keys are prefixed with 'comfyui-' and are generated from user account settings. The same 'comfyui-' key is also accepted in Authorization: Bearer (see BearerAuth), and when both headers carry a key, X-API-Key wins.

````