From 68214b6abdadaa9b39ad015f46febaadea93d55d Mon Sep 17 00:00:00 2001 From: james00012 <96548424+james00012@users.noreply.github.com> Date: Sat, 3 Oct 2026 04:49:46 +0000 Subject: [PATCH] chore: sync vendored Comfy API v2 spec from cloud@e32a655 --- spec/openapi.yaml | 294 +++++++++++++++++++++++++++-- src/comfy_low/models/_generated.py | 144 ++++++++++++-- 2 files changed, 407 insertions(+), 31 deletions(-) diff --git a/spec/openapi.yaml b/spec/openapi.yaml index 7d38013..214f700 100644 --- a/spec/openapi.yaml +++ b/spec/openapi.yaml @@ -124,11 +124,13 @@ paths: schema: $ref: '#/components/schemas/ErrorEnvelope' '422': - description: '`idempotency_key_reuse` or validation failure.' + description: '`idempotency_key_reuse`, `input_blocked` (the bytes are already stored and flagged by content moderation, so `file_path` is not registered for them), or validation failure.' content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' + '429': + $ref: '#/components/responses/RateLimited' '500': $ref: '#/components/responses/UpstreamError' /api/v2/assets/from-hash: @@ -201,6 +203,14 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' + '422': + description: '`invalid_request` on a deployment: the `file_path`, `tags` or `expires_in` is malformed.' + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorEnvelope' + '429': + $ref: '#/components/responses/RateLimited' '500': $ref: '#/components/responses/UpstreamError' /api/v2/assets/by-hash/{hash}: @@ -219,6 +229,8 @@ paths: description: No blob the caller may mint from. '401': $ref: '#/components/responses/Unauthorized' + '429': + $ref: '#/components/responses/RateLimited' '500': $ref: '#/components/responses/UpstreamError' /api/v2/assets/{id}: @@ -243,6 +255,8 @@ paths: $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/RateLimited' '500': $ref: '#/components/responses/UpstreamError' delete: @@ -281,6 +295,8 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' + '429': + $ref: '#/components/responses/RateLimited' '500': $ref: '#/components/responses/UpstreamError' /api/v2/assets/{id}/content: @@ -336,6 +352,14 @@ paths: $ref: '#/components/responses/NotFound' '416': description: Range not satisfiable. + '429': + $ref: '#/components/responses/RateLimited' + '451': + description: '`content_blocked` on a deployment: content moderation flagged these bytes, so they are not served.' + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorEnvelope' '500': $ref: '#/components/responses/UpstreamError' /api/v2/jobs: @@ -414,12 +438,18 @@ paths: additionalProperties: true extra_data: type: object - description: 'Per-prompt ComfyUI `extra_data`, same shape as Comfy Cloud and local ComfyUI. Closed object: only the enumerated keys are accepted, keeping the contract fully typed. Forwarded to the worker per-prompt, never persisted, and excluded from idempotency comparison.' + description: 'Per-prompt ComfyUI `extra_data`, same shape as Comfy Cloud and local ComfyUI. Closed object: only the enumerated keys are accepted, keeping the contract fully typed. Forwarded to the worker per-prompt and excluded from idempotency comparison. On a deployment it is dispatch-only and never stored; on Comfy Cloud it is persisted with the prompt, because the worker needs it, and redacted on every path that returns a workflow to a caller. + + + Send the one credential you hold: an API key as `api_key_comfy_org`, or the session token an interactively signed-in client has instead as `auth_token_comfy_org`. Sending both is accepted and both are forwarded, but it is not a supported combination and which one a node uses is not defined here. Note a session token is short-lived and is not re-minted for you, so one submitted long before it executes may expire in the queue.' additionalProperties: false properties: api_key_comfy_org: type: string description: API key for partner (API) nodes. + auth_token_comfy_org: + type: string + description: Session bearer token for partner (API) nodes — the equivalent of `api_key_comfy_org` for a caller authenticated by session rather than by key. responses: '201': description: Job created and queued. @@ -444,7 +474,7 @@ paths: schema: $ref: '#/components/schemas/ErrorEnvelope' '429': - description: '`queue_full` (bounded queue depth reached) or, on deployment-scoped surfaces, `deployment_not_ready` (deployment still provisioning/starting). Disambiguate by `error.code`; both mean back off and retry after `Retry-After`.' + description: '`queue_full` (bounded queue depth reached) or, on deployment-scoped surfaces, `deployment_not_ready` (deployment still provisioning/starting) or `deployment_unavailable` (the deployment is ready but its GPU provider is not taking work on it yet), or `rate_limited` (the caller is past a request rate limit). Disambiguate by `error.code`; all four mean back off and retry after `Retry-After`.' headers: Retry-After: $ref: '#/components/headers/RetryAfter' @@ -516,6 +546,134 @@ paths: $ref: '#/components/responses/RateLimited' '500': $ref: '#/components/responses/UpstreamError' + /api/v2/jobs/{id}/logs: + get: + operationId: getJobLogs + tags: + - jobs + summary: What the run printed + description: 'Returns the job''s captured execution log. Fetched on demand: a log + + is a debugging artifact a caller wants occasionally, while + + `GET /api/v2/jobs/{id}` is polled to terminal on every run, so the + + log is a resource of its own rather than a field that would ride + + every one of those polls to be read at most once. + + + Captured whenever the worker reports its own outcome, success and + + failure alike, since a job that succeeds while producing the wrong + + thing is exactly what a failure-only log cannot explain. A run the + + platform or the provider killed — out of memory, a crashed worker, a + + timeout, a job past its maximum runtime — never gets that far, so it + + reaches a terminal status carrying no log at all. That is a real gap + + and worth stating: the failures a caller most wants a log for are + + the ones least likely to have produced one. + + + **`204` is the normal answer for a job with no log**, and the cases + + behind it are deliberately not distinguished: this surface does not + + capture logs at all, the job has not finished, the job predates log + + capture, the run was killed before the worker could report one, + + capture was attempted and failed, or the job ran on the public demo + + deployment, which captures and stores the log like every other + + serverless deployment but withholds it on read, because that surface + + takes callers with no credential and a job id would otherwise be the + + only thing between one anonymous caller and another''s run. + + + Because a `204` never says which of those it is, do not branch on the + + reason — but do note that one of them resolves itself. A job that has + + not finished may have a log once it does, so a caller that wants one + + reads again after a terminal status. A `204` on a job already in a + + terminal state is final, and so is a missing `urls.logs`; both mean + + stop asking. + + + **Only jobs run on the serverless platform** (a + + `{deployment}.run.comfy.app` host) have one today. An implementation + + that captures no logs must still serve this operation, answering + + `204` for every job it can read, so that the two answers stay + + distinct — Comfy Cloud does. A self-hosted deployment on a build + + predating this operation has not implemented it yet and will answer + + a routing `404` instead, which is the case `job.urls.logs` exists to + + keep a client out of: its absence says the surface has no logs at + + all, without a request. + + + Tied to the job''s own retention: this `404`s under the same + + conditions `GET /api/v2/jobs/{id}` does (unknown, not-yours, or past + + its retention deadline). Nothing ages a log out ahead of the job''s + + own `expires_at`, so a job never outlives its log. + + + Live tailing is not offered here yet. When it is, it arrives on this + + same path under `Accept: text/event-stream`, leaving this + + JSON snapshot the default; its resume semantics will be defined + + then, against a capture that is incremental. Until then the SSE + + `log` event on `GET /api/v2/jobs/{id}/events` is the reserved live + + rail, and this is the authoritative snapshot it reconciles against. + + ' + parameters: + - $ref: '#/components/parameters/JobId' + responses: + '200': + description: The captured log. + content: + application/json: + schema: + $ref: '#/components/schemas/JobLogs' + '204': + description: This job has no log. A normal answer, not an error — see the description for the cases it covers. + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/RateLimited' + '500': + $ref: '#/components/responses/UpstreamError' /api/v2/jobs/{id}/events: get: operationId: getJobEvents @@ -528,7 +686,9 @@ paths: `progress`, and the most recent `preview` if any), then future - updates. The stream ends after the terminal `status` event. + updates. The stream ends after either the terminal `status` event + + or a terminal `error` event (see the `error` event below). Live push only — NOT a replayable log: events carry no `id`, there @@ -556,9 +716,15 @@ paths: description: 'Emitted the moment each output asset is committed, carrying the same `Output` object that appears on `job.outputs[]`. A latency optimization only: it lets a client render each result as it lands instead of waiting for the terminal `status` event. It is delivered best-effort over the live broadcast path — an output whose durable asset record is not yet resolvable when its node finishes may be delivered on a slightly later event or, failing that, only in the terminal `status` snapshot — so the authoritative, complete set of outputs is always `job.outputs[]` on `GET /api/v2/jobs/{id}` and on the terminal `status` event. A client must therefore treat these as additive hints and must not assume it receives one per output.' schema: '#/components/schemas/Output' log: - description: Selected execution log lines. Best-effort diagnostics; the one event type with no snapshot equivalent. NOT YET EMITTED by the server in the first iteration — reserved in the catalog so the wire contract is stable. Clients must not depend on receiving this event yet. + description: 'Selected execution log lines, carried while the run is still going. Best-effort diagnostics, and lossy by the same rule as the rest of this stream: lines emitted while a client was disconnected are gone and no `Last-Event-ID` replays them. The authoritative, complete log is the snapshot at `GET /api/v2/jobs/{id}/logs`, which a client re-reads after a terminal status to reconcile whatever it missed — on a surface that captures logs at all. Comfy Cloud does not, and answers `204` there for every job, so this event has nothing to be the live view of; see that operation for what a self-hosted deployment answers. NOT YET EMITTED by the server in the first iteration — reserved in the catalog so the wire contract is stable. Clients must not depend on receiving this event yet: to get a log today, stream to a terminal status and read the snapshot.' x-sse-not-yet-emitted: true schema: '#/components/schemas/LogEvent' + error: + description: 'Terminal event sent when the stream ends for a reason OTHER than the job reaching a terminal state — the credential the stream was opened with stopped being accepted (`credential_expired`, the common case for a short-lived Cloud JWT or OAuth access token that expires while a long job is still running), or the job stopped being accessible (`job_not_found`, `forbidden`). Transient upstream failures (5xx, network errors) do NOT end the stream — they are retried on the next poll. No `status` event follows it. Its whole purpose is to make an aborted stream distinguishable from a completed one: without it both simply close, and a client cannot tell "your job finished" from "you were cut off". On `credential_expired` the job itself is unaffected — refresh the credential and reopen the stream, or fall back to `GET /api/v2/jobs/{id}`. A serverless deployment sends only `job_not_found`. + + + Browser note: a browser `EventSource` also fires a BUILT-IN `error` event on any transport failure, and a server-sent `event: error` frame is delivered to the same listener. The two are told apart by the payload — this event always carries a JSON `ErrorEnvelope` in `data`, the built-in one carries none — so a handler should check for `data` before treating an `error` as an explained, terminal end-of-stream rather than a retryable connection drop.' + schema: '#/components/schemas/ErrorEnvelope' parameters: - $ref: '#/components/parameters/JobId' responses: @@ -576,7 +742,7 @@ paths: '404': $ref: '#/components/responses/NotFound' '429': - description: '`too_many_streams` — the caller already has the maximum number of concurrent GET .../events streams open. Close an existing stream (or wait for one to reach a terminal status) before opening another; GET /api/v2/jobs/{id} remains available as a plain poll regardless of this limit.' + description: '`too_many_streams` — the caller already has the maximum number of concurrent GET .../events streams open. Close an existing stream (or wait for one to reach a terminal status) before opening another; GET /api/v2/jobs/{id} remains available as a plain poll regardless of this limit. Or `rate_limited`, the caller past a request rate limit; retry after `Retry-After`.' headers: Retry-After: $ref: '#/components/headers/RetryAfter' @@ -600,7 +766,9 @@ paths: summary: Request cancellation description: 'Requests cancellation and returns the current job object — - `canceling` (interruption takes effect at node/step boundaries) or + `canceling` (interruption takes effect at node/step boundaries), + + `canceled` where the provider''s record already shows the interrupt, or already-terminal. Idempotent: canceling a finished job is a no-op @@ -634,7 +802,7 @@ components: bearerAuth: type: http scheme: bearer - description: '`Authorization: Bearer ` — account-scoped API keys on Cloud and serverless. Self-hosted accepts unauthenticated requests by default and can be configured with a static bearer token.' + description: '`Authorization: Bearer `. The credential is one of: an account-scoped API key (`comfyui-…`), accepted on Cloud and serverless; a Comfy Cloud session JWT; or an OAuth access token issued for the Comfy Cloud resource. Which kinds a given deployment accepts is deployment configuration — an API key always works on Cloud and serverless, and a deployment that does not accept JWT bearers answers `401` with a message saying so. Self-hosted accepts unauthenticated requests by default and can be configured with a static bearer token.' parameters: IdempotencyKey: name: Idempotency-Key @@ -696,7 +864,7 @@ components: schema: $ref: '#/components/schemas/ErrorEnvelope' RateLimited: - description: '`rate_limited` — the caller has exceeded the request rate limit for this account. Account/rate-scoped, not job-specific — this can be returned even for a job id the caller doesn''t own or that doesn''t exist, without revealing which.' + description: '`rate_limited` — the caller has exceeded a request rate limit for this account. Account/rate-scoped, not resource-specific — this can be returned even for a job, asset or deployment the caller doesn''t own or that doesn''t exist.' headers: Retry-After: $ref: '#/components/headers/RetryAfter' @@ -821,6 +989,36 @@ components: execution_ms: 42000 urls: $ref: '#/components/schemas/JobUrls' + deployment_id: + type: string + description: 'The deployment the job was sent to: the id in the address it was posted at, which stays the same when the deployment moves to another release. Absent on a surface that runs jobs on no deployment.' + example: dep-0f19a2b3c4d5 + release_id: + type: string + description: The release of the deployment's build that ran the job, which can differ from the release the deployment runs now. Absent where the serving surface does not report it. + example: 7c1e9a40-3b2d-4f6a-9e81-0c5d2a7b4f13 + JobLogs: + type: object + description: 'A job''s captured execution log — the body of `GET /api/v2/jobs/{id}/logs`. Diagnostics, not a contract on content: this is whatever the workflow''s own code and nodes wrote to standard output, in the order they wrote it, so nothing about its shape is stable between runs or between releases of a build. It is **untrusted text** — a workflow chooses what goes in it — and must be rendered as plain text rather than interpreted.' + required: + - text + - truncated + - captured_at + - complete + properties: + text: + type: string + description: The captured output. + truncated: + type: boolean + description: 'The BEGINNING of the captured output was discarded — `text` is the TAIL of a longer run. Implementations bound what they capture and store, so a workflow that prints megabytes keeps its last lines, where a failure normally is, instead of being dropped whole. True with an empty `text` means the log was captured and then shed entirely to fit. This describes the stored log, never the response: it does not mean a caller asked for part of one.' + captured_at: + type: string + format: date-time + description: When the run's output was read back off the worker. + complete: + type: boolean + description: No further output will be appended to this log. Always `true` today, because a log is read back off the worker once, when the run ends, so a log that exists is already whole. Sent so that a surface which later captures output while a run is still going can say so, and a client written now against `false` keeps working when it does. `false` does not promise that more output will arrive, only that this snapshot may not be the last one. JobWorkflowResponse: type: object description: The workflow behind a job. See GET /api/v2/jobs/{id}/workflow's description for exactly when `format` is `save` vs `api`. @@ -850,7 +1048,9 @@ components: - expired description: 'Lifecycle: queued → running → succeeded | failed | expired; - a cancel request moves running → canceling → canceled. + a cancel request, or the deletion of the deployment the job is running + + on, moves running → canceling → canceled. Terminal states: succeeded, canceled, failed, expired. @@ -872,6 +1072,12 @@ components: cancel: type: string format: uri-reference + logs: + type: string + format: uri-reference + description: 'Where to read what this run printed. Present on any surface that captures execution logs, which is why it is the one link here that is optional: absent means this surface captures none, for any job, so a client can stop looking without spending a request on an answer it already has. + + Follow this link rather than building the path from the job id. The two are not interchangeable: a surface may be mounted under a prefix this link already carries and a hand-built path would not, and a surface that does not implement the operation at all answers a routing `404` — indistinguishable, to the client, from the `404` that means the job itself is gone. Present does NOT mean this job has a log, and it is deliberately not a signal about one: a surface that captures logs offers the link on every job, including those it will answer `204` for and those whose log it withholds. Read the log, not the link.' Progress: type: object description: Server-computed progress snapshot (node-count and sampler-step weighted). Complete per snapshot — one fully re-syncs a client. @@ -929,6 +1135,7 @@ components: properties: node_id: type: string + description: The workflow node that reported this file; empty when the worker named none. example: '9' name: type: string @@ -982,6 +1189,7 @@ components: example: node_execution_error message: type: string + description: Why the job failed, written for a person to read. On a serverless deployment, where ComfyUI refused the workflow, it names the rejected nodes it has room for and their reasons, which `node_errors` carries in full. Its wording may change; read `code` and `node_errors` rather than matching this text. node_id: type: string nullable: true @@ -991,6 +1199,45 @@ components: traceback: type: string nullable: true + node_errors: + type: object + description: 'Every node ComfyUI rejected when it refused the workflow before running any of it (a value outside its allowed range, a model the deployment does not contain, a graph that loops back on itself), keyed by node id, under the names ComfyUI gave them. Absent when the workflow ran and a node raised: `node_id`, `class_type` and `traceback` describe that failure instead.' + additionalProperties: + $ref: '#/components/schemas/JobNodeError' + example: + '22': + class_type: LoraLoader + errors: + - type: value_not_in_list + message: Value not in list + details: 'lora_name: ''sdxl\Hyper-SDXL-8steps-lora.safetensors'' not in (list of length 40)' + JobNodeError: + type: object + description: 'One node that was rejected, and why: by ComfyUI when it refused the workflow after dispatch, or by the gateway when it refused the workflow at submit.' + required: + - errors + properties: + class_type: + type: string + errors: + type: array + items: + $ref: '#/components/schemas/JobNodeErrorReason' + JobNodeErrorReason: + type: object + description: 'One problem found with a node. `type` is the code for it: ComfyUI''s (for example `value_not_in_list`, `value_bigger_than_max`, `dependency_cycle`), or the gateway''s `unknown_node_class` (a node class the deployment''s build does not contain). ComfyUI''s `missing_node_type` means the same as `unknown_node_class`, found after dispatch rather than at submit. For either, `message` ends naming the node pack that provides the class where the Comfy node registry knows one. `details` usually starts with the input it is about.' + required: + - type + - message + properties: + type: + type: string + example: value_not_in_list + message: + type: string + example: Value not in list + details: + type: string ErrorEnvelope: type: object description: 'Shared error envelope with machine-readable codes. Core codes (v1): @@ -1001,17 +1248,27 @@ components: (404), `idempotency_key_reuse` (422), - `queue_full` (429 + Retry-After), `insufficient_credits` (402), + `queue_full` (429 + Retry-After), `rate_limited` (429 + Retry-After: + + the caller is past a request rate limit; retry), `insufficient_credits` - `not_found` (404), `unauthorized` (401), `forbidden` (403). + (402), `not_found` (404), `unauthorized` (401), `forbidden` (403). Deployment-scoped surfaces add: `deployment_not_ready` (429 + - Retry-After — the deployment can still reach ready; retry) and + Retry-After — the deployment can still reach ready; retry), + + `deployment_unavailable` (429 + Retry-After: the deployment is ready + + but its GPU provider is not taking work on it yet; retry), `deployment_stopped` (422 — terminal deployment state; a retry - cannot succeed without operator action). A 429 is disambiguated + cannot succeed without operator action), `invalid_request` (422: + + a malformed asset upload or asset-from-hash field) and `content_blocked` (451: + + content moderation flagged the asset''s bytes). A 429 is disambiguated by `error.code` alone; clients should treat any 429 + Retry-After @@ -1037,11 +1294,16 @@ components: type: object nullable: true additionalProperties: true + description: Machine-readable detail for the code. When it carries `node_errors`, that is keyed by node id and each value is a `JobNodeError`, the same shape as a job's `error.node_errors`, whether the refusal came at submit (for example `unknown_node_class`, a node class the deployment's build does not contain) or from ComfyUI after dispatch. example: node_errors: '12': - - field: model - reason: missing_input + class_type: SomeCustomNode + errors: + - type: unknown_node_class + message: this deployment's build does not contain the node class SomeCustomNode. + unknown_node_classes: + - SomeCustomNode StatusEvent: type: object description: SSE `status` event payload. diff --git a/src/comfy_low/models/_generated.py b/src/comfy_low/models/_generated.py index c188060..8b7b1fc 100644 --- a/src/comfy_low/models/_generated.py +++ b/src/comfy_low/models/_generated.py @@ -49,6 +49,30 @@ class Asset(BaseModel): ] = None +class JobLogs(BaseModel): + """ + A job's captured execution log — the body of `GET /api/v2/jobs/{id}/logs`. Diagnostics, not a contract on content: this is whatever the workflow's own code and nodes wrote to standard output, in the order they wrote it, so nothing about its shape is stable between runs or between releases of a build. It is **untrusted text** — a workflow chooses what goes in it — and must be rendered as plain text rather than interpreted. + """ + + text: Annotated[str, Field(description='The captured output.')] + truncated: Annotated[ + bool, + Field( + description='The BEGINNING of the captured output was discarded — `text` is the TAIL of a longer run. Implementations bound what they capture and store, so a workflow that prints megabytes keeps its last lines, where a failure normally is, instead of being dropped whole. True with an empty `text` means the log was captured and then shed entirely to fit. This describes the stored log, never the response: it does not mean a caller asked for part of one.' + ), + ] + captured_at: Annotated[ + AwareDatetime, + Field(description="When the run's output was read back off the worker."), + ] + complete: Annotated[ + bool, + Field( + description='No further output will be appended to this log. Always `true` today, because a log is read back off the worker once, when the run ends, so a log that exists is already whole. Sent so that a surface which later captures output while a run is still going can say so, and a client written now against `false` keeps working when it does. `false` does not promise that more output will arrive, only that this snapshot may not be the last one.' + ), + ] + + class Format(Enum): """ Discriminates the `workflow` field's shape. `save`: the original authoring workflow JSON, at the version pinned to the job. `api`: the executed API-format prompt graph. @@ -78,7 +102,8 @@ class JobWorkflowResponse(BaseModel): class JobStatus(Enum): """ Lifecycle: queued → running → succeeded | failed | expired; - a cancel request moves running → canceling → canceled. + a cancel request, or the deletion of the deployment the job is running + on, moves running → canceling → canceled. Terminal states: succeeded, canceled, failed, expired. """ @@ -100,6 +125,12 @@ class JobUrls(BaseModel): self: str events: str cancel: str + logs: Annotated[ + str | None, + Field( + description='Where to read what this run printed. Present on any surface that captures execution logs, which is why it is the one link here that is optional: absent means this surface captures none, for any job, so a client can stop looking without spending a request on an answer it already has.\nFollow this link rather than building the path from the job id. The two are not interchangeable: a surface may be mounted under a prefix this link already carries and a hand-built path would not, and a surface that does not implement the operation at all answers a routing `404` — indistinguishable, to the client, from the `404` that means the job itself is gone. Present does NOT mean this job has a log, and it is deliberately not a signal about one: a surface that captures logs offers the link on every job, including those it will answer `204` for and those whose log it withholds. Read the log, not the link.' + ), + ] = None class Progress(BaseModel): @@ -138,16 +169,14 @@ class OutputType(Enum): latent = 'latent' -class JobError(BaseModel): +class JobNodeErrorReason(BaseModel): """ - Execution failure detail, carried in `job.error` (not an HTTP error). + One problem found with a node. `type` is the code for it: ComfyUI's (for example `value_not_in_list`, `value_bigger_than_max`, `dependency_cycle`), or the gateway's `unknown_node_class` (a node class the deployment's build does not contain). ComfyUI's `missing_node_type` means the same as `unknown_node_class`, found after dispatch rather than at submit. For either, `message` ends naming the node pack that provides the class where the Comfy node registry knows one. `details` usually starts with the input it is about. """ - code: Annotated[str, Field(examples=['node_execution_error'])] - message: str - node_id: str | None = None - class_type: str | None = None - traceback: str | None = None + type: Annotated[str, Field(examples=['value_not_in_list'])] + message: Annotated[str, Field(examples=['Value not in list'])] + details: str | None = None class Error(BaseModel): @@ -159,9 +188,23 @@ class Error(BaseModel): details: Annotated[ dict[str, Any] | None, Field( + description="Machine-readable detail for the code. When it carries `node_errors`, that is keyed by node id and each value is a `JobNodeError`, the same shape as a job's `error.node_errors`, whether the refusal came at submit (for example `unknown_node_class`, a node class the deployment's build does not contain) or from ComfyUI after dispatch.", examples=[ - {'node_errors': {'12': [{'field': 'model', 'reason': 'missing_input'}]}} - ] + { + 'node_errors': { + '12': { + 'class_type': 'SomeCustomNode', + 'errors': [ + { + 'type': 'unknown_node_class', + 'message': "this deployment's build does not contain the node class SomeCustomNode.", + } + ], + } + }, + 'unknown_node_classes': ['SomeCustomNode'], + } + ], ), ] = None @@ -172,12 +215,17 @@ class ErrorEnvelope(BaseModel): `invalid_workflow` (422), `workflow_format_ui` (422), `missing_asset` (422), `hash_mismatch` (409), `blob_not_found` (404), `idempotency_key_reuse` (422), - `queue_full` (429 + Retry-After), `insufficient_credits` (402), - `not_found` (404), `unauthorized` (401), `forbidden` (403). + `queue_full` (429 + Retry-After), `rate_limited` (429 + Retry-After: + the caller is past a request rate limit; retry), `insufficient_credits` + (402), `not_found` (404), `unauthorized` (401), `forbidden` (403). Deployment-scoped surfaces add: `deployment_not_ready` (429 + - Retry-After — the deployment can still reach ready; retry) and + Retry-After — the deployment can still reach ready; retry), + `deployment_unavailable` (429 + Retry-After: the deployment is ready + but its GPU provider is not taking work on it yet; retry), `deployment_stopped` (422 — terminal deployment state; a retry - cannot succeed without operator action). A 429 is disambiguated + cannot succeed without operator action), `invalid_request` (422: + a malformed asset upload or asset-from-hash field) and `content_blocked` (451: + content moderation flagged the asset's bytes). A 429 is disambiguated by `error.code` alone; clients should treat any 429 + Retry-After as "back off and retry". @@ -251,7 +299,13 @@ class Output(BaseModel): A committed job output. Outputs are assets: `id` is the asset UUID, retrievable via GET /api/v2/assets/{id} for as long as the job is retained. `hash` is lazily computed and may be null on the retrieval hot path. """ - node_id: Annotated[str, Field(examples=['9'])] + node_id: Annotated[ + str, + Field( + description='The workflow node that reported this file; empty when the worker named none.', + examples=['9'], + ), + ] name: Annotated[str, Field(examples=['ComfyUI_00001_.png'])] type: OutputType content_type: Annotated[str, Field(examples=['image/png'])] @@ -269,6 +323,52 @@ class Output(BaseModel): ] = None +class JobNodeError(BaseModel): + """ + One node that was rejected, and why: by ComfyUI when it refused the workflow after dispatch, or by the gateway when it refused the workflow at submit. + """ + + class_type: str | None = None + errors: list[JobNodeErrorReason] + + +class JobError(BaseModel): + """ + Execution failure detail, carried in `job.error` (not an HTTP error). + """ + + code: Annotated[str, Field(examples=['node_execution_error'])] + message: Annotated[ + str, + Field( + description='Why the job failed, written for a person to read. On a serverless deployment, where ComfyUI refused the workflow, it names the rejected nodes it has room for and their reasons, which `node_errors` carries in full. Its wording may change; read `code` and `node_errors` rather than matching this text.' + ), + ] + node_id: str | None = None + class_type: str | None = None + traceback: str | None = None + node_errors: Annotated[ + dict[str, JobNodeError] | None, + Field( + description='Every node ComfyUI rejected when it refused the workflow before running any of it (a value outside its allowed range, a model the deployment does not contain, a graph that loops back on itself), keyed by node id, under the names ComfyUI gave them. Absent when the workflow ran and a node raised: `node_id`, `class_type` and `traceback` describe that failure instead.', + examples=[ + { + '22': { + 'class_type': 'LoraLoader', + 'errors': [ + { + 'type': 'value_not_in_list', + 'message': 'Value not in list', + 'details': "lora_name: 'sdxl\\Hyper-SDXL-8steps-lora.safetensors' not in (list of length 40)", + } + ], + } + } + ], + ), + ] = None + + class Job(BaseModel): """ One execution of a workflow. Durable from creation until `expires_at`; `outputs` populates incrementally during execution. @@ -302,3 +402,17 @@ class Job(BaseModel): ), ] = None urls: JobUrls + deployment_id: Annotated[ + str | None, + Field( + description='The deployment the job was sent to: the id in the address it was posted at, which stays the same when the deployment moves to another release. Absent on a surface that runs jobs on no deployment.', + examples=['dep-0f19a2b3c4d5'], + ), + ] = None + release_id: Annotated[ + str | None, + Field( + description="The release of the deployment's build that ran the job, which can differ from the release the deployment runs now. Absent where the serving surface does not report it.", + examples=['7c1e9a40-3b2d-4f6a-9e81-0c5d2a7b4f13'], + ), + ] = None