diff --git a/spec/router-openapi.yaml b/spec/router-openapi.yaml index 29c3382..fca146e 100644 --- a/spec/router-openapi.yaml +++ b/spec/router-openapi.yaml @@ -110,7 +110,7 @@ paths: schema: $ref: '#/components/schemas/RouterModelOutput' '400': - $ref: '#/components/responses/RouterRequestError' + $ref: '#/components/responses/RouterRunRequestError' '401': $ref: '#/components/responses/RouterRequestError' '403': @@ -426,7 +426,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 - 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' @@ -439,12 +439,14 @@ components: schema: $ref: '#/components/schemas/RouterErrorResponse' RouterModelValidationError: - description: 'The request reached the model and the model rejected its contents. 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. The check runs before any provider call, so this answer is never charged - and because nothing was dispatched, it does not consume your `Idempotency-Key` either. The key is RELEASED: re-send the SAME key with the corrected body and the call runs for real, rather than replaying this `422` or colliding with it as a `409`.' + 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. headers: X-Comfy-Error-Type: $ref: '#/components/headers/RouterErrorTypeHeader' X-Comfy-Request-Id: $ref: '#/components/headers/RouterRequestIdHeader' + Idempotent-Replayed: + $ref: '#/components/headers/RouterIdempotentReplayedHeader' content: application/json: schema: @@ -460,6 +462,19 @@ components: 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: + X-Comfy-Error-Type: + $ref: '#/components/headers/RouterErrorTypeHeader' + X-Comfy-Request-Id: + $ref: '#/components/headers/RouterRequestIdHeader' + Idempotent-Replayed: + $ref: '#/components/headers/RouterIdempotentReplayedHeader' + content: + application/json: + schema: + $ref: '#/components/schemas/RouterErrorResponse' parameters: RouterCatalogCursor: name: cursor @@ -481,7 +496,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, 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 24 hours later; 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. Models whose results are assets are re-hosted onto Comfy storage before they are returned, and where a model''s output schema says that re-hosting is per entry - the xAI Grok Imagine image family says so explicitly - an entry Comfy could not re-host carries the PARTNER''s own short-lived URL, which carries no promise of outliving the record, so that answer is deliberately not replayed rather than replayed dead. The action is the same as for any other `409 invalid_input` on a key: use a new one. 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. Models whose results are assets are re-hosted onto Comfy storage before they are returned, and where a model''s output schema says that re-hosting is per entry - the xAI Grok Imagine image family says so explicitly - an entry Comfy could not re-host carries the PARTNER''s own short-lived URL, which carries no promise of outliving the record. On a model that returns its result on the original call - the xAI Grok Imagine image family again - that answer is still 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; after that it is refused 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