From 9c7556347b282924f9231d035453429469b18e60 Mon Sep 17 00:00:00 2001 From: mattmillerai <7741082+mattmillerai@users.noreply.github.com> Date: Fri, 28 Aug 2026 21:02:05 +0000 Subject: [PATCH] chore: sync vendored Comfy Router spec from cloud@2f46977 --- spec/router-openapi.yaml | 76 ++++++++++++++++++++++++++++++---------- 1 file changed, 58 insertions(+), 18 deletions(-) diff --git a/spec/router-openapi.yaml b/spec/router-openapi.yaml index 69adefd..3a06256 100644 --- a/spec/router-openapi.yaml +++ b/spec/router-openapi.yaml @@ -15,10 +15,10 @@ tags: - name: Comfy Router description: Comfy Router's canonical, model-ID-addressed routes. paths: - /v1/models: + /v2/models: get: summary: List the models Comfy Router can run. - description: 'Comfy Router''s model catalog - one page of the canonical model IDs that `POST /v1/models/{provider}/{model}` accepts. An SDK calls this on cold start to discover what is runnable, and the `model_not_found` suggestions come from the same catalog, so an ID listed here that then 404s on invocation would be worse than either failure alone. That agreement is structural rather than a promise: an entry''s `provider` and `model` are the two path segments of the invocation route and reference the SAME schema components that route''s path parameters do, and `id` is those two segments joined by `/`.' + description: 'Comfy Router''s model catalog - one page of the canonical model IDs that `POST /v2/models/{provider}/{model}` accepts. An SDK calls this on cold start to discover what is runnable, and the `model_not_found` suggestions come from the same catalog, so an ID listed here that then 404s on invocation would be worse than either failure alone. That agreement is structural rather than a promise: an entry''s `provider` and `model` are the two path segments of the invocation route and reference the SAME schema components that route''s path parameters do, and `id` is those two segments joined by `/`.' operationId: listRouterModels tags: - Comfy Router @@ -45,7 +45,7 @@ paths: $ref: '#/components/responses/RouterRequestError' '503': $ref: '#/components/responses/RouterRequestError' - /v1/models/{provider}/{model}: + /v2/models/{provider}/{model}: get: summary: Read one partner model's catalog entry by canonical model ID. description: Per-model detail for a single Comfy Router model, so a caller can check one model without walking the whole paginated catalog. The SDKs use it to look a model up immediately before invoking it. @@ -84,6 +84,7 @@ paths: parameters: - $ref: '#/components/parameters/RouterProvider' - $ref: '#/components/parameters/RouterModel' + - $ref: '#/components/parameters/RouterIdempotencyKey' requestBody: required: true description: The partner model's native JSON input, forwarded to the provider unchanged. @@ -93,18 +94,28 @@ paths: $ref: '#/components/schemas/RouterModelInput' responses: '200': - description: OK - the partner model's native JSON output, returned unchanged. + description: 'OK - the partner model''s native JSON output, returned unchanged. When this response was replayed from the record held against an `Idempotency-Key` rather than produced by running the model again, it carries `Idempotent-Replayed: true` and is not charged a second time.' headers: X-Comfy-Request-Id: $ref: '#/components/headers/RouterRequestIdHeader' + Idempotent-Replayed: + $ref: '#/components/headers/RouterIdempotentReplayedHeader' content: application/json: schema: $ref: '#/components/schemas/RouterModelOutput' + '400': + $ref: '#/components/responses/RouterRequestError' + '401': + $ref: '#/components/responses/RouterRequestError' '403': $ref: '#/components/responses/RouterRequestError' '404': $ref: '#/components/responses/RouterRequestError' + '409': + $ref: '#/components/responses/RouterIdempotencyConflict' + '413': + $ref: '#/components/responses/RouterRequestError' '422': $ref: '#/components/responses/RouterModelValidationError' '429': @@ -113,10 +124,10 @@ paths: $ref: '#/components/responses/RouterRequestError' '504': $ref: '#/components/responses/RouterDeadlineExceeded' - /v1/models/{provider}/{model}/openapi.json: + /v2/models/{provider}/{model}/openapi.json: get: - summary: Read one partner model's input schema as an OpenAPI document. - description: The per-model input schema for a single Comfy Router model, served as a standalone OpenAPI document, so a caller - an SDK, a codegen tool, or an agent - can discover a model's arguments without reading Comfy's prose docs. It is the discovery mechanism the SDK quickstart depends on. + summary: Read one partner model's input and output schemas as an OpenAPI document. + description: The per-model input AND output schemas for a single Comfy Router model, served as a standalone OpenAPI document, so a caller - an SDK, a codegen tool, or an agent - can discover a model's arguments, and the shape of what it returns, without reading Comfy's prose docs. It is the discovery mechanism the SDK quickstart depends on. operationId: getRouterModelInputSchema tags: - Comfy Router @@ -133,7 +144,7 @@ paths: type: string responses: '200': - description: OK - the model's input schema, as a standalone OpenAPI document. + description: OK - the model's input AND output schemas, as a standalone OpenAPI document. headers: X-Comfy-Request-Id: $ref: '#/components/headers/RouterRequestIdHeader' @@ -189,7 +200,7 @@ components: x-comfy-error-types: - value: invalid_input tier: request - meaning: The request was rejected before it reached the model - a malformed body, a malformed or expired pagination cursor, or an input the model's own schema does not accept. + 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.' @@ -213,7 +224,7 @@ components: meaning: The credential is valid but is not entitled to this model or this operation. - value: concurrency_limit_exceeded tier: transport - meaning: The workspace already has as many calls in flight as it is allowed; retry once one of them finishes. + 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.' @@ -255,10 +266,10 @@ components: format: uri pattern: ^https:// maxLength: 2048 - description: 'Pointer to this model''s input schema document - the description of the body `POST /v1/models/{provider}/{model}` accepts for this model. Only the POINTER is part of this contract: the document it addresses is authored separately. Absent when no schema has been authored for the model.' + description: 'Pointer to this model''s input schema document - the description of the body `POST /v2/models/{provider}/{model}` accepts for this model. Only the POINTER is part of this contract: the document it addresses is authored separately. Absent when no schema has been authored for the model.' RouterModelId: type: string - description: A canonical Comfy Router model ID, `{provider}/{model}` - exactly the value that addresses the model on `POST /v1/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. + 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: fal-ai/flux-pro @@ -268,7 +279,7 @@ components: additionalProperties: true RouterModelInputSchemaDocument: type: object - description: A standalone OpenAPI document describing ONE Comfy Router model's input - the body `POST /v1/models/{provider}/{model}` accepts for that model. It is what `GET /v1/models/{provider}/{model}/openapi.json` returns. + description: A standalone OpenAPI document describing ONE Comfy Router model's input AND output - the body `POST /v2/models/{provider}/{model}` accepts for that model, under the operation's `requestBody`, and the body it returns, under that operation's `200` content. It is what `GET /v2/models/{provider}/{model}/openapi.json` returns. The component keeps its historical name, which predates the output half; the shape it describes is the whole document, not the input alone. additionalProperties: true RouterModelListEntry: type: object @@ -313,7 +324,7 @@ components: - limit RouterModelOutput: type: object - description: 'A partner model''s native JSON output document, returned to the caller as-is. Its concrete shape is owned by the partner rather than by Comfy, so this is an open object: Router does not narrow, rename, or re-envelope the fields. It is a named component (never an inline anonymous object) because ComfyUI''s spec-driven codegen needs a class to generate.' + description: 'A partner model''s native JSON output document, returned to the caller as-is. Its concrete shape is owned by the partner rather than by Comfy, so this is an open object: Router does not narrow, rename, or re-envelope the fields. It is a named component (never an inline anonymous object) because ComfyUI''s spec-driven codegen needs a class to generate. For the concrete shape ONE model returns, read that model''s own document at `GET /v2/models/{provider}/{model}/openapi.json`, whose `200` carries the per-model output schema when Comfy has described it.' additionalProperties: true RouterModelSegment: type: string @@ -396,7 +407,20 @@ components: schema: $ref: '#/components/schemas/RouterErrorResponse' RouterDeadlineExceeded: - description: Comfy stopped holding the connection at its own configured bound (`deadline_exceeded`). The body and the two headers are exactly `RouterRequestError`'s; what this adds is the optional `Retry-After`, present when a retry with the same `Idempotency-Key` will collect the generation that is still running rather than dispatch a new one. See the `504` on `POST /v1/models/{provider}/{model}`. + description: Comfy stopped holding the connection at its own configured bound (`deadline_exceeded`). The body and the two headers are exactly `RouterRequestError`'s; what this adds is the optional `Retry-After`, present when a retry with the same `Idempotency-Key` will collect the generation that is still running rather than dispatch a new one. See the `504` on `POST /v2/models/{provider}/{model}`. + headers: + X-Comfy-Error-Type: + $ref: '#/components/headers/RouterErrorTypeHeader' + X-Comfy-Request-Id: + $ref: '#/components/headers/RouterRequestIdHeader' + Retry-After: + $ref: '#/components/headers/RouterRetryAfterHeader' + content: + application/json: + 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 faithful copy of its response to replay, 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' @@ -409,7 +433,7 @@ 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. + 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`.' headers: X-Comfy-Error-Type: $ref: '#/components/headers/RouterErrorTypeHeader' @@ -447,6 +471,16 @@ components: type: integer maximum: 100 default: 20 + RouterIdempotencyKey: + 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. 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 + maxLength: 255 + example: 6f1a1a6e-6a53-4a5f-9d3a-2b3b0a1f9c21 RouterModel: name: model in: path @@ -491,6 +525,12 @@ components: required: true schema: $ref: '#/components/schemas/RouterErrorType' + RouterIdempotentReplayedHeader: + description: Present and `true` when this response was served from an `Idempotency-Key`'s record rather than by running the model again. It carries the original call's status, body and content type, and it is not billed a second time - the charge settled when the original completed. The header is ABSENT on a fresh run rather than sent as `false`, so branch on its presence. + required: false + schema: + type: boolean + example: true 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 @@ -499,7 +539,7 @@ components: format: uuid example: 6f1a1a6e-6a53-4a5f-9d3a-2b3b0a1f9c21 RouterRetryAfterHeader: - description: 'Seconds to wait before retrying the SAME request with the SAME `Idempotency-Key`. Present on a `deadline_exceeded` `504` only when Comfy holds a handle to a generation the provider is still running; the value is Router''s own poll interval, which is the one honest number this route has for "ask again later". Absent when there is nothing to collect: an unkeyed call, or a bound that expired before the provider accepted anything.' + description: 'Seconds to wait before retrying the SAME request with the SAME `Idempotency-Key`. It is set on the two answers such a retry can actually collect from: a `409` carrying `error_type: concurrency_limit_exceeded`, where the original call for that key is still running, and a `deadline_exceeded` `504`, where Comfy stopped holding the connection but still holds a handle to a generation the provider is running. In both cases the value is the interval Router itself would wait before asking again, which is the one honest number this route has for "ask again later". Absent when there is nothing to collect: an unkeyed call, a bound that expired before the provider accepted anything, or a `409` that refuses the key outright instead of asking the caller to wait.' required: false schema: type: integer @@ -512,7 +552,7 @@ components: type: string example: private, max-age=300, must-revalidate RouterSchemaETagHeader: - description: Strong entity tag over the served document's bytes, for `GET /v1/models/{provider}/{model}/openapi.json`. A per-model schema changes rarely and an SDK re-fetches it often, so a caller should store this value and send it back as `If-None-Match` to get a `304` instead of the document. + description: Strong entity tag over the served document's bytes, for `GET /v2/models/{provider}/{model}/openapi.json`. A per-model schema changes rarely and an SDK re-fetches it often, so a caller should store this value and send it back as `If-None-Match` to get a `304` instead of the document. required: true schema: type: string