@civitai/sdk 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BREAKING.md +1 -1
- package/README.md +269 -5
- package/dist/app/index.d.ts +15 -1
- package/dist/app/index.js +44 -10
- package/dist/core/transports/iframe-transport.js +4 -1
- package/dist/http/retry.d.ts +3 -0
- package/dist/http/retry.js +14 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1 -0
- package/dist/orchestration/index.d.ts +1 -1
- package/dist/orchestration/index.js +1 -14
- package/dist/version.generated.d.ts +2 -0
- package/dist/version.generated.js +3 -0
- package/dist/workflows/index.d.ts +181 -0
- package/dist/workflows/index.js +149 -0
- package/dist/workflows/types.generated.d.ts +1703 -0
- package/dist/workflows/types.generated.js +7 -0
- package/package.json +4 -2
|
@@ -0,0 +1,1703 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A Civitai-hosted source image for an img2img generation. Mirrors civitai's
|
|
3
|
+
* `blockSourceImageSchema` (`{ url, width, height }` — all three REQUIRED), the
|
|
4
|
+
* element type of BOTH `blockTextToImageBodySchema.sourceImages` (current) and
|
|
5
|
+
* its deprecated singular alias `.sourceImage`.
|
|
6
|
+
*
|
|
7
|
+
* `url` MUST resolve to a Civitai-controlled host — the server rejects an
|
|
8
|
+
* arbitrary remote URL (SSRF / arbitrary-fetch). An image obtained from
|
|
9
|
+
* {@link BlockUploadedImageInfo.url} (via `useImageUpload`) satisfies this.
|
|
10
|
+
* `width`/`height` are the intrinsic dimensions the graph uses for its
|
|
11
|
+
* denoise/aspect derivation; the server bounds each to 64–2048.
|
|
12
|
+
*
|
|
13
|
+
* In the array form EVERY element is validated individually against exactly
|
|
14
|
+
* these rules — there is no "first element only" shortcut server-side.
|
|
15
|
+
*/
|
|
16
|
+
export interface BlockSourceImage {
|
|
17
|
+
url: string;
|
|
18
|
+
width: number;
|
|
19
|
+
height: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The Buzz currency pools a Civitai App may spend from / read a balance for.
|
|
23
|
+
*
|
|
24
|
+
* DELIBERATELY narrow — the domain-clamped set the Civitai Apps host honors:
|
|
25
|
+
* `blue` (free/earned), `green` (creator-compensation pool), and `yellow`
|
|
26
|
+
* (purchased). It intentionally omits the platform-internal pools (`red`,
|
|
27
|
+
* `purple`, …) that appear in the broader `@civitai/app-sdk/oauth`
|
|
28
|
+
* `BuzzAccountType` — a block can neither prefer nor read those. Keep this in
|
|
29
|
+
* lockstep with civitai/civitai's block submit-body schema clamp.
|
|
30
|
+
*/
|
|
31
|
+
export type BuzzAccountType = 'blue' | 'green' | 'yellow';
|
|
32
|
+
/**
|
|
33
|
+
* Generation parameters a block can override. All optional — the host fills
|
|
34
|
+
* sensible defaults (sampler='Euler', steps=25, dimensions from the
|
|
35
|
+
* base-model family) when omitted, so the simplest block can submit
|
|
36
|
+
* `{ kind: 'textToImage', modelId, modelVersionId, params: { prompt } }`.
|
|
37
|
+
*
|
|
38
|
+
* Bounds mirror civitai/civitai's `blockWorkflowBodySchema` zod gate; over-
|
|
39
|
+
* limit values are rejected server-side before reaching the orchestrator.
|
|
40
|
+
*/
|
|
41
|
+
export interface BlockTextToImageParams {
|
|
42
|
+
prompt: string;
|
|
43
|
+
negativePrompt?: string;
|
|
44
|
+
/** Range 1–30. */
|
|
45
|
+
cfgScale?: number;
|
|
46
|
+
/** Sampler name (e.g. 'Euler', 'DPM++ 2M Karras'). Defaults to 'Euler'. */
|
|
47
|
+
sampler?: string;
|
|
48
|
+
/** Range 1–50. */
|
|
49
|
+
steps?: number;
|
|
50
|
+
/** `null` lets the orchestrator pick. */
|
|
51
|
+
seed?: number | null;
|
|
52
|
+
/** Range 64–2048. Defaults to 1024 for SDXL/Flux, 512 for SD1/SD2. */
|
|
53
|
+
width?: number;
|
|
54
|
+
/** Range 64–2048. Same defaults as width. */
|
|
55
|
+
height?: number;
|
|
56
|
+
/** Per-resource CLIP layer skip count (SD1/SDXL). Range 0–12. Flux ignores it. */
|
|
57
|
+
clipSkip?: number;
|
|
58
|
+
/** Range 1–4. Defaults to 1. */
|
|
59
|
+
quantity?: number;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The text-to-image member of the {@link WorkflowBody} discriminated union
|
|
63
|
+
* (`kind: 'textToImage'`). This is the original, single-member shape — kept
|
|
64
|
+
* byte-identical for backward compatibility. An existing
|
|
65
|
+
* `{ kind: 'textToImage', modelId, modelVersionId, params }` body must
|
|
66
|
+
* continue to satisfy {@link WorkflowBody} unchanged.
|
|
67
|
+
*
|
|
68
|
+
* Both `modelId` and `modelVersionId` are required even though they're
|
|
69
|
+
* conceptually redundant — the host validates that `modelId` matches the
|
|
70
|
+
* JWT's `ctx.modelId` (context binding) AND that the version belongs to
|
|
71
|
+
* that model (DB lookup). The block always has both values from
|
|
72
|
+
* `useBlockContext().context as ModelSlotContext`.
|
|
73
|
+
*/
|
|
74
|
+
export type WorkflowBodyTextToImage = {
|
|
75
|
+
kind: 'textToImage';
|
|
76
|
+
modelId: number;
|
|
77
|
+
modelVersionId: number;
|
|
78
|
+
/**
|
|
79
|
+
* Optional additional generation resources (LoRAs) layered on top of the
|
|
80
|
+
* checkpoint (`modelVersionId`). Mirrors civitai's `blockWorkflowBodySchema`:
|
|
81
|
+
* - max 5 entries
|
|
82
|
+
* - each: { modelVersionId: positive int, strength?: number in [-1, 2], default 1 }
|
|
83
|
+
* - the server is LoRA-only for additional resources (non-LoRA versions are
|
|
84
|
+
* rejected) and enforces base-model-family compatibility with the checkpoint
|
|
85
|
+
* + per-resource entitlement (early-access/Private) before any Buzz spend.
|
|
86
|
+
* Omit for a checkpoint-only generation (backward compatible).
|
|
87
|
+
*/
|
|
88
|
+
additionalResources?: Array<{
|
|
89
|
+
modelVersionId: number;
|
|
90
|
+
strength?: number;
|
|
91
|
+
}>;
|
|
92
|
+
/**
|
|
93
|
+
* Optional img2img init/source image (App Blocks IMAGE bridge). When present,
|
|
94
|
+
* the block bridge emits an img2img graph workflow instead of `txt2img`; omit
|
|
95
|
+
* for a plain text-to-image generation (backward compatible). Constraints
|
|
96
|
+
* (all SERVER-ENFORCED — mirrors civitai's `blockTextToImageBodySchema`):
|
|
97
|
+
* - `url` must be a Civitai-hosted https image (an uploaded image from
|
|
98
|
+
* {@link BlockUploadedImageInfo.url} qualifies) — never an arbitrary remote URL.
|
|
99
|
+
* - The checkpoint's ecosystem must support an img2img variant — SD-family →
|
|
100
|
+
* `img2img`, edit-capable (OpenAI / Qwen / Flux Kontext / …) →
|
|
101
|
+
* `img2img:edit`. An ecosystem supporting NEITHER is rejected fail-closed.
|
|
102
|
+
* - PAGE apps only — the server rejects source images on a model-bound token.
|
|
103
|
+
*
|
|
104
|
+
* @deprecated Send {@link WorkflowBodyTextToImage.sourceImages} instead — a
|
|
105
|
+
* 1-element `sourceImages` array produces a byte-identical generation (the
|
|
106
|
+
* server normalizes this singular field into a 1-element array and everything
|
|
107
|
+
* downstream sees only arrays). This alias keeps working INDEFINITELY — every
|
|
108
|
+
* deployed block and the published developer docs ship it — so nothing breaks
|
|
109
|
+
* by leaving it; it just cannot express more than one image. Sending BOTH
|
|
110
|
+
* fields is rejected as ambiguous.
|
|
111
|
+
*/
|
|
112
|
+
sourceImage?: BlockSourceImage;
|
|
113
|
+
/**
|
|
114
|
+
* Optional img2img/edit source images — multi-image conditioning (App Blocks
|
|
115
|
+
* IMAGE bridge). The current field; supersedes the deprecated singular
|
|
116
|
+
* {@link WorkflowBodyTextToImage.sourceImage}. Omit BOTH for a plain
|
|
117
|
+
* text-to-image generation (backward compatible).
|
|
118
|
+
*
|
|
119
|
+
* Constraints (all SERVER-ENFORCED — mirrors civitai's
|
|
120
|
+
* `blockTextToImageBodySchema.sourceImages`, which is
|
|
121
|
+
* `z.array(blockSourceImageSchema).min(1).max(BLOCK_SOURCE_IMAGES_WIRE_MAX)`):
|
|
122
|
+
* - **Every element is validated individually** — each `url` must be a
|
|
123
|
+
* Civitai-hosted **https** image (an image from
|
|
124
|
+
* {@link BlockUploadedImageInfo.url}, or a
|
|
125
|
+
* {@link BlockGenerationSourceImageInfo} from a `'generationSource'`
|
|
126
|
+
* upload, qualifies) and each `width`/`height` must be within 64–2048.
|
|
127
|
+
* A bad element ANYWHERE rejects the whole body — there is no
|
|
128
|
+
* "first element only" path.
|
|
129
|
+
* - **The maximum count is PER-ECOSYSTEM, not a constant.** It is derived
|
|
130
|
+
* from the checkpoint's own generation-graph `images` node, so it tracks
|
|
131
|
+
* what the ecosystem actually supports: SD-family / Flux.1 Kontext / Boogu
|
|
132
|
+
* / MAI **1**; Qwen / Qwen2 / MageFlow **3**; Reve / HiDream-O1 **4**;
|
|
133
|
+
* WanImage **5**; Flux.2 / Flux.2 Klein / OpenAI / NanoBanana / Seedream /
|
|
134
|
+
* Grok **7**. Exceeding the checkpoint's cap is REJECTED (never silently
|
|
135
|
+
* truncated), and the error names both the count sent and the ecosystem's
|
|
136
|
+
* limit. A flat wire bound of 10 additionally rejects an oversized array
|
|
137
|
+
* before the body is parsed — it is NOT the product cap.
|
|
138
|
+
* - **An empty array is REJECTED** (`.min(1)`); it is NOT treated as "no
|
|
139
|
+
* source image". Omit the field entirely for text-to-image.
|
|
140
|
+
* - **PAGE apps only** — the server rejects source images on a model-bound
|
|
141
|
+
* token, for this array form as well as the singular one.
|
|
142
|
+
* - Sending **both** `sourceImage` and `sourceImages` is REJECTED as
|
|
143
|
+
* ambiguous rather than resolved to a winner. TypeScript cannot express
|
|
144
|
+
* that mutual exclusion here (both are independently optional on this
|
|
145
|
+
* member), so it surfaces as a server-side validation error — send exactly
|
|
146
|
+
* one.
|
|
147
|
+
* - Element ORDER is preserved into the graph's `images` input.
|
|
148
|
+
*
|
|
149
|
+
* 🔴 Requires a host running civitai/civitai#3518 or later. The text-to-image
|
|
150
|
+
* body schema is NOT `.strict()`, so a host predating #3518 does not error on
|
|
151
|
+
* this field — it SILENTLY STRIPS it and runs (and bills) a plain
|
|
152
|
+
* text-to-image generation with no image conditioning at all. There is no
|
|
153
|
+
* client-side way to detect that, so until #3518 is deployed everywhere you
|
|
154
|
+
* target, send the singular `sourceImage` (which works on both).
|
|
155
|
+
*/
|
|
156
|
+
sourceImages?: BlockSourceImage[];
|
|
157
|
+
/**
|
|
158
|
+
* Optional shared-storage key of the published content this generation runs on
|
|
159
|
+
* behalf of. The server resolves it to the content's author for attribution
|
|
160
|
+
* (see the G5 civitai PR). Opaque string — the block passes back the `key` it
|
|
161
|
+
* got from `useSharedStorage()` for the content being generated against; omit
|
|
162
|
+
* when not applicable.
|
|
163
|
+
*/
|
|
164
|
+
sharedContentKey?: string;
|
|
165
|
+
/**
|
|
166
|
+
* Optional preferred Buzz pool to fund this generation from — a *preference*,
|
|
167
|
+
* not a guarantee. The host clamps it server-side to what the viewer actually
|
|
168
|
+
* holds and to the domain-allowed pools (a `blockWorkflowBodySchema` field on
|
|
169
|
+
* civitai/civitai; preferred-first, then falls back). Omit for today's default
|
|
170
|
+
* host-chosen funding order (backward compatible). Whichever pool ended up the
|
|
171
|
+
* primary funder is echoed back on {@link BlockWorkflowSnapshot.spentAccountType}.
|
|
172
|
+
*/
|
|
173
|
+
accountType?: BuzzAccountType;
|
|
174
|
+
params: BlockTextToImageParams;
|
|
175
|
+
};
|
|
176
|
+
/**
|
|
177
|
+
* The RECIPE arm of {@link WorkflowBodyCustomComfy} — runs a
|
|
178
|
+
* **server-registered, code-reviewed ComfyUI recipe** end-to-end. The block
|
|
179
|
+
* sends only a registered `recipe` id plus a small, per-recipe-validated
|
|
180
|
+
* `params` object; the civitai server owns the entire graph (built by object
|
|
181
|
+
* construction, so the prompt is a leaf value that cannot perturb graph
|
|
182
|
+
* topology).
|
|
183
|
+
*
|
|
184
|
+
* This is the DEFAULT arm. `mode` is optional here and a body that omits it
|
|
185
|
+
* lands on this arm, so every body written before the inline arm existed is
|
|
186
|
+
* unchanged and keeps working byte-for-byte.
|
|
187
|
+
*
|
|
188
|
+
* Trust / safety model (all SERVER-ENFORCED — mirrors civitai's
|
|
189
|
+
* `blockCustomComfyBodySchema`):
|
|
190
|
+
* - `recipe` is a **registered recipe id** resolved against a code-reviewed,
|
|
191
|
+
* in-repo recipe registry. An unknown id is **rejected server-side, fail-
|
|
192
|
+
* closed** (the schema enum is derived from the registry keys).
|
|
193
|
+
* - `params` are **bounded and validated per-recipe** by the server's
|
|
194
|
+
* `.strict()` Zod param schema (extra fields rejected); each recipe pins
|
|
195
|
+
* its own resources (checkpoint/LoRA/diffusion AIRs) — the block cannot
|
|
196
|
+
* supply them.
|
|
197
|
+
*
|
|
198
|
+
* Billing is **post-paid** (the underlying orchestrator `customComfy` step
|
|
199
|
+
* bills measured GPU runtime at a fixed rate, so there is NO exact pre-price):
|
|
200
|
+
* - `estimate` returns a per-recipe **display estimate**, not a firm quote —
|
|
201
|
+
* surface it as an estimate; the actual cost is known only on terminal.
|
|
202
|
+
* - Each recipe declares a hard per-job `maxBuzz` ceiling backed by an
|
|
203
|
+
* aggressive step `timeout`; the orchestrator physically caps the job at
|
|
204
|
+
* that ceiling server-side (worst-case Buzz = the timeout in seconds), and
|
|
205
|
+
* the host gates `maxBuzz <= token.buzzBudget` before submit. A single job
|
|
206
|
+
* therefore cannot exceed the recipe's declared ceiling no matter what.
|
|
207
|
+
*/
|
|
208
|
+
export type WorkflowBodyCustomComfyRecipe = {
|
|
209
|
+
kind: 'customComfy';
|
|
210
|
+
/**
|
|
211
|
+
* The arm discriminator, OPTIONAL on this arm. Omit it (the normal case) or
|
|
212
|
+
* set it to `'recipe'`; both land here. It exists so the inline arm can be
|
|
213
|
+
* selected explicitly with `mode: 'inline'`.
|
|
214
|
+
*
|
|
215
|
+
* 🔴 The host's schema declares this `z.literal('recipe').optional()` — NOT
|
|
216
|
+
* `.default('recipe')` — precisely so a body with no `mode` key still parses
|
|
217
|
+
* as a recipe. Do not "helpfully" send `mode: undefined` as a spread from an
|
|
218
|
+
* optional variable if you can avoid it; it parses fine today, but omitting
|
|
219
|
+
* the key entirely is the shape every deployed block sends.
|
|
220
|
+
*/
|
|
221
|
+
mode?: 'recipe';
|
|
222
|
+
/**
|
|
223
|
+
* A **registered recipe id** (e.g. `'seamless-pano-360'`). Resolved server-
|
|
224
|
+
* side against the code-reviewed recipe registry; an unknown id is rejected
|
|
225
|
+
* fail-closed. The recipe fixes the graph, the resource allowlist, the
|
|
226
|
+
* checkpoint policy, and the `maxBuzz`/`timeout` budget ceiling — none of
|
|
227
|
+
* which the block can influence beyond selecting the recipe + `params`.
|
|
228
|
+
*/
|
|
229
|
+
recipe: string;
|
|
230
|
+
/**
|
|
231
|
+
* Bounded, per-recipe-validated parameters. Only the fields a recipe's
|
|
232
|
+
* `.strict()` Zod schema accepts are honored; extra fields are rejected
|
|
233
|
+
* server-side. The common shape:
|
|
234
|
+
* - `prompt` — the generation prompt (a leaf string; injected into the
|
|
235
|
+
* server-built graph by object construction, never string-templated).
|
|
236
|
+
* - `seed` — optional; `null`/omitted lets the orchestrator pick.
|
|
237
|
+
* - `engine` — optional recipe engine-variant selector (e.g. a DiT engine);
|
|
238
|
+
* the recipe defaults it when omitted.
|
|
239
|
+
* - `accountType` — optional preferred Buzz pool (a preference, clamped
|
|
240
|
+
* server-side to pools the viewer actually holds; see {@link BuzzAccountType}).
|
|
241
|
+
*/
|
|
242
|
+
params: {
|
|
243
|
+
prompt: string;
|
|
244
|
+
seed?: number | null;
|
|
245
|
+
engine?: string;
|
|
246
|
+
accountType?: BuzzAccountType;
|
|
247
|
+
};
|
|
248
|
+
};
|
|
249
|
+
/**
|
|
250
|
+
* One node of a ComfyUI `/prompt` graph, as it travels on the wire.
|
|
251
|
+
*
|
|
252
|
+
* Mirrors the host's `inlineComfyNodeSchema`, which is `.strict()` with exactly
|
|
253
|
+
* these two fields — so a node carrying anything else (a `_meta` key from a
|
|
254
|
+
* ComfyUI export, for instance) is REJECTED at the wire, not dropped. Strip
|
|
255
|
+
* extras before submitting.
|
|
256
|
+
*
|
|
257
|
+
* `inputs` values are whatever ComfyUI accepts: literals, or a
|
|
258
|
+
* `[nodeId, outputIndex]` pair wiring this input to another node's output.
|
|
259
|
+
*/
|
|
260
|
+
export type InlineComfyNode = {
|
|
261
|
+
/** The ComfyUI node class, e.g. `'KSampler'`, `'CLIPTextEncode'`. */
|
|
262
|
+
class_type: string;
|
|
263
|
+
/** The node's inputs, keyed by input name. */
|
|
264
|
+
inputs: Record<string, unknown>;
|
|
265
|
+
};
|
|
266
|
+
/**
|
|
267
|
+
* The INLINE-GRAPH arm of {@link WorkflowBodyCustomComfy} (`kind:
|
|
268
|
+
* 'customComfy'`, `mode: 'inline'`) — the block ships **the ComfyUI graph
|
|
269
|
+
* itself**, so it can run a workflow the server's recipe registry does not
|
|
270
|
+
* contain.
|
|
271
|
+
*
|
|
272
|
+
* 🔴 THIS IS LIVE IN PRODUCTION. An earlier revision of this doc comment
|
|
273
|
+
* asserted that "there is no way for a block to run an arbitrary/unreviewed
|
|
274
|
+
* graph". That is FALSE and was removed: it predates the inline arm and cost a
|
|
275
|
+
* developer a dogfooding session, who trusted it over a working feature.
|
|
276
|
+
*
|
|
277
|
+
* 🔴 IT IS NOT "APP DEVELOPERS ONLY", AND THAT CLAIM USED TO BE HERE. This
|
|
278
|
+
* block read "**App developers only** — the host runs
|
|
279
|
+
* `assertViewerIsAppDeveloper` on every `customComfy` estimate AND submit. A
|
|
280
|
+
* non-developer viewer of your published block cannot submit one." Both
|
|
281
|
+
* sentences are FALSE: NEITHER `customComfy` arm runs any app-developer check,
|
|
282
|
+
* on the estimate or on the submit. The host's own schema module records the
|
|
283
|
+
* same retraction. Do not reintroduce it, in any wording — it is a SECURITY
|
|
284
|
+
* claim, and believing it is what makes an author ship an inline graph they
|
|
285
|
+
* would not ship to every viewer. An ordinary viewer of your published block
|
|
286
|
+
* CAN reach this arm once the refusals below pass.
|
|
287
|
+
*
|
|
288
|
+
* WHO CAN USE IT — stated as the refusals the host actually runs, before either
|
|
289
|
+
* arm's body is inspected. Each is a path your block has to handle:
|
|
290
|
+
* - **Page tokens only** — a model-bound token is rejected.
|
|
291
|
+
* - The token must carry the **`ai:write:budgeted`** consent scope.
|
|
292
|
+
* - The viewer must be **signed in** — a token whose subject does not resolve
|
|
293
|
+
* is refused.
|
|
294
|
+
* - The viewer must be **enabled for Apps** (the runtime kill-switch, evaluated
|
|
295
|
+
* on the token's subject; this is a closed beta).
|
|
296
|
+
* - On **submit** only: the token must carry a **positive per-call Buzz
|
|
297
|
+
* budget**. That budget is minted from YOUR OWN manifest
|
|
298
|
+
* (`page.buzzBudgetPerGen`) — it is not a property of the viewer.
|
|
299
|
+
* A registered recipe is how you get a reviewed graph you do not have to ship in
|
|
300
|
+
* the body. It is not a way onto a surface the inline arm cannot reach.
|
|
301
|
+
*
|
|
302
|
+
* WHAT REPLACED CODE REVIEW. A recipe is reviewed in-repo, and that review was
|
|
303
|
+
* the trust root. An inline graph has none, so the host substitutes three
|
|
304
|
+
* mechanical, fail-closed gates that all run BEFORE any spend or orchestrator
|
|
305
|
+
* call (a rejection therefore costs nothing):
|
|
306
|
+
* 1. **AIR containment.** Every AIR URN appearing anywhere in the graph —
|
|
307
|
+
* including as an object KEY — must also appear in {@link resources}. The
|
|
308
|
+
* match is whole-string (trimmed, case-insensitive), so an AIR embedded in
|
|
309
|
+
* a longer string does not count as declared and the body is rejected.
|
|
310
|
+
* 2. **Entitlement**, over the declared `resources`. Stricter than the onsite
|
|
311
|
+
* generator: early-access `hasAccess` and Private/epoch subscription are
|
|
312
|
+
* both folded in, an unresolvable version id is a hard `FORBIDDEN`, and a
|
|
313
|
+
* resource the site would silently SUBSTITUTE with a sibling version is
|
|
314
|
+
* REJECTED instead — your graph names one specific AIR and nothing rewrites
|
|
315
|
+
* it.
|
|
316
|
+
* 3. **Moderation sweep.** The audit reads the declared `prompt` AND every
|
|
317
|
+
* distinct string leaf in the graph, because a real graph carries its
|
|
318
|
+
* prompts inside `CLIPTextEncode` nodes. A clean declared `prompt` cannot
|
|
319
|
+
* launder a graph prompt.
|
|
320
|
+
*
|
|
321
|
+
* @example A minimal inline body.
|
|
322
|
+
* ```ts
|
|
323
|
+
* const body: WorkflowBody = {
|
|
324
|
+
* kind: 'customComfy',
|
|
325
|
+
* mode: 'inline',
|
|
326
|
+
* resources: ['urn:air:sdxl:checkpoint:civitai:101055@128078'],
|
|
327
|
+
* workflow: {
|
|
328
|
+
* '1': {
|
|
329
|
+
* class_type: 'CheckpointLoaderSimple',
|
|
330
|
+
* inputs: { ckpt_name: 'urn:air:sdxl:checkpoint:civitai:101055@128078' },
|
|
331
|
+
* },
|
|
332
|
+
* '2': {
|
|
333
|
+
* class_type: 'CLIPTextEncode',
|
|
334
|
+
* inputs: { text: 'a mountain at dawn', clip: ['1', 1] },
|
|
335
|
+
* },
|
|
336
|
+
* // …sampler, VAE decode, SaveImage…
|
|
337
|
+
* },
|
|
338
|
+
* prompt: 'a mountain at dawn',
|
|
339
|
+
* maxBuzz: 60,
|
|
340
|
+
* };
|
|
341
|
+
* ```
|
|
342
|
+
*/
|
|
343
|
+
export type WorkflowBodyCustomComfyInline = {
|
|
344
|
+
kind: 'customComfy';
|
|
345
|
+
/**
|
|
346
|
+
* The arm discriminator. REQUIRED and exactly `'inline'` — omitting it does
|
|
347
|
+
* not "default to inline because a `workflow` is present"; it routes the body
|
|
348
|
+
* to the recipe arm, which then rejects it for a missing `recipe`.
|
|
349
|
+
*/
|
|
350
|
+
mode: 'inline';
|
|
351
|
+
/**
|
|
352
|
+
* The ComfyUI `/prompt` graph, keyed by node id (the same object shape
|
|
353
|
+
* ComfyUI's "Save (API Format)" export produces).
|
|
354
|
+
*
|
|
355
|
+
* Server-enforced bounds — all three REJECT, none truncates:
|
|
356
|
+
* - at least **1** node, at most **300**;
|
|
357
|
+
* - the serialized graph must be at most **262144 bytes** (256 KB);
|
|
358
|
+
* - nesting at most **128** levels deep (the gate walks the graph, and a
|
|
359
|
+
* graph it cannot finish walking is one it cannot prove safe).
|
|
360
|
+
*/
|
|
361
|
+
workflow: Record<string, InlineComfyNode>;
|
|
362
|
+
/**
|
|
363
|
+
* The DECLARED DOWNLOAD MANIFEST: every resource the graph needs, as an AIR
|
|
364
|
+
* URN. This is the array the entitlement gate runs over, and gate 1 above is
|
|
365
|
+
* what makes gating it sufficient.
|
|
366
|
+
*
|
|
367
|
+
* 🔴 **Declaring is not optional and it is not inferred.** Every AIR your
|
|
368
|
+
* graph names must ALSO be listed here or the submit is rejected — and
|
|
369
|
+
* anything the graph references that is missing from this list would fail at
|
|
370
|
+
* load time inside ComfyUI anyway. At most **24** entries, each at most 512
|
|
371
|
+
* characters.
|
|
372
|
+
*
|
|
373
|
+
* A `civitai`-sourced AIR MUST carry a model VERSION id
|
|
374
|
+
* (`urn:air:<ecosystem>:<type>:civitai:<modelId>@<versionId>`) — without one
|
|
375
|
+
* there is no version to check entitlement against and the body is rejected.
|
|
376
|
+
* The AIR grammar is read strictly here: all of
|
|
377
|
+
* `urn:air:<ecosystem>:<type>:<source>:<id>[@<version>]` is required, unlike
|
|
378
|
+
* the lenient parser used elsewhere.
|
|
379
|
+
*
|
|
380
|
+
* The permitted AIR `type`s are model WEIGHTS — `checkpoint`,
|
|
381
|
+
* `diffusion_model`, `unet`, `lora`, `lycoris`, `dora`, `embedding`,
|
|
382
|
+
* `hypernet`, `controlnet`, `vae`, `upscaler`, `clip`, `clipvision`,
|
|
383
|
+
* `text_encoders`, `motion` and a few siblings. An `oci:image` container AIR
|
|
384
|
+
* is NOT permitted. Anything the allowlist does not name is rejected rather
|
|
385
|
+
* than passed through.
|
|
386
|
+
*
|
|
387
|
+
* Duplicate entries are rejected, not deduped.
|
|
388
|
+
*/
|
|
389
|
+
resources: string[];
|
|
390
|
+
/**
|
|
391
|
+
* The prompt to surface and to audit alongside the graph sweep. Optional
|
|
392
|
+
* (defaults to `''` server-side) and at most 1500 characters.
|
|
393
|
+
*
|
|
394
|
+
* It is NOT the moderation surface on its own — the graph's own strings are
|
|
395
|
+
* swept too — and it is NOT what generates: the text that generates is
|
|
396
|
+
* whatever your `CLIPTextEncode` node carries. Set it to the same text so
|
|
397
|
+
* what you show the user matches what ran.
|
|
398
|
+
*/
|
|
399
|
+
prompt?: string;
|
|
400
|
+
/** Optional declared negative prompt, at most 1500 characters. Same posture as {@link prompt}. */
|
|
401
|
+
negativePrompt?: string;
|
|
402
|
+
/**
|
|
403
|
+
* The per-job Buzz ceiling. Required, an integer in **1…250**.
|
|
404
|
+
*
|
|
405
|
+
* 🔴 **IT IS ALSO THE STEP TIMEOUT, IN SECONDS.** The host stamps
|
|
406
|
+
* `stepTimeoutSeconds = maxBuzz` — there is only one number, which is exactly
|
|
407
|
+
* what makes the ceiling physically enforceable rather than merely asserted.
|
|
408
|
+
* So `maxBuzz: 10` does not buy a cheap generation; it buys a job that is
|
|
409
|
+
* KILLED after 10 seconds and comes back `expired`. Size it to the wall-clock
|
|
410
|
+
* time your graph actually needs, not to what you hope to pay.
|
|
411
|
+
*
|
|
412
|
+
* You are billed the REAL cost. Billing is post-paid against measured GPU
|
|
413
|
+
* seconds and settles to actual, refunding the unused remainder of the
|
|
414
|
+
* ceiling — so a generous `maxBuzz` costs nothing extra when the job finishes
|
|
415
|
+
* early. The only thing a low value guarantees is a timeout.
|
|
416
|
+
*
|
|
417
|
+
* `estimate` on an inline body echoes this number back as `cost.total`. That
|
|
418
|
+
* is an upper bound, not a price — surface it as "up to N Buzz". There is no
|
|
419
|
+
* honest per-graph estimate: the orchestrator forwards the graph opaquely and
|
|
420
|
+
* cannot price it.
|
|
421
|
+
*
|
|
422
|
+
* The host additionally requires `maxBuzz <= token.buzzBudget` before submit;
|
|
423
|
+
* over-budget comes back as a `failed` snapshot naming both numbers.
|
|
424
|
+
*/
|
|
425
|
+
maxBuzz: number;
|
|
426
|
+
};
|
|
427
|
+
/**
|
|
428
|
+
* The `customComfy` member of the {@link WorkflowBody} discriminated union
|
|
429
|
+
* (`kind: 'customComfy'`) — itself a discriminated union on `mode`, mirroring
|
|
430
|
+
* the host's `blockCustomComfyMemberSchema`:
|
|
431
|
+
*
|
|
432
|
+
* - {@link WorkflowBodyCustomComfyRecipe} (`mode` omitted, or `'recipe'`) —
|
|
433
|
+
* names a server-registered, code-reviewed recipe. The default.
|
|
434
|
+
* - {@link WorkflowBodyCustomComfyInline} (`mode: 'inline'`) — ships the
|
|
435
|
+
* ComfyUI graph itself. Page-tokens-only, and fenced by three server-side
|
|
436
|
+
* gates instead of code review. 🔴 NOT developer-only — this line said it was,
|
|
437
|
+
* and no `customComfy` arm runs an app-developer check. See that type's own
|
|
438
|
+
* doc comment for the refusals that DO run.
|
|
439
|
+
*
|
|
440
|
+
* Both arms are `.strict()` server-side, so a body naming BOTH `recipe` and
|
|
441
|
+
* `workflow` is rejected by both rather than resolved to a winner. Narrow on
|
|
442
|
+
* `body.mode === 'inline'` — NOT on the presence of a `mode` key, and not on
|
|
443
|
+
* the presence of a `workflow` key.
|
|
444
|
+
*/
|
|
445
|
+
export type WorkflowBodyCustomComfy = WorkflowBodyCustomComfyRecipe | WorkflowBodyCustomComfyInline;
|
|
446
|
+
/**
|
|
447
|
+
* The REGISTRY ARM of the `kind: 'step'` member — a **registered orchestrator
|
|
448
|
+
* step**, submitted through the host's step registry, the uniform bridge for
|
|
449
|
+
* step types that are not full generation recipes (image conversion, chat
|
|
450
|
+
* completion, captioning, …).
|
|
451
|
+
*
|
|
452
|
+
* 🔴 `kind: 'step'` HAS TWO ARMS, discriminated by whether `step` is present.
|
|
453
|
+
* This one names a REGISTERED id and the host translates it; the other is
|
|
454
|
+
* {@link WorkflowBodyPassThroughStep}, which omits `step`, names an orchestrator
|
|
455
|
+
* `$type` directly and has the host forward `input` unmodified. The trust model
|
|
456
|
+
* below describes THIS arm only — the pass-through arm deliberately drops four
|
|
457
|
+
* of these controls, and its own doc comment enumerates which.
|
|
458
|
+
*
|
|
459
|
+
* Mirrors the host's `blockStepBodySchema` element-for-element. That schema is
|
|
460
|
+
* `.strict()` with exactly these three fields, so anything else on this object
|
|
461
|
+
* is REJECTED server-side rather than dropped.
|
|
462
|
+
*
|
|
463
|
+
* Trust / safety model (all SERVER-ENFORCED, same posture as
|
|
464
|
+
* {@link WorkflowBodyCustomComfy}):
|
|
465
|
+
* - `step` is a **registered step id** resolved against a code-reviewed,
|
|
466
|
+
* non-DB-editable registry. THIS ARM's wire enum is DERIVED from the registry
|
|
467
|
+
* keys, so an unregistered id is rejected **fail-closed at the schema**,
|
|
468
|
+
* before any translator, any spend reservation, or any orchestrator call.
|
|
469
|
+
* 🔴 That is a statement about the REGISTRY ARM, not about `kind: 'step'` as
|
|
470
|
+
* a whole: a body that omits `step` lands on
|
|
471
|
+
* {@link WorkflowBodyPassThroughStep} instead, where an id the registry has
|
|
472
|
+
* never heard of is exactly the supported case.
|
|
473
|
+
* - `params` are **bounded and validated per-step** by that step's own
|
|
474
|
+
* `.strict()` Zod schema. They are deliberately opaque on the wire (the host
|
|
475
|
+
* keeps the transport step-agnostic), so this field is `Record<string,
|
|
476
|
+
* unknown>` here and the host's per-step schema is the authority for what a
|
|
477
|
+
* given step accepts. An unknown param is a `BAD_REQUEST`, never a silent
|
|
478
|
+
* drop.
|
|
479
|
+
* - Each entry declares its own billing mode and moderation posture in that
|
|
480
|
+
* registry; a step that produces free text has its output scanned before it
|
|
481
|
+
* can reach the block.
|
|
482
|
+
*
|
|
483
|
+
* Registered ids at the time of writing: `'convert-image'` (fixed-price image
|
|
484
|
+
* format conversion + resize) and `'chat-completion'` (fixed-price LLM chat
|
|
485
|
+
* completion over a server-pinned model allowlist). The set grows additively —
|
|
486
|
+
* `step` is typed `string` rather than a literal union on purpose, because the
|
|
487
|
+
* registry lives on the host and a pinned union here would go stale against any
|
|
488
|
+
* host deploy that adds one.
|
|
489
|
+
*/
|
|
490
|
+
export type WorkflowBodyStep = {
|
|
491
|
+
kind: 'step';
|
|
492
|
+
/**
|
|
493
|
+
* A **registered step id** (e.g. `'chat-completion'`). Resolved server-side
|
|
494
|
+
* against the code-reviewed step registry; on THIS arm an unregistered id is
|
|
495
|
+
* rejected fail-closed at the wire schema.
|
|
496
|
+
*
|
|
497
|
+
* Its presence is also the ARM DISCRIMINATOR. Omit it and the body is read as
|
|
498
|
+
* a {@link WorkflowBodyPassThroughStep} instead, where an unregistered id is
|
|
499
|
+
* not rejected because there is no registry lookup at all.
|
|
500
|
+
*/
|
|
501
|
+
step: string;
|
|
502
|
+
/**
|
|
503
|
+
* Bounded, per-step-validated parameters. Only the fields that step's
|
|
504
|
+
* `.strict()` schema accepts are honored; anything else is rejected.
|
|
505
|
+
*
|
|
506
|
+
* For `'chat-completion'` the accepted shape is
|
|
507
|
+
* `{ model: string; messages: Array<{ role: 'system' | 'user' | 'assistant';
|
|
508
|
+
* content: string }>; maxTokens: number; temperature?: number }`, where
|
|
509
|
+
* `model` must be one of the host's allowlisted models and `maxTokens` is
|
|
510
|
+
* REQUIRED and bounded. Documented rather than typed here: the host's schema
|
|
511
|
+
* is the single source of truth, and a hand-mirrored param type in this
|
|
512
|
+
* package would drift against it silently.
|
|
513
|
+
*/
|
|
514
|
+
params: Record<string, unknown>;
|
|
515
|
+
};
|
|
516
|
+
/**
|
|
517
|
+
* The PASS-THROUGH ARM of the `kind: 'step'` member — the block names an
|
|
518
|
+
* ORCHESTRATOR `$type` directly and the host forwards `input` **unmodified**.
|
|
519
|
+
* No registry entry, no server-side translator, no per-step param schema.
|
|
520
|
+
*
|
|
521
|
+
* Mirrors the host's `blockPassThroughStepBodySchema`. That schema is
|
|
522
|
+
* `.strict()` with exactly these fields, so anything else on this object is
|
|
523
|
+
* REJECTED server-side rather than dropped.
|
|
524
|
+
*
|
|
525
|
+
* 🔴 **`step` IS THE ARM DISCRIMINATOR AND IT MUST BE ABSENT.** Send
|
|
526
|
+
* `{ kind: 'step', $type, input, maxBuzz }` with no `step` key at all. It is
|
|
527
|
+
* typed `step?: undefined` here so that omitting it satisfies the type while
|
|
528
|
+
* setting it to anything is a compile error — the host's discriminated union
|
|
529
|
+
* uses `z.undefined()` for the same job, and a body carrying both `step` and
|
|
530
|
+
* `$type` is rejected by BOTH arms (each is `.strict()`) rather than resolved to
|
|
531
|
+
* a winner.
|
|
532
|
+
*
|
|
533
|
+
* WHAT THIS ARM GIVES UP relative to {@link WorkflowBodyStep}. Four of the
|
|
534
|
+
* registry's controls are deliberately absent, by operator decision — this is
|
|
535
|
+
* documented so it reads as a decision rather than an oversight:
|
|
536
|
+
* 1. **No per-step `.strict()` param schema.** `input` is opaque; the
|
|
537
|
+
* orchestrator's own per-`$type` validation is the only shape gate, and it
|
|
538
|
+
* runs AFTER the spend reservation rather than at the wire.
|
|
539
|
+
* 2. **No moderation posture and no prompt audit.** Nothing audits `input`.
|
|
540
|
+
* Moderation moved to the PUBLISH boundary — nothing a block generates is
|
|
541
|
+
* public until published, and that path is moderated.
|
|
542
|
+
* 3. **No resource policy / `urn:air:` scan.** AIR resources are ALLOWED here.
|
|
543
|
+
* Spend, not entitlement, is the binding control.
|
|
544
|
+
* 4. **No `billingMode` and no load-time price invariant.** {@link maxBuzz}
|
|
545
|
+
* and a per-request orchestrator `whatif` quote replace them — see
|
|
546
|
+
* `maxBuzz` below for how the two combine.
|
|
547
|
+
*
|
|
548
|
+
* WHAT STILL BOUNDS IT, all server-side:
|
|
549
|
+
* - A **denylist** of platform-internal `$type`s (scanners, moderation
|
|
550
|
+
* classifiers, hashing/model-ingestion, web egress) is refused by the host
|
|
551
|
+
* router before any spend reservation or orchestrator call. It is a DENYLIST,
|
|
552
|
+
* not an allowlist: a `$type` the host has never heard of is allowed through
|
|
553
|
+
* by construction, which is the point of this arm. `training` and
|
|
554
|
+
* `imageResourceTraining` are deliberately NOT on it (a host operator
|
|
555
|
+
* decision): both may be submitted here, on the same terms as every other
|
|
556
|
+
* `$type`. If the orchestrator quotes the run, the host reserves the quote
|
|
557
|
+
* (or `maxBuzz`, if larger), gated by the token's per-call budget, and
|
|
558
|
+
* stamps no timeout; if it does
|
|
559
|
+
* not, `maxBuzz` is both the reservation and the timeout — see `maxBuzz`
|
|
560
|
+
* below. Whether a given training input is quoted, and at what price, is
|
|
561
|
+
* the orchestrator's answer, not this package's: read it from `estimate`.
|
|
562
|
+
* A host that supports it reports the trained epochs on
|
|
563
|
+
* {@link BlockWorkflowSnapshot.trainedEpochs} without exposing the
|
|
564
|
+
* checkpoint itself.
|
|
565
|
+
* - `$type` is bounded to **1…64 characters** and `input` to **262144 bytes**
|
|
566
|
+
* (256 KB) serialized. Both REJECT; neither truncates.
|
|
567
|
+
* - **`repeat` is refused on this arm**, outright: a `repeat` may only
|
|
568
|
+
* iterate an array in a body's own `arguments`, and this arm has none. Use
|
|
569
|
+
* a `kind: 'workflow'` body ({@link WorkflowBodyWorkflow}) to repeat a step.
|
|
570
|
+
* - `maxBuzz` is the app's spend knob, and the host's quote can raise the
|
|
571
|
+
* reservation above it — see its own note below.
|
|
572
|
+
*
|
|
573
|
+
* @example A pass-through body for an orchestrator step the registry does not
|
|
574
|
+
* carry. Note there is no `step` key.
|
|
575
|
+
* ```ts
|
|
576
|
+
* const body: WorkflowBody = {
|
|
577
|
+
* kind: 'step',
|
|
578
|
+
* $type: 'imageBackgroundRemoval',
|
|
579
|
+
* input: { image: 'https://image.civitai.com/…/00001.jpeg' },
|
|
580
|
+
* maxBuzz: 10,
|
|
581
|
+
* };
|
|
582
|
+
* ```
|
|
583
|
+
*/
|
|
584
|
+
export type WorkflowBodyPassThroughStep = {
|
|
585
|
+
kind: 'step';
|
|
586
|
+
/**
|
|
587
|
+
* 🔴 THE ARM DISCRIMINATOR — **omit this key**. It exists in the type only so
|
|
588
|
+
* that a body which sets it cannot be mistaken for a pass-through body: the
|
|
589
|
+
* only assignable value is `undefined`, and the wire payload carries no `step`
|
|
590
|
+
* key at all (JSON cannot express `undefined`).
|
|
591
|
+
*
|
|
592
|
+
* To name a REGISTERED step id instead, you want {@link WorkflowBodyStep}.
|
|
593
|
+
*/
|
|
594
|
+
step?: undefined;
|
|
595
|
+
/**
|
|
596
|
+
* The ORCHESTRATOR step type to run, verbatim — e.g. `'imageBackgroundRemoval'`.
|
|
597
|
+
* Required, 1…64 characters.
|
|
598
|
+
*
|
|
599
|
+
* This is the orchestrator's own `$type` discriminator, NOT a Civitai step-
|
|
600
|
+
* registry id, and it is not resolved against any allowlist. It is refused
|
|
601
|
+
* only if it names a platform-internal type (the denylist above; the match is
|
|
602
|
+
* case-insensitive). The host records the submitted value as the subtype of
|
|
603
|
+
* the generation it stamps, so a `$type` longer than the cap is rejected
|
|
604
|
+
* rather than silently degraded.
|
|
605
|
+
*/
|
|
606
|
+
$type: string;
|
|
607
|
+
/**
|
|
608
|
+
* The orchestrator step's own input object, **forwarded unmodified**. The host
|
|
609
|
+
* does not read, rewrite, merge or default any field in here — that is the
|
|
610
|
+
* whole point of this arm, and the step the orchestrator receives carries an
|
|
611
|
+
* `input` byte-identical to this value.
|
|
612
|
+
*
|
|
613
|
+
* Consequently the orchestrator's per-`$type` schema is the ONLY authority for
|
|
614
|
+
* what a given `$type` accepts; nothing in this package or on the host mirrors
|
|
615
|
+
* it. Bounded only by size: at most **262144 bytes** (256 KB) serialized, which
|
|
616
|
+
* is a payload-DoS bound and not a shape gate.
|
|
617
|
+
*/
|
|
618
|
+
input: Record<string, unknown>;
|
|
619
|
+
/**
|
|
620
|
+
* The app's declared per-job Buzz amount: the least the host reserves, and —
|
|
621
|
+
* only when unquoted — the step timeout in seconds. NOT a spend ceiling (see
|
|
622
|
+
* below). Required, an integer in **1…250**.
|
|
623
|
+
*
|
|
624
|
+
* 🔴 **WHAT IT BOUNDS DEPENDS ON WHETHER THE HOST GOT A QUOTE** — this is NOT
|
|
625
|
+
* the mechanism of {@link WorkflowBodyCustomComfyInline.maxBuzz}, which is
|
|
626
|
+
* always the timeout. On both `estimate` and `submit` the host first asks the
|
|
627
|
+
* orchestrator for a `whatif` quote of this exact step, then:
|
|
628
|
+
* - **Quoted** — the host reserves `max(maxBuzz, quote)` and stamps NO step
|
|
629
|
+
* timeout. That reservation can exceed 250: it is gated against the token's
|
|
630
|
+
* per-call budget (`token.buzzBudget` — the app's per-generation budget,
|
|
631
|
+
* capped at 1000 when the host mints a production token), and a submit
|
|
632
|
+
* whose reservation exceeds that budget comes back `failed` with an
|
|
633
|
+
* `insufficient buzz budget` error before any Buzz is reserved.
|
|
634
|
+
* - **Unquoted** (the orchestrator returned no price, or the quote could not
|
|
635
|
+
* be had) — the host reserves `maxBuzz` and stamps it as the step timeout,
|
|
636
|
+
* in seconds. `maxBuzz: 10` then buys a job that is KILLED after 10
|
|
637
|
+
* seconds and comes back `expired`, so size it to the wall-clock time the
|
|
638
|
+
* step actually needs. The same per-call budget gate applies. The timeout
|
|
639
|
+
* bounds wall-clock time; it bounds spend only for a step billed by
|
|
640
|
+
* compute time. A step priced per unit can bill ABOVE `maxBuzz`.
|
|
641
|
+
*
|
|
642
|
+
* `estimate` on a pass-through body returns, as `cost.total`, the number the
|
|
643
|
+
* submit would reserve: `max(maxBuzz, quote)`, or `maxBuzz` when unquoted.
|
|
644
|
+
* It is a reservation, NOT a guaranteed ceiling: the terminal settle refunds
|
|
645
|
+
* any unused remainder, but nothing on this arm stops a job billing above
|
|
646
|
+
* the reservation (see the unquoted case above). Do not present it to the
|
|
647
|
+
* viewer as a maximum.
|
|
648
|
+
*/
|
|
649
|
+
maxBuzz: number;
|
|
650
|
+
/**
|
|
651
|
+
* The viewer-confirmable quote this job is charged against —
|
|
652
|
+
* {@link BlockStepQuote.quoteId} from this body's own estimate
|
|
653
|
+
* (`sq_` + 32 lowercase hex characters, {@link BLOCK_STEP_QUOTE_ID_REGEX}).
|
|
654
|
+
*
|
|
655
|
+
* OMIT it everywhere except a CONFIRMED run: an estimate never carries it,
|
|
656
|
+
* and neither does an ordinary submit. It is REQUIRED on
|
|
657
|
+
* `RUN_CONFIRMED_STEP` (`useRunConfirmedStep()` in `@civitai/blocks-react`),
|
|
658
|
+
* the only message that can run a job priced above the token's per-call
|
|
659
|
+
* budget — see {@link BlockWorkflowSnapshot.stepQuote} for the flow.
|
|
660
|
+
*
|
|
661
|
+
* 🔴 A body carrying it is NOT runnable through `SUBMIT_WORKFLOW`. The server
|
|
662
|
+
* charges a confirmed job only against a quote the viewer confirmed in
|
|
663
|
+
* Civitai's own dialog from a signed-in session, so the same body sent as a
|
|
664
|
+
* plain submit is refused. Change nothing else in the body between the
|
|
665
|
+
* estimate and the run: the server checks it against the quoted one
|
|
666
|
+
* (`quoteId` itself is excluded from that comparison).
|
|
667
|
+
*/
|
|
668
|
+
quoteId?: string;
|
|
669
|
+
};
|
|
670
|
+
/**
|
|
671
|
+
* A REFERENCE inside a `kind: 'workflow'` step's `input` — the orchestrator's
|
|
672
|
+
* own mechanism for feeding one step's result into a later step. Put it
|
|
673
|
+
* anywhere a value would go, at any depth.
|
|
674
|
+
*
|
|
675
|
+
* `$ref` names where the value comes from:
|
|
676
|
+
* - `'$arguments'` — the body's own {@link WorkflowBodyWorkflow.arguments};
|
|
677
|
+
* - the `name` of an EARLIER step in the same body;
|
|
678
|
+
* - `'$<index>'` — an EARLIER step you did NOT name, by its zero-based
|
|
679
|
+
* position (`'$0'` is the first step). A step that has a `name` is
|
|
680
|
+
* referenced by that name only.
|
|
681
|
+
*
|
|
682
|
+
* An object counts as a reference only when it has BOTH `$ref` and `path` and
|
|
683
|
+
* no key other than `default` / `format`; anything else is ordinary input.
|
|
684
|
+
*
|
|
685
|
+
* `path` selects the value inside that source, e.g. `'output.images[0].url'`
|
|
686
|
+
* for a step, or `'prompt'` for `$arguments`. The orchestrator resolves it; the
|
|
687
|
+
* path is not checked by this package or by the host.
|
|
688
|
+
*
|
|
689
|
+
* 🔴 A reference may only point BACKWARDS. A step that names itself, a later
|
|
690
|
+
* step, or a step that does not exist is refused by the host with
|
|
691
|
+
* `errorCode: 'workflow_ref_invalid'` before anything is priced or reserved.
|
|
692
|
+
* A body may hold at most 64 references, in inputs nested at most 32 levels.
|
|
693
|
+
*
|
|
694
|
+
* `default` is the value used when the path resolves to nothing; `format` is a
|
|
695
|
+
* template applied to the resolved value. Both are passed to the orchestrator
|
|
696
|
+
* unmodified.
|
|
697
|
+
*/
|
|
698
|
+
export type WorkflowStepRef = {
|
|
699
|
+
$ref: '$arguments' | `$${number}` | (string & {});
|
|
700
|
+
path: string;
|
|
701
|
+
default?: unknown;
|
|
702
|
+
format?: string;
|
|
703
|
+
};
|
|
704
|
+
/**
|
|
705
|
+
* `T` with a {@link WorkflowStepRef} allowed in place of any value, at any
|
|
706
|
+
* depth. Use it to keep a step input typed while still wiring references in:
|
|
707
|
+
*
|
|
708
|
+
* ```ts
|
|
709
|
+
* type CutoutInput = { image: string; format?: 'png' | 'webp' };
|
|
710
|
+
* const input: WithWorkflowStepRefs<CutoutInput> = {
|
|
711
|
+
* image: { $ref: 'draw', path: 'output.images[0].url' },
|
|
712
|
+
* format: 'png',
|
|
713
|
+
* };
|
|
714
|
+
* ```
|
|
715
|
+
*/
|
|
716
|
+
export type WithWorkflowStepRefs<T> = WorkflowStepRef | (T extends ReadonlyArray<infer U> ? Array<WithWorkflowStepRefs<U>> : T extends object ? {
|
|
717
|
+
[K in keyof T]: WithWorkflowStepRefs<T[K]>;
|
|
718
|
+
} : T);
|
|
719
|
+
/** One step of a {@link WorkflowBodyWorkflow}. */
|
|
720
|
+
export type WorkflowBodyWorkflowStep = {
|
|
721
|
+
/**
|
|
722
|
+
* What later steps call this one in a {@link WorkflowStepRef}, and the `name`
|
|
723
|
+
* it reports under on {@link BlockWorkflowSnapshot.steps}. Optional: an
|
|
724
|
+
* unnamed step is named by its position (`'$0'`, `'$1'`, …).
|
|
725
|
+
*
|
|
726
|
+
* A lowercase letter followed by up to 31 of `a-z`, `0-9`, `_` or `-`
|
|
727
|
+
* (`/^[a-z][a-z0-9_-]{0,31}$/`), unique within the body, and not
|
|
728
|
+
* `'block-step'`, which is reserved. A name that breaks a rule is refused
|
|
729
|
+
* with `errorCode: 'workflow_ref_invalid'`.
|
|
730
|
+
*/
|
|
731
|
+
name?: string;
|
|
732
|
+
/**
|
|
733
|
+
* The ORCHESTRATOR step type to run, verbatim — e.g. `'imageGen'`. Not
|
|
734
|
+
* resolved against any allowlist: it is refused only when it names a
|
|
735
|
+
* platform-internal type (scanners, moderation classifiers, hashing, web
|
|
736
|
+
* scrape/search, and the retired `textToImage` step type), with
|
|
737
|
+
* `errorCode: 'workflow_step_denied'`. 1…64 characters.
|
|
738
|
+
*
|
|
739
|
+
* Two step types are read by shape, because they name their sources outside
|
|
740
|
+
* a {@link WorkflowStepRef}:
|
|
741
|
+
* - **`repeat` — over `arguments` only, at most
|
|
742
|
+
* {@link BLOCK_WORKFLOW_REPEAT_MAX_ITERATIONS} iterations.** Its input must
|
|
743
|
+
* be exactly `{ for: { $ref, path, as }, template: { $type, name?, input? } }`
|
|
744
|
+
* with `for.$ref === '$arguments'` and `path` naming an ARRAY in the body's
|
|
745
|
+
* `arguments`; its length is the iteration count, nested repeats
|
|
746
|
+
* multiplied. A repeat over another step's output, a missing or non-array
|
|
747
|
+
* path, or too many iterations is refused with
|
|
748
|
+
* `errorCode: 'workflow_repeat_unbounded'`; a wrong shape, or repeats nested
|
|
749
|
+
* more than 4 deep, with `workflow_ref_invalid`. Inside the template the
|
|
750
|
+
* `as` name is a source a reference may name; it may not equal a step name.
|
|
751
|
+
* The template's `$type` is held to the denylist too.
|
|
752
|
+
* - **`merge`** — every entry of `input.fields` needs a string `$ref` and a
|
|
753
|
+
* `reduce`, and each `$ref` must name `$arguments` or an EARLIER step,
|
|
754
|
+
* never a repeat variable. Each counts toward the 64-reference cap.
|
|
755
|
+
* Otherwise `workflow_ref_invalid`.
|
|
756
|
+
*/
|
|
757
|
+
$type: string;
|
|
758
|
+
/**
|
|
759
|
+
* The step's own input object. Any value in it, at any depth, may be a
|
|
760
|
+
* {@link WorkflowStepRef}. The orchestrator's per-`$type` schema is the only
|
|
761
|
+
* authority for what a given `$type` accepts. Every step's `input` plus the
|
|
762
|
+
* body's `arguments` may total at most 262144 bytes (256 KB) serialized.
|
|
763
|
+
*/
|
|
764
|
+
input: Record<string, unknown>;
|
|
765
|
+
/**
|
|
766
|
+
* `true` marks an INTERMEDIATE step: its blobs are not surfaced, neither on
|
|
767
|
+
* its own {@link BlockWorkflowStepSnapshot.imageUrls} nor in the top-level
|
|
768
|
+
* {@link BlockWorkflowSnapshot.imageUrls}. The step still runs and is still
|
|
769
|
+
* charged.
|
|
770
|
+
*/
|
|
771
|
+
suppressOutput?: boolean;
|
|
772
|
+
};
|
|
773
|
+
/**
|
|
774
|
+
* The `workflow` member of {@link WorkflowBody} — SEVERAL orchestrator steps
|
|
775
|
+
* submitted as ONE workflow, where a later step can consume an earlier step's
|
|
776
|
+
* result through a {@link WorkflowStepRef}. It travels through the same
|
|
777
|
+
* messages and hooks as every other kind (`estimate`, `submit`, `watch`,
|
|
778
|
+
* `cancel`, a batch-estimate cell, `useWorkflowBatch`).
|
|
779
|
+
*
|
|
780
|
+
* Between 1 and {@link BLOCK_WORKFLOW_MAX_STEPS} steps. There is no `maxBuzz`:
|
|
781
|
+
* the spend ceiling is the app's per-generation budget × the number of steps,
|
|
782
|
+
* enforced by the host. The host's schema is strict: an unknown key on the
|
|
783
|
+
* body or on a step is rejected, not dropped.
|
|
784
|
+
*
|
|
785
|
+
* 🔴 THREE THINGS ABOUT THE PRICE, all of them different from a single-step
|
|
786
|
+
* body:
|
|
787
|
+
* 1. **The estimate is NOT a maximum.** When a step's input references an
|
|
788
|
+
* earlier step, the orchestrator re-prices that step once the referenced
|
|
789
|
+
* values are known and charges the difference. Show the estimate as an
|
|
790
|
+
* estimate; never as "you will pay at most".
|
|
791
|
+
* 2. **A workflow the host cannot price has no estimate, but its submit still
|
|
792
|
+
* proceeds.** `estimate()` (and that body's cell in a batch estimate) FAILS
|
|
793
|
+
* with no `cost` and `errorCode` `'workflow_quote_rejected'`,
|
|
794
|
+
* `'workflow_quote_unavailable'` or `'workflow_quote_unpriced'` — see
|
|
795
|
+
* {@link BlockWorkflowSnapshot.errorCode} for which of them a retry can
|
|
796
|
+
* clear. `submit()` of the same body is NOT refused: it is reserved at the
|
|
797
|
+
* ceiling above instead of at a quote. So a failed estimate does not predict
|
|
798
|
+
* a refused submit. Two exceptions are refused on BOTH, before any
|
|
799
|
+
* reservation: a workflow containing a training step, exactly as on the
|
|
800
|
+
* single-step arm (a `training-quote-*` `errorCode`), and one with a
|
|
801
|
+
* `repeat` step at any depth (`errorCode: 'workflow_repeat_unbounded'`).
|
|
802
|
+
* Their `error` states the cause; only "none could be obtained; try again"
|
|
803
|
+
* is one a retry of the same body can clear.
|
|
804
|
+
* (An OLDER host returns the ceiling as the estimate's `cost.total` instead
|
|
805
|
+
* of failing, so there a total equal to the ceiling may be unpriced.)
|
|
806
|
+
* 3. **One author fee per workflow**, not one per step, computed on the
|
|
807
|
+
* QUOTED base. The estimate's `cost.authorFee` is that single fee, already
|
|
808
|
+
* inside `cost.total`. If the workflow bills more than the quote, the fee
|
|
809
|
+
* charged is held to the fee shown. A workflow that does not succeed has
|
|
810
|
+
* its WHOLE fee reversed, even when earlier steps delivered output.
|
|
811
|
+
*
|
|
812
|
+
* The app's velocity limit counts one unit per step, and a `repeat` step one
|
|
813
|
+
* per iteration (nested repeats multiplied) instead of one.
|
|
814
|
+
*
|
|
815
|
+
* 🔴 A FAILED WORKFLOW CAN STILL HAVE COST AND IMAGES. Steps that succeeded
|
|
816
|
+
* before the failing one stay charged, so a `failed` snapshot can carry
|
|
817
|
+
* `cost.total > 0` and usable `imageUrls`. See {@link BlockWorkflowSnapshot.steps}.
|
|
818
|
+
*
|
|
819
|
+
* 🔴 REQUIRES A HOST THAT KNOWS THIS KIND. An older host refuses the body.
|
|
820
|
+
*
|
|
821
|
+
* @example An image step whose output a second step consumes.
|
|
822
|
+
* ```ts
|
|
823
|
+
* const body: WorkflowBody = {
|
|
824
|
+
* kind: 'workflow',
|
|
825
|
+
* arguments: { prompt: 'a lighthouse at dusk' },
|
|
826
|
+
* steps: [
|
|
827
|
+
* {
|
|
828
|
+
* name: 'draw',
|
|
829
|
+
* $type: 'imageGen',
|
|
830
|
+
* input: {
|
|
831
|
+
* engine: 'google',
|
|
832
|
+
* model: 'nano-banana-2',
|
|
833
|
+
* prompt: { $ref: '$arguments', path: 'prompt' },
|
|
834
|
+
* },
|
|
835
|
+
* suppressOutput: true,
|
|
836
|
+
* },
|
|
837
|
+
* {
|
|
838
|
+
* name: 'cutout',
|
|
839
|
+
* $type: 'imageBackgroundRemoval',
|
|
840
|
+
* input: { image: { $ref: 'draw', path: 'output.images[0].url' } },
|
|
841
|
+
* },
|
|
842
|
+
* ],
|
|
843
|
+
* };
|
|
844
|
+
* ```
|
|
845
|
+
*/
|
|
846
|
+
export type WorkflowBodyWorkflow = {
|
|
847
|
+
kind: 'workflow';
|
|
848
|
+
/** The steps, in order. 1…{@link BLOCK_WORKFLOW_MAX_STEPS}. */
|
|
849
|
+
steps: WorkflowBodyWorkflowStep[];
|
|
850
|
+
/**
|
|
851
|
+
* Values the steps can read through `{ $ref: '$arguments', path }` — one
|
|
852
|
+
* place to put a prompt or a seed that several steps share.
|
|
853
|
+
*/
|
|
854
|
+
arguments?: Record<string, unknown>;
|
|
855
|
+
};
|
|
856
|
+
/** `lrScheduler` values the host's ai-toolkit schema accepts. */
|
|
857
|
+
export type AiToolkitLrScheduler = 'constant' | 'constant_with_warmup' | 'cosine' | 'linear' | 'step';
|
|
858
|
+
/** `optimizerType` values the host's ai-toolkit schema accepts. */
|
|
859
|
+
export type AiToolkitOptimizerType = 'adamw' | 'adamw8bit' | 'adam8bit' | 'lion' | 'lion8bit' | 'adafactor' | 'adagrad' | 'prodigy' | 'prodigy8bit' | 'automagic';
|
|
860
|
+
/** The fields every ai-toolkit ecosystem shares (`aiToolkitBaseParams` on the host). */
|
|
861
|
+
export interface AiToolkitTrainingParamsBase {
|
|
862
|
+
engine: 'ai-toolkit';
|
|
863
|
+
/** Saved checkpoint count, integer 1…50. With `steps` absent it is also the length knob. */
|
|
864
|
+
epochs?: number;
|
|
865
|
+
/** Total training steps, integer ≥ 1 — the primary length (and price) knob. */
|
|
866
|
+
steps?: number;
|
|
867
|
+
/** Integer ≥ 1. Exactly `1` on the `qwen21` / `ming` / `yue2` ecosystems. */
|
|
868
|
+
batchSize?: number;
|
|
869
|
+
sampleCfgScale?: number;
|
|
870
|
+
sampleStrength?: number;
|
|
871
|
+
continueFrom?: string;
|
|
872
|
+
resolution: number | null;
|
|
873
|
+
/**
|
|
874
|
+
* Learning rate. 🔴 Must be BELOW `0.1` unless `optimizerType` is `prodigy` or
|
|
875
|
+
* `prodigy8bit` — a host refinement this type cannot express; a body that
|
|
876
|
+
* breaks it is refused at estimate.
|
|
877
|
+
*/
|
|
878
|
+
lr: number;
|
|
879
|
+
textEncoderLr: number | null;
|
|
880
|
+
trainTextEncoder: boolean;
|
|
881
|
+
lrScheduler: AiToolkitLrScheduler;
|
|
882
|
+
optimizerType: AiToolkitOptimizerType;
|
|
883
|
+
networkDim: number | null;
|
|
884
|
+
networkAlpha: number | null;
|
|
885
|
+
noiseOffset: number | null;
|
|
886
|
+
minSnrGamma: number | null;
|
|
887
|
+
flipAugmentation: boolean;
|
|
888
|
+
shuffleTokens: boolean;
|
|
889
|
+
keepTokens: number;
|
|
890
|
+
numRepeats?: number;
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* The ai-toolkit ecosystems that take NO `modelVariant` — send the key absent.
|
|
894
|
+
* (`qwen21` / `ming` / `yue2` also take none; they live on their own arm of
|
|
895
|
+
* {@link AiToolkitTrainingParams} because they pin `batchSize` to `1`.)
|
|
896
|
+
*/
|
|
897
|
+
export type AiToolkitPlainEcosystem = 'sd1' | 'sdxl' | 'chroma' | 'qwen' | 'zimageturbo' | 'zimagebase' | 'ltx2' | 'ltx23' | 'ltx25' | 'minimaxh3' | 'ernie' | 'anima' | 'boogu' | 'krea2' | 'mageflow' | 'ideogram4' | 'ace_step_15';
|
|
898
|
+
/**
|
|
899
|
+
* `params` of a {@link WorkflowBodyTraining} — the host's
|
|
900
|
+
* `aiToolkitTrainingParamsSchema`, the same schema Civitai's own training form
|
|
901
|
+
* validates against, discriminated on `ecosystem`.
|
|
902
|
+
*
|
|
903
|
+
* ⚠️ THE ECOSYSTEM LIST IS CLOSED HERE AND OPEN ON THE HOST. The host adds
|
|
904
|
+
* ecosystems over time; until this package mirrors one, a body naming it does not
|
|
905
|
+
* type-check. That is the cost of a typed copy, paid deliberately: a free
|
|
906
|
+
* `string` would let a typo through to a refused estimate.
|
|
907
|
+
*/
|
|
908
|
+
export type AiToolkitTrainingParams = AiToolkitTrainingParamsBase & ({
|
|
909
|
+
ecosystem: AiToolkitPlainEcosystem;
|
|
910
|
+
modelVariant?: undefined;
|
|
911
|
+
} | {
|
|
912
|
+
ecosystem: 'sd3';
|
|
913
|
+
modelVariant: 'large' | 'medium';
|
|
914
|
+
} | {
|
|
915
|
+
ecosystem: 'flux1';
|
|
916
|
+
modelVariant: 'dev' | 'schnell';
|
|
917
|
+
} | {
|
|
918
|
+
ecosystem: 'wan';
|
|
919
|
+
modelVariant: '2.1' | '2.2';
|
|
920
|
+
} | {
|
|
921
|
+
ecosystem: 'flux2klein';
|
|
922
|
+
modelVariant: '4b' | '9b';
|
|
923
|
+
} | {
|
|
924
|
+
ecosystem: 'qwen21' | 'ming' | 'yue2';
|
|
925
|
+
modelVariant?: undefined;
|
|
926
|
+
batchSize?: 1;
|
|
927
|
+
} | {
|
|
928
|
+
ecosystem: 'ace_step_15_xl';
|
|
929
|
+
modelVariant: 'base' | 'sft';
|
|
930
|
+
});
|
|
931
|
+
/**
|
|
932
|
+
* Train a LoRA with the ai-toolkit engine on a dataset Civitai prepared from the
|
|
933
|
+
* VIEWER'S OWN images — the App Blocks `kind: 'training'` workflow body.
|
|
934
|
+
*
|
|
935
|
+
* ## Availability — read before building on it
|
|
936
|
+
*
|
|
937
|
+
* - 🔴 Behind the host flag **`app-blocks-training-kind`**, which ships OFF and is
|
|
938
|
+
* evaluated per viewer. Where it is off, every training call is refused
|
|
939
|
+
* (`training from apps is not enabled`).
|
|
940
|
+
* - **Page apps only** (`/apps/run/<slug>`). The model slot answers both training
|
|
941
|
+
* messages with an error, and the server refuses a non-page token.
|
|
942
|
+
* - The manifest must declare **`ai:write:budgeted`** and the viewer must have
|
|
943
|
+
* granted it; the viewer must be signed in.
|
|
944
|
+
* - **Not from a development or review session**: dev tokens (`dev:live`, dev
|
|
945
|
+
* tunnel) and review-sandbox tokens are refused, run-for-real included. Use the
|
|
946
|
+
* mock host (`createMockHost` / `Harness`) to build the flow.
|
|
947
|
+
*
|
|
948
|
+
* ## The flow
|
|
949
|
+
*
|
|
950
|
+
* 1. **Dataset** — `usePrepareTrainingDataset()` (`PREPARE_TRAINING_DATASET`)
|
|
951
|
+
* with `[{ imageId, caption }]`. Only the viewer's OWN scanned, unflagged
|
|
952
|
+
* images within the page's maturity ceiling are admitted; the rest come back
|
|
953
|
+
* in `rejected`. You get an opaque {@link WorkflowBodyTraining.datasetId}.
|
|
954
|
+
* 2. **Quote** — `useBuzzWorkflow().estimate(body)` with this body, `quoteId`
|
|
955
|
+
* ABSENT. The reply's {@link BlockWorkflowSnapshot.trainingQuote} is the
|
|
956
|
+
* orchestrator's own price for exactly this run, stored server-side.
|
|
957
|
+
* 3. **Run** — `useRunTraining().runTraining({ ...body, quoteId })`
|
|
958
|
+
* (`RUN_TRAINING`). Civitai shows the viewer a consent dialog in ITS chrome —
|
|
959
|
+
* price, base model, length, dataset size, all read back from the server,
|
|
960
|
+
* never from your body — and submits only on their click.
|
|
961
|
+
* 4. **Follow** — `useBuzzWorkflow().watch(snapshot.workflowId)`.
|
|
962
|
+
*
|
|
963
|
+
* 🔴 `useBuzzWorkflow().submit()` REFUSES THIS BODY before sending it. The server
|
|
964
|
+
* charges a training run only against a quote the viewer confirmed from a
|
|
965
|
+
* signed-in session, which only `RUN_TRAINING`'s dialog can record.
|
|
966
|
+
*
|
|
967
|
+
* ## Money
|
|
968
|
+
*
|
|
969
|
+
* There is no `maxBuzz` and no timeout knob, on purpose: the price is the quote.
|
|
970
|
+
* A run may cost more than the token's per-call `buzzBudget` (the viewer confirms
|
|
971
|
+
* the exact price), up to {@link BLOCK_TRAINING_MAX_BUZZ_PER_RUN} (5,000 Buzz);
|
|
972
|
+
* an estimate above that is refused.
|
|
973
|
+
*/
|
|
974
|
+
export interface WorkflowBodyTraining {
|
|
975
|
+
kind: 'training';
|
|
976
|
+
/** The handle `usePrepareTrainingDataset()` returned — `tds_` + 32 hex characters. */
|
|
977
|
+
datasetId: string;
|
|
978
|
+
/** The only engine accepted. */
|
|
979
|
+
engine: 'ai-toolkit';
|
|
980
|
+
/**
|
|
981
|
+
* A base-model key from Civitai's training catalog (1…64 characters), resolved
|
|
982
|
+
* and gated server-side. Custom AIRs are not accepted.
|
|
983
|
+
*/
|
|
984
|
+
model: string;
|
|
985
|
+
params: AiToolkitTrainingParams;
|
|
986
|
+
/** At most {@link BLOCK_TRAINING_TRIGGER_WORD_MAX_CHARS} characters. Moderated like a prompt. */
|
|
987
|
+
triggerWord: string;
|
|
988
|
+
/**
|
|
989
|
+
* At most {@link BLOCK_TRAINING_SAMPLE_PROMPTS_MAX} prompts of at most
|
|
990
|
+
* {@link BLOCK_TRAINING_SAMPLE_PROMPT_MAX_CHARS} characters each. Moderated like
|
|
991
|
+
* prompts.
|
|
992
|
+
*/
|
|
993
|
+
samplePrompts: string[];
|
|
994
|
+
/**
|
|
995
|
+
* The quote this run is charged against — {@link BlockTrainingQuote.quoteId}
|
|
996
|
+
* from the estimate (`tq_` + 32 hex characters). OMIT it on the estimate;
|
|
997
|
+
* REQUIRED on `RUN_TRAINING`. The server checks the rest of the body against
|
|
998
|
+
* the body that was quoted, so change nothing else between the two.
|
|
999
|
+
*/
|
|
1000
|
+
quoteId?: string;
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Body the block sends to `useBuzzWorkflow().{submit,estimate}`. A real
|
|
1004
|
+
* discriminated union keyed by `kind`:
|
|
1005
|
+
* - {@link WorkflowBodyTextToImage} (`kind: 'textToImage'`) — the original
|
|
1006
|
+
* checkpoint/LoRA/img2img generation body (unchanged, back-compatible).
|
|
1007
|
+
* - {@link WorkflowBodyCustomComfy} (`kind: 'customComfy'`) — post-paid ComfyUI,
|
|
1008
|
+
* itself a union on `mode`: a bounded, server-registered
|
|
1009
|
+
* {@link WorkflowBodyCustomComfyRecipe} (the default), or a
|
|
1010
|
+
* {@link WorkflowBodyCustomComfyInline} graph the block ships itself
|
|
1011
|
+
* (`mode: 'inline'`; page-tokens-only, and NOT developer-only — that claim
|
|
1012
|
+
* used to be here and is false).
|
|
1013
|
+
* - {@link WorkflowBodyStep} (`kind: 'step'`, `step` PRESENT) — a bounded,
|
|
1014
|
+
* server-registered orchestrator step (the host's step registry; billing mode
|
|
1015
|
+
* and moderation posture are declared per entry).
|
|
1016
|
+
* - {@link WorkflowBodyPassThroughStep} (`kind: 'step'`, `step` ABSENT) — names
|
|
1017
|
+
* an orchestrator `$type` directly and has the host forward `input`
|
|
1018
|
+
* unmodified. Bounded by a platform-internal denylist and by `maxBuzz`, not
|
|
1019
|
+
* by a registry.
|
|
1020
|
+
* - {@link WorkflowBodyTraining} (`kind: 'training'`) — an ai-toolkit LoRA run
|
|
1021
|
+
* on a server-prepared dataset of the viewer's own images. ESTIMATE ONLY
|
|
1022
|
+
* through `useBuzzWorkflow`; it runs through `useRunTraining()`, whose
|
|
1023
|
+
* host-chrome consent dialog is the only way to confirm its quote.
|
|
1024
|
+
* - {@link WorkflowBodyWorkflow} (`kind: 'workflow'`) — up to
|
|
1025
|
+
* {@link BLOCK_WORKFLOW_MAX_STEPS} orchestrator steps submitted as one
|
|
1026
|
+
* workflow, a later step consuming an earlier one's result through a
|
|
1027
|
+
* {@link WorkflowStepRef}. Its estimate is not a maximum — read its doc.
|
|
1028
|
+
*
|
|
1029
|
+
* `kind: 'step'` is therefore itself a union, discriminated on the PRESENCE of
|
|
1030
|
+
* `step` — the same nesting {@link WorkflowBodyCustomComfy} has on `mode`.
|
|
1031
|
+
* Narrowing on `kind === 'step'` alone leaves both arms in play; narrow further
|
|
1032
|
+
* with `'$type' in body` (or `body.step === undefined`) before touching
|
|
1033
|
+
* arm-specific fields.
|
|
1034
|
+
*
|
|
1035
|
+
* New kinds extend this union as the host gains support for them. Narrow on
|
|
1036
|
+
* `body.kind` before touching member-specific fields (e.g. `modelId`/`params`
|
|
1037
|
+
* live only on the `textToImage` member).
|
|
1038
|
+
*
|
|
1039
|
+
* ⚠️ Adding a member is additive for PRODUCERS (every existing body still
|
|
1040
|
+
* satisfies the union) but narrowing for CONSUMERS that `switch` exhaustively
|
|
1041
|
+
* over `kind`. Host code that must handle every member gets a compile error
|
|
1042
|
+
* pointing at the new one, which is the intended behaviour.
|
|
1043
|
+
*/
|
|
1044
|
+
export type WorkflowBody = WorkflowBodyTextToImage | WorkflowBodyCustomComfy | WorkflowBodyStep | WorkflowBodyPassThroughStep | WorkflowBodyTraining | WorkflowBodyWorkflow;
|
|
1045
|
+
/**
|
|
1046
|
+
* The host-mediated view of an orchestrator workflow that an iframe block
|
|
1047
|
+
* receives over `postMessage`.
|
|
1048
|
+
*
|
|
1049
|
+
* This is intentionally a flattened **subset** of `WorkflowSnapshot` from
|
|
1050
|
+
* `../orchestrator/` — the host (civitai.com) maps the full orchestrator
|
|
1051
|
+
* payload down to this shape before forwarding. Notable differences:
|
|
1052
|
+
* - `workflowId` here = orchestrator's `id`
|
|
1053
|
+
* - `imageUrls` here = flattened from `steps[].output.images[].url`
|
|
1054
|
+
* - `cost.total` is the host-attested total (not the raw orchestrator field)
|
|
1055
|
+
* - the status union omits orchestrator-internal states like `unassigned`
|
|
1056
|
+
*
|
|
1057
|
+
* If the orchestrator gains a status the host doesn't recognize, the host
|
|
1058
|
+
* is responsible for mapping it to one of the values here (typically
|
|
1059
|
+
* `processing` or `failed`).
|
|
1060
|
+
*/
|
|
1061
|
+
export interface BlockWorkflowSnapshot {
|
|
1062
|
+
workflowId: string;
|
|
1063
|
+
status: 'pending' | 'processing' | 'succeeded' | 'failed' | 'expired' | 'canceled';
|
|
1064
|
+
cost?: {
|
|
1065
|
+
/** Whole Buzz. Includes {@link authorFee} whenever that field is present. */
|
|
1066
|
+
total: number;
|
|
1067
|
+
/**
|
|
1068
|
+
* The part of `total` that is the app author's per-generation fee, in whole
|
|
1069
|
+
* Buzz — so `total - authorFee` is the generation's own price. Display only:
|
|
1070
|
+
* it is the amount the host added into `total` for this reply.
|
|
1071
|
+
*
|
|
1072
|
+
* - **A number** wherever `total` was built from a fee quote: the
|
|
1073
|
+
* `estimate()` result for a `textToImage` or registered-`step` body, and
|
|
1074
|
+
* the refusal `submit()` resolves for those kinds when a budget, spend cap
|
|
1075
|
+
* or rate limit refuses the quoted price. **`0` means a fee was looked up
|
|
1076
|
+
* and none applies** to this request.
|
|
1077
|
+
* - **Absent** when the total is not itemised: a host that predates the
|
|
1078
|
+
* field; every snapshot of a submitted workflow (`submit` success, `poll`,
|
|
1079
|
+
* `cancel` — its total is the realized generation cost and the fee is a
|
|
1080
|
+
* separate charge); the registered-`step` `submit()` refusal for a missing
|
|
1081
|
+
* orchestrator price quote, which refuses before any fee is looked up; and
|
|
1082
|
+
* the kinds that price no fee (`customComfy`, pass-through `step`,
|
|
1083
|
+
* `training`). **Do not read absent as `0`** — show `total` on its own.
|
|
1084
|
+
*
|
|
1085
|
+
* A `kind: 'workflow'` estimate itemises it too, and it is ONE fee for the
|
|
1086
|
+
* whole workflow however many steps it has.
|
|
1087
|
+
*/
|
|
1088
|
+
authorFee?: number;
|
|
1089
|
+
};
|
|
1090
|
+
imageUrls?: string[];
|
|
1091
|
+
error?: string;
|
|
1092
|
+
/**
|
|
1093
|
+
* A stable, machine-readable companion to {@link error}, for a block to
|
|
1094
|
+
* branch on instead of matching the prose. OPTIONAL: set only on the refusals
|
|
1095
|
+
* that define a code, absent everywhere else (and on every reply from a host
|
|
1096
|
+
* that predates the field).
|
|
1097
|
+
*
|
|
1098
|
+
* WHERE IT APPEARS today: the refusal for a pass-through `step` body
|
|
1099
|
+
* ({@link WorkflowBodyPassThroughStep}) whose `$type` is a training type
|
|
1100
|
+
* (`'training'` or `'imageResourceTraining'`, in any casing — the host
|
|
1101
|
+
* compares `$type` case-insensitively) when the host could not get a
|
|
1102
|
+
* price quote for it. That refusal is `{ workflowId: 'failed', status:
|
|
1103
|
+
* 'failed', error, errorCode }` with no `cost`, and it is the reply to
|
|
1104
|
+
* `estimate()` (which rejects with `WorkflowEstimateError`), to `submit()`
|
|
1105
|
+
* (which rejects with `WorkflowSubmitError`, before anything is reserved or
|
|
1106
|
+
* charged), and to that body's cell in a batch estimate. Read it from the
|
|
1107
|
+
* error's `.snapshot.errorCode`. A `kind: 'training'` ESTIMATE does not set
|
|
1108
|
+
* it. The confirmed SUBMIT of a training run or a confirmed step can — see
|
|
1109
|
+
* `'daily-limit-exceeded'` below.
|
|
1110
|
+
* A refused `kind: 'workflow'` body ({@link WorkflowBodyWorkflow}) sets it in
|
|
1111
|
+
* the same shape, on the same three replies — except the three
|
|
1112
|
+
* `workflow_quote_*` codes, which are ESTIMATE-ONLY (`estimate()` and a
|
|
1113
|
+
* batch-estimate cell): `submit()` of the same body defines no such refusal
|
|
1114
|
+
* and proceeds, reserved at the ceiling.
|
|
1115
|
+
* A REGISTERED `step` body ({@link WorkflowBodyStep}) the host has no price
|
|
1116
|
+
* quote for gets one of the three `step_quote_*` codes, also ESTIMATE-ONLY
|
|
1117
|
+
* (`estimate()` and a batch-estimate cell); its `submit()` is unchanged. The
|
|
1118
|
+
* pass-through `step` arm does not set them.
|
|
1119
|
+
*
|
|
1120
|
+
* The values the host sends today ({@link KnownBlockWorkflowErrorCode}):
|
|
1121
|
+
* - `'training-quote-rejected'` — the orchestrator refused the request
|
|
1122
|
+
* itself; resending the same body will not help. `error` then reads
|
|
1123
|
+
* `Training could not be priced: <reason>` when the reason is safe to show.
|
|
1124
|
+
* - `'training-quote-unavailable'` — no usable answer (an outage, a timeout,
|
|
1125
|
+
* a rate limit, insufficient funds, …). Retrying later may succeed.
|
|
1126
|
+
* - `'training-quote-unpriced'` — the orchestrator answered without a price.
|
|
1127
|
+
* - `'daily-limit-exceeded'` — a viewer-confirmed job (a `kind: 'training'`
|
|
1128
|
+
* run or a confirmed step) would exceed what remains today of the daily
|
|
1129
|
+
* Buzz limit THE VIEWER set for this app. Nothing was charged and no job
|
|
1130
|
+
* was started; the confirm cannot override it, and only the viewer can
|
|
1131
|
+
* raise the limit. This one arrives on the failed snapshot a confirmed
|
|
1132
|
+
* SUBMIT resolves (`{ workflowId: 'failed', status: 'failed', cost, error,
|
|
1133
|
+
* errorCode }`), not on an estimate. On a confirmed step the host also
|
|
1134
|
+
* reports it as the `error` code of `CONFIRMED_STEP_RESULT` — see
|
|
1135
|
+
* {@link BlockRunConfirmedStepHostError}.
|
|
1136
|
+
* - `'workflow_step_denied'` — a step of a `kind: 'workflow'` body names a
|
|
1137
|
+
* platform-internal `$type`. Remove or replace that step.
|
|
1138
|
+
* - `'workflow_ref_invalid'` — a {@link WorkflowStepRef} in a step's `input`
|
|
1139
|
+
* points at the step itself, a later step, or nothing; or a step `name`
|
|
1140
|
+
* breaks the naming rules (see {@link WorkflowBodyWorkflowStep.name}).
|
|
1141
|
+
* `error` names the step by its position.
|
|
1142
|
+
* - `'workflow_too_many_steps'` — more than
|
|
1143
|
+
* {@link BLOCK_WORKFLOW_MAX_STEPS} steps.
|
|
1144
|
+
* - `'workflow_repeat_unbounded'` — a `repeat` step iterates something other
|
|
1145
|
+
* than an array in the body's `arguments`, or more than
|
|
1146
|
+
* {@link BLOCK_WORKFLOW_REPEAT_MAX_ITERATIONS} times, nested repeats
|
|
1147
|
+
* multiplied; or the workflow has a `repeat` and the host has no price
|
|
1148
|
+
* quote for it. `error` then reads `workflow step N: a workflow with a
|
|
1149
|
+
* repeat needs a price quote and <cause>`, where `<cause>` is one of the
|
|
1150
|
+
* three in the `workflow_quote_*` messages below. A retry of the same body
|
|
1151
|
+
* can succeed only when the cause is `none could be obtained; try again`;
|
|
1152
|
+
* otherwise the body is what has to change.
|
|
1153
|
+
* - `'workflow_quote_rejected'` — ESTIMATE only. The orchestrator refused
|
|
1154
|
+
* the body, so there is no price to show; resending the same body will not
|
|
1155
|
+
* help. `error` reads `No price quote for this workflow: the orchestrator
|
|
1156
|
+
* rejected the request: <reason>` when the reason is safe to show, else
|
|
1157
|
+
* `No price quote for this workflow: the orchestrator rejected the request;
|
|
1158
|
+
* check the step inputs and references (retrying the same body will not
|
|
1159
|
+
* help)`.
|
|
1160
|
+
* - `'workflow_quote_unavailable'` — ESTIMATE only. No usable answer (an
|
|
1161
|
+
* outage, a timeout, a rate limit, …). The one `workflow_quote_*` code a
|
|
1162
|
+
* retry of the same body can clear. `error` reads `No price quote for this
|
|
1163
|
+
* workflow: none could be obtained; try again`.
|
|
1164
|
+
* - `'workflow_quote_unpriced'` — ESTIMATE only. The orchestrator answered
|
|
1165
|
+
* without a price. `error` reads `No price quote for this workflow: the
|
|
1166
|
+
* orchestrator answered without one`.
|
|
1167
|
+
*
|
|
1168
|
+
* Each `workflow_quote_*` reply is `{ workflowId: 'failed', status: 'failed',
|
|
1169
|
+
* error, errorCode }` with NO `cost`. A host that predates these codes
|
|
1170
|
+
* resolves the estimate instead, with the spend ceiling as `cost.total`.
|
|
1171
|
+
* - `'step_quote_rejected'` — ESTIMATE only, registered `step` body. The
|
|
1172
|
+
* orchestrator refused the request; resending the same body will not help.
|
|
1173
|
+
* `error` reads `No price quote for this step: the orchestrator rejected
|
|
1174
|
+
* the request: <reason>` when the reason is safe to show, else `No price
|
|
1175
|
+
* quote for this step: the orchestrator rejected the request; check the
|
|
1176
|
+
* step inputs and references (retrying the same body will not help)`.
|
|
1177
|
+
* - `'step_quote_unavailable'` — ESTIMATE only, registered `step` body. No
|
|
1178
|
+
* usable answer (an outage, a timeout, a rate limit, …). The one
|
|
1179
|
+
* `step_quote_*` code a retry of the same body can clear. `error` reads
|
|
1180
|
+
* `No price quote for this step: none could be obtained; try again`.
|
|
1181
|
+
* - `'step_quote_unpriced'` — ESTIMATE only, registered `step` body. The
|
|
1182
|
+
* orchestrator answered without a price. `error` reads `No price quote for
|
|
1183
|
+
* this step: the orchestrator answered without one`.
|
|
1184
|
+
*
|
|
1185
|
+
* Each `step_quote_*` reply is `{ workflowId: 'failed', status: 'failed',
|
|
1186
|
+
* error, errorCode }` with NO `cost`. A host that predates these codes
|
|
1187
|
+
* resolves the estimate instead, priced at the step's declared estimate, so
|
|
1188
|
+
* there a registered-step estimate may not be a quote.
|
|
1189
|
+
*
|
|
1190
|
+
* 🔴 AN UNKNOWN VALUE IS POSSIBLE: the host may add codes, and the SDK's
|
|
1191
|
+
* inbound validator accepts any string here. Handle an unrecognised code as a
|
|
1192
|
+
* generic failure and show `error`.
|
|
1193
|
+
*/
|
|
1194
|
+
errorCode?: KnownBlockWorkflowErrorCode | (string & {});
|
|
1195
|
+
/**
|
|
1196
|
+
* The Buzz pool that was the PRIMARY FUNDER of this generation — i.e. the
|
|
1197
|
+
* account with the LARGEST debit, which the host stamps onto the workflow
|
|
1198
|
+
* snapshot server-side. This is NOT necessarily "the paid account": a
|
|
1199
|
+
* generation covered mostly by free/earned Buzz reports `spentAccountType:
|
|
1200
|
+
* 'blue'`. Populated by the host from the Phase-1 backend `spentAccountType`
|
|
1201
|
+
* field; absent when the host predates it or no spend occurred. Informational
|
|
1202
|
+
* only — surface it (e.g. "funded from your yellow balance") but don't gate on it.
|
|
1203
|
+
*/
|
|
1204
|
+
spentAccountType?: BuzzAccountType;
|
|
1205
|
+
/**
|
|
1206
|
+
* Set when the host opportunistically claimed a Buzz reward on the user's
|
|
1207
|
+
* behalf during submit. Currently the host only fires this for the daily
|
|
1208
|
+
* boost (25 blue Buzz, one per UTC day) when the user's balance would
|
|
1209
|
+
* otherwise have been short by less than the boost amount.
|
|
1210
|
+
*
|
|
1211
|
+
* Informational only — the block has no obligation to reconcile state
|
|
1212
|
+
* (the claim already settled in the orchestrator). A typical block UX
|
|
1213
|
+
* surfaces a small "+25 daily boost claimed" notice next to the
|
|
1214
|
+
* succeeded result.
|
|
1215
|
+
*/
|
|
1216
|
+
autoClaim?: {
|
|
1217
|
+
type: 'dailyBoost';
|
|
1218
|
+
amount: number;
|
|
1219
|
+
accountType: 'yellow' | 'blue' | 'red' | 'green';
|
|
1220
|
+
};
|
|
1221
|
+
/**
|
|
1222
|
+
* Checkpoint versions the host SILENTLY replaced before running the
|
|
1223
|
+
* generation. `requested` is the version id the block asked for, `applied` is
|
|
1224
|
+
* the one that actually ran (and was billed).
|
|
1225
|
+
*
|
|
1226
|
+
* On an ecosystem whose model is locked to a workflow, the host swaps a
|
|
1227
|
+
* version id that is not offered for the current workflow for that workflow's
|
|
1228
|
+
* default and still succeeds — graceful degradation that keeps an app pinned
|
|
1229
|
+
* to a since-retired version working. This field only REPORTS the swap; the
|
|
1230
|
+
* behaviour is unchanged.
|
|
1231
|
+
*
|
|
1232
|
+
* WHERE IT APPEARS: on the `kind: 'textToImage'` path only — the submit reply,
|
|
1233
|
+
* the estimate reply, every submit reply that quotes a cost without
|
|
1234
|
+
* submitting, and every later poll/cancel of that workflow (the record is
|
|
1235
|
+
* persisted with it, so it sits next to the `imageUrls` it describes). An
|
|
1236
|
+
* estimate creates no persisted workflow, so there it exists only on that
|
|
1237
|
+
* reply. It does NOT appear for `customComfy` or `step` bodies: neither
|
|
1238
|
+
* resolves a checkpoint, so there is nothing to substitute.
|
|
1239
|
+
*
|
|
1240
|
+
* OMITTED entirely when nothing was substituted. Absent on hosts that predate
|
|
1241
|
+
* it.
|
|
1242
|
+
*/
|
|
1243
|
+
modelSubstitutions?: BlockModelSubstitution[];
|
|
1244
|
+
/**
|
|
1245
|
+
* Generated FREE TEXT produced by a registered text-output step (e.g.
|
|
1246
|
+
* `chat-completion`) — every string here has PASSED the host's output
|
|
1247
|
+
* moderation scan.
|
|
1248
|
+
*
|
|
1249
|
+
* WHERE IT APPEARS: the poll and cancel replies (`poll()` / `cancel()` /
|
|
1250
|
+
* `watch()` on `useBuzzWorkflow`). NOT on the submit reply — a submit returns
|
|
1251
|
+
* a freshly-queued workflow with no output yet, so poll for the text. Also
|
|
1252
|
+
* never on {@link AppWorkflow}, which has no text field.
|
|
1253
|
+
*
|
|
1254
|
+
* OMITTED entirely when there is no text. Independent of
|
|
1255
|
+
* {@link BlockWorkflowSnapshot.textOutputWithheld}: a workflow with two text
|
|
1256
|
+
* steps can release one and withhold the other, and both fields then appear.
|
|
1257
|
+
*/
|
|
1258
|
+
textOutputs?: string[];
|
|
1259
|
+
/**
|
|
1260
|
+
* Set when a text-output step produced nothing the block can be given, with a
|
|
1261
|
+
* user-facing sentence saying why. TWO distinct causes land here:
|
|
1262
|
+
*
|
|
1263
|
+
* 1. WITHHELD — text WAS produced and is being kept back: a content-policy
|
|
1264
|
+
* hit, or any other fail-CLOSED scan outcome (scanner error or timeout,
|
|
1265
|
+
* no verdict at all, an over-cap payload, or requested labels that came
|
|
1266
|
+
* back missing or errored). Every one of those paths returns the SAME
|
|
1267
|
+
* message, so this cause is one string with several origins.
|
|
1268
|
+
* 2. UNPUBLISHABLE — the step SUCCEEDED and the scan RELEASED, but none of
|
|
1269
|
+
* what came back could be published (e.g. a provider tool-call `id`
|
|
1270
|
+
* outside the published charset). Nothing was moderated in this case,
|
|
1271
|
+
* and the message says so explicitly.
|
|
1272
|
+
*
|
|
1273
|
+
* `reason` is deliberately generic: it does not name the labels that
|
|
1274
|
+
* triggered.
|
|
1275
|
+
*
|
|
1276
|
+
* 🔴 THE TWO CAUSES ARE NOT MACHINE-SEPARABLE without matching on the string,
|
|
1277
|
+
* which is NOT a contract. Do not branch on the text of `reason`.
|
|
1278
|
+
*
|
|
1279
|
+
* 🔴 ONLY CAUSE 2 IS STATUS-GATED, and that gate reads the STEP's own
|
|
1280
|
+
* `succeeded` status — never the workflow's. Cause 1 is not gated at all, so a
|
|
1281
|
+
* CANCELLED generation CAN still set this field: the orchestrator returns
|
|
1282
|
+
* whatever text it had already produced, that text is still scanned, and a
|
|
1283
|
+
* hit still withholds it. A block that skips rendering `reason` on cancel
|
|
1284
|
+
* would suppress a real content-policy explanation.
|
|
1285
|
+
*
|
|
1286
|
+
* A block polling a healthy in-flight generation sees the field absent — not
|
|
1287
|
+
* because of its status, but because a step that has produced no text yet can
|
|
1288
|
+
* reach neither cause.
|
|
1289
|
+
*
|
|
1290
|
+
* Independent of {@link BlockWorkflowSnapshot.textOutputs} rather than
|
|
1291
|
+
* mutually exclusive, so `reason` is scoped to "a step in this response",
|
|
1292
|
+
* never to the response as a whole. Same appearance rules as `textOutputs`.
|
|
1293
|
+
*/
|
|
1294
|
+
textOutputWithheld?: {
|
|
1295
|
+
reason: string;
|
|
1296
|
+
};
|
|
1297
|
+
/**
|
|
1298
|
+
* Structured tool calls the model requested on a registered text-output step
|
|
1299
|
+
* (e.g. `chat-completion` with tools), which have PASSED the same output
|
|
1300
|
+
* moderation scan as {@link BlockWorkflowSnapshot.textOutputs}: every
|
|
1301
|
+
* `arguments` string here was part of the text the scan released. The tool
|
|
1302
|
+
* `name` is restricted to `[A-Za-z0-9_-]`, so it cannot carry prose.
|
|
1303
|
+
*
|
|
1304
|
+
* Each entry is fully populated — the host drops a call it cannot fill
|
|
1305
|
+
* completely rather than publishing a partial one.
|
|
1306
|
+
*
|
|
1307
|
+
* Same appearance rules as `textOutputs` (poll and cancel replies only).
|
|
1308
|
+
* OMITTED entirely when there are none.
|
|
1309
|
+
*/
|
|
1310
|
+
toolCalls?: BlockStepToolCall[];
|
|
1311
|
+
/**
|
|
1312
|
+
* The raw orchestrator output of each PASS-THROUGH step
|
|
1313
|
+
* ({@link WorkflowBodyPassThroughStep}: `kind: 'step'` with a `$type`), as
|
|
1314
|
+
* `{ $type, output }`, forwarded as-is — minus its blobs.
|
|
1315
|
+
*
|
|
1316
|
+
* Blob-SHAPED values are removed from `output`; their available urls go to
|
|
1317
|
+
* {@link BlockWorkflowSnapshot.imageUrls} (and to {@link AppWorkflow.images})
|
|
1318
|
+
* instead, so a pass-through step does not open a second image channel.
|
|
1319
|
+
* "Blob-shaped" is the exact scope of that: a url held in a plain string
|
|
1320
|
+
* field of the output is not a blob and can still appear here.
|
|
1321
|
+
*
|
|
1322
|
+
* 🔴 UNSCANNED. Unlike `textOutputs`, no moderation scan runs over this
|
|
1323
|
+
* field; moderation for the pass-through arm happens at the publish boundary.
|
|
1324
|
+
* Treat any text in it accordingly.
|
|
1325
|
+
*
|
|
1326
|
+
* 🔴 A `$type` that is ALSO a registered step's orchestrator type (e.g.
|
|
1327
|
+
* `chatCompletion`, which `chat-completion` uses) is handled by that
|
|
1328
|
+
* registered step instead — its text arrives on `textOutputs`, scanned, and
|
|
1329
|
+
* it does not appear here. Which channel a pass-through `$type` lands on can
|
|
1330
|
+
* therefore change the day the host registers a step for it.
|
|
1331
|
+
*
|
|
1332
|
+
* `output` is `unknown`: its shape is whatever the orchestrator returns for
|
|
1333
|
+
* that `$type`. OMITTED entirely when the workflow has no pass-through step
|
|
1334
|
+
* with an unregistered `$type`.
|
|
1335
|
+
*/
|
|
1336
|
+
stepOutputs?: Array<{
|
|
1337
|
+
$type: string;
|
|
1338
|
+
output: unknown;
|
|
1339
|
+
}>;
|
|
1340
|
+
/**
|
|
1341
|
+
* The epochs of a pass-through `training` / `imageResourceTraining` step
|
|
1342
|
+
* (see {@link WorkflowBodyPassThroughStep}) that PRODUCED A CHECKPOINT — one
|
|
1343
|
+
* entry per epoch, each naming the step's `$type` and its `epochNumber`.
|
|
1344
|
+
* Only runs whose training moderation status is approved are listed; an
|
|
1345
|
+
* unmoderated or refused run reports no epochs.
|
|
1346
|
+
*
|
|
1347
|
+
* 🔴 THE CHECKPOINT ITSELF IS DELIBERATELY NOT EXPOSED — this field carries
|
|
1348
|
+
* no download url. (An older host may still surface checkpoint urls in
|
|
1349
|
+
* `imageUrls` or `stepOutputs` for a training step; that is a host defect,
|
|
1350
|
+
* not a contract — never treat an `imageUrls` entry as a checkpoint.) To
|
|
1351
|
+
* publish a trained epoch, navigate the viewer to Civitai's own model wizard:
|
|
1352
|
+
*
|
|
1353
|
+
* ```ts
|
|
1354
|
+
* const { navigate } = useCivitaiNavigate();
|
|
1355
|
+
* navigate(
|
|
1356
|
+
* `models/train/from-orchestrator?workflowId=${snapshot.workflowId}&epoch=${epoch.epochNumber}`,
|
|
1357
|
+
* { scope: 'site' },
|
|
1358
|
+
* );
|
|
1359
|
+
* ```
|
|
1360
|
+
*
|
|
1361
|
+
* The viewer completes (or abandons) the publish there, on Civitai's surface;
|
|
1362
|
+
* {@link BlockWorkflowSnapshot.publishedModel} then reports the result.
|
|
1363
|
+
*
|
|
1364
|
+
* 🔴 REQUIRES A HOST VERSION THAT EMITS IT. Absent on older hosts — treat
|
|
1365
|
+
* absence as "unknown", not as "no checkpoint". The run's spend and timeout
|
|
1366
|
+
* follow the pass-through arm's rules — see
|
|
1367
|
+
* {@link WorkflowBodyPassThroughStep.maxBuzz}.
|
|
1368
|
+
*/
|
|
1369
|
+
trainedEpochs?: BlockTrainedEpoch[];
|
|
1370
|
+
/**
|
|
1371
|
+
* The Civitai model this run has been published as — present once the viewer
|
|
1372
|
+
* has STARTED or FINISHED publishing it through the model wizard reached from
|
|
1373
|
+
* {@link BlockWorkflowSnapshot.trainedEpochs}. `published` is `false` while the
|
|
1374
|
+
* model is still a draft and `true` once it is published.
|
|
1375
|
+
*
|
|
1376
|
+
* 🔴 REQUIRES A HOST VERSION THAT EMITS IT. Absent on older hosts, and absent
|
|
1377
|
+
* on any run nobody has started publishing.
|
|
1378
|
+
*/
|
|
1379
|
+
publishedModel?: BlockPublishedModel;
|
|
1380
|
+
/**
|
|
1381
|
+
* The quote a {@link WorkflowBodyTraining} ESTIMATE produced — the orchestrator's
|
|
1382
|
+
* own price for exactly that run, stored server-side. Pass
|
|
1383
|
+
* `trainingQuote.quoteId` back as the body's `quoteId` on
|
|
1384
|
+
* `useRunTraining().runTraining()`; the consent dialog then shows the viewer
|
|
1385
|
+
* this price, read back from the server.
|
|
1386
|
+
*
|
|
1387
|
+
* `cost.total` on the same snapshot equals `trainingQuote.total`.
|
|
1388
|
+
* `expiresAt` is an ISO-8601 timestamp: after it the run is refused and you
|
|
1389
|
+
* estimate again. `imageCount` is the server-derived size of the prepared
|
|
1390
|
+
* dataset, never a number the block sent.
|
|
1391
|
+
*
|
|
1392
|
+
* Present ONLY on the reply to a `kind: 'training'` estimate. Absent on hosts
|
|
1393
|
+
* that predate civitai/civitai#5434.
|
|
1394
|
+
*/
|
|
1395
|
+
trainingQuote?: BlockTrainingQuote;
|
|
1396
|
+
/**
|
|
1397
|
+
* The viewer-confirmable quote a pass-through `step` ESTIMATE
|
|
1398
|
+
* ({@link WorkflowBodyPassThroughStep}) produced, when the job costs more than
|
|
1399
|
+
* the app may spend without asking. Its presence means: this job cannot run
|
|
1400
|
+
* through `submit()`, and CAN run through `useRunConfirmedStep()` once the
|
|
1401
|
+
* viewer confirms `stepQuote.total` in Civitai's own dialog.
|
|
1402
|
+
*
|
|
1403
|
+
* THE FLOW, mirroring a training run's:
|
|
1404
|
+
* 1. `useBuzzWorkflow().estimate(body)` — the reply carries `stepQuote`.
|
|
1405
|
+
* 2. `useRunConfirmedStep().runConfirmedStep({ ...body, quoteId:
|
|
1406
|
+
* stepQuote.quoteId })` — Civitai shows the viewer the price (read back
|
|
1407
|
+
* from the server, never from the body) and submits on their confirm.
|
|
1408
|
+
* 3. `useBuzzWorkflow().watch(snapshot.workflowId)`.
|
|
1409
|
+
*
|
|
1410
|
+
* WHEN THE HOST SENDS IT — all of these, decided server-side:
|
|
1411
|
+
* - the orchestrator quoted a **FIXED** price for exactly this step:
|
|
1412
|
+
* positive, and not a variable or metered one. The viewer confirms what
|
|
1413
|
+
* they are charged, so a price that may move is never confirmable;
|
|
1414
|
+
* - that price is ABOVE the token's per-call budget and at most
|
|
1415
|
+
* {@link BLOCK_CONFIRMED_STEP_MAX_BUZZ_PER_JOB} (5,000 Buzz);
|
|
1416
|
+
* - the block runs in a page app for a signed-in viewer on an ordinary
|
|
1417
|
+
* token. Development, dev-tunnel and review sessions never get one, and
|
|
1418
|
+
* neither does the model slot.
|
|
1419
|
+
* Everywhere else the field is ABSENT and the estimate is exactly what it was.
|
|
1420
|
+
* Absent is therefore not an error — it is also what every host that
|
|
1421
|
+
* predates civitai/civitai#5786 sends.
|
|
1422
|
+
*
|
|
1423
|
+
* 🔴 ONLY A SINGLE ESTIMATE CARRIES IT. A batch estimate
|
|
1424
|
+
* (`ESTIMATE_WORKFLOW_BATCH`, `useBatchEstimate()`) stores nothing, so its
|
|
1425
|
+
* cells never have a `stepQuote`, whatever they cost. Price a grid in a batch,
|
|
1426
|
+
* then estimate the one body you want to run on its own.
|
|
1427
|
+
*
|
|
1428
|
+
* 🔴 TWO BODIES MAKE THAT ESTIMATE FAIL INSTEAD OF RETURNING A PRICE — only
|
|
1429
|
+
* where a quote would otherwise have been granted (so not below the budget,
|
|
1430
|
+
* and not in a batch). The reply is the failure snapshot `{ workflowId:
|
|
1431
|
+
* 'failed', status: 'failed', error }`, which `estimate()` rejects with; the
|
|
1432
|
+
* same text arrives as `CONFIRMED_STEP_RESULT`'s `error` if such a body is
|
|
1433
|
+
* run. Neither has an {@link BlockWorkflowSnapshot.errorCode}; they are
|
|
1434
|
+
* server messages, so match on the text and keep a generic branch:
|
|
1435
|
+
* - `a training step type cannot run as a confirmed step — use
|
|
1436
|
+
* kind:'training'` — `$type` is `training` or `imageResourceTraining`, in
|
|
1437
|
+
* any casing.
|
|
1438
|
+
* Training has its own kind ({@link WorkflowBodyTraining}).
|
|
1439
|
+
* - `a confirmed step needs plain JSON input (objects, arrays, strings,
|
|
1440
|
+
* finite numbers, booleans, null) — <path> is <what>` — `input` holds a
|
|
1441
|
+
* `Date`, `Map`, `Set`, class instance, `BigInt`, `NaN`/`Infinity`, an
|
|
1442
|
+
* `undefined` array element or an empty slot in a sparse array
|
|
1443
|
+
* (`… — body.input.list[1] is an empty array slot`). The quote is bound to the exact body, so the
|
|
1444
|
+
* body must be exactly representable. The part after the dash names the
|
|
1445
|
+
* offending path (`body.input.when is a Date`) and varies: match on the
|
|
1446
|
+
* text BEFORE the dash.
|
|
1447
|
+
*
|
|
1448
|
+
* `total` is the price the viewer confirms and is charged. 🔴 It can differ
|
|
1449
|
+
* from `cost.total` on the same snapshot, which stays what an UNCONFIRMED
|
|
1450
|
+
* submit would reserve (`max(maxBuzz, quote)`); show `stepQuote.total` on a
|
|
1451
|
+
* confirm button. `expiresAt` is an ISO-8601 timestamp 15 minutes out; after
|
|
1452
|
+
* it the run is refused and you estimate again. A quote is single-use.
|
|
1453
|
+
*/
|
|
1454
|
+
stepQuote?: BlockStepQuote;
|
|
1455
|
+
/**
|
|
1456
|
+
* What each 3D model in {@link BlockWorkflowSnapshot.imageUrls} IS — one label
|
|
1457
|
+
* per model a pass-through step produced (e.g. `polyGen`).
|
|
1458
|
+
*
|
|
1459
|
+
* Every model url is still listed in `imageUrls`, unchanged and in the same
|
|
1460
|
+
* order as before the field existed; this only says which entry is which.
|
|
1461
|
+
* Match on `url`:
|
|
1462
|
+
*
|
|
1463
|
+
* ```ts
|
|
1464
|
+
* const glb = snapshot.model3dOutputs?.find(
|
|
1465
|
+
* (m) => m.format === 'glb' && m.variant === 'model',
|
|
1466
|
+
* );
|
|
1467
|
+
* if (glb) viewer.load(glb.url);
|
|
1468
|
+
* ```
|
|
1469
|
+
*
|
|
1470
|
+
* OMITTED when the workflow produced no 3D model, and on hosts that predate
|
|
1471
|
+
* civitai/civitai#5786 — in which case `imageUrls` is all there is.
|
|
1472
|
+
*/
|
|
1473
|
+
model3dOutputs?: BlockModel3dOutput[];
|
|
1474
|
+
/**
|
|
1475
|
+
* The steps of a `kind: 'workflow'` run ({@link WorkflowBodyWorkflow}), in
|
|
1476
|
+
* the orchestrator's order, each with its own status and results. The steps a
|
|
1477
|
+
* `repeat` step generates follow it, named `<repeat name>.<n>`, so the list
|
|
1478
|
+
* can be longer than the body's.
|
|
1479
|
+
*
|
|
1480
|
+
* The top-level fields keep their meaning: `status` is the workflow's,
|
|
1481
|
+
* `imageUrls` is the union of every step that did NOT set `suppressOutput`,
|
|
1482
|
+
* and `cost` is the whole workflow's.
|
|
1483
|
+
*
|
|
1484
|
+
* 🔴 A `failed` WORKFLOW IS NOT AN EMPTY ONE. Steps that succeeded before the
|
|
1485
|
+
* failing step stay charged and keep their results, so a snapshot with
|
|
1486
|
+
* `status: 'failed'` can carry `cost.total > 0` and usable `imageUrls`. Read
|
|
1487
|
+
* `steps` to see which step failed (`status: 'failed'`, with its `error`)
|
|
1488
|
+
* and which finished.
|
|
1489
|
+
*
|
|
1490
|
+
* Read it from the poll and cancel replies of a `kind: 'workflow'` run; do
|
|
1491
|
+
* not rely on it being on the submit reply. ABSENT for every other kind, on
|
|
1492
|
+
* an estimate (which creates no workflow) and on a host that predates it.
|
|
1493
|
+
*/
|
|
1494
|
+
steps?: BlockWorkflowStepSnapshot[];
|
|
1495
|
+
}
|
|
1496
|
+
/** One entry of {@link BlockWorkflowSnapshot.steps}. */
|
|
1497
|
+
export interface BlockWorkflowStepSnapshot {
|
|
1498
|
+
/** The step's `name` from the body, or the name the host gave an unnamed step. */
|
|
1499
|
+
name: string;
|
|
1500
|
+
/** The orchestrator step type that ran. */
|
|
1501
|
+
$type: string;
|
|
1502
|
+
status: 'pending' | 'processing' | 'succeeded' | 'failed' | 'expired' | 'canceled';
|
|
1503
|
+
/**
|
|
1504
|
+
* This step's own image urls. Omitted when it has none, and always omitted
|
|
1505
|
+
* for a step submitted with `suppressOutput: true`.
|
|
1506
|
+
*/
|
|
1507
|
+
imageUrls?: string[];
|
|
1508
|
+
/**
|
|
1509
|
+
* The step's raw orchestrator output, minus its blobs — the same treatment
|
|
1510
|
+
* and the same caveats as {@link BlockWorkflowSnapshot.stepOutputs}: its shape
|
|
1511
|
+
* is whatever the orchestrator returns for that `$type`, and it is UNSCANNED.
|
|
1512
|
+
*/
|
|
1513
|
+
output?: unknown;
|
|
1514
|
+
/** Why this step failed. Diagnostic text; do not render it verbatim. */
|
|
1515
|
+
error?: string;
|
|
1516
|
+
}
|
|
1517
|
+
/** {@link BlockWorkflowSnapshot.trainingQuote}. */
|
|
1518
|
+
export interface BlockTrainingQuote {
|
|
1519
|
+
/** `tq_` + 32 hex characters. Opaque — pass it back verbatim. */
|
|
1520
|
+
quoteId: string;
|
|
1521
|
+
/** The quoted price, in Buzz. */
|
|
1522
|
+
total: number;
|
|
1523
|
+
/** How many images the prepared dataset holds. */
|
|
1524
|
+
imageCount: number;
|
|
1525
|
+
/** ISO-8601. The quote cannot be run after this. */
|
|
1526
|
+
expiresAt: string;
|
|
1527
|
+
}
|
|
1528
|
+
/** {@link BlockWorkflowSnapshot.stepQuote}. */
|
|
1529
|
+
export interface BlockStepQuote {
|
|
1530
|
+
/**
|
|
1531
|
+
* `sq_` + 32 lowercase hex characters ({@link BLOCK_STEP_QUOTE_ID_REGEX}).
|
|
1532
|
+
* Opaque — pass it back verbatim as the body's `quoteId`.
|
|
1533
|
+
*/
|
|
1534
|
+
quoteId: string;
|
|
1535
|
+
/** The exact price the viewer confirms and is charged, in Buzz. */
|
|
1536
|
+
total: number;
|
|
1537
|
+
/** The orchestrator `$type` that was priced — the body's own `$type`. */
|
|
1538
|
+
stepType: string;
|
|
1539
|
+
/** ISO-8601. The quote cannot be run after this. */
|
|
1540
|
+
expiresAt: string;
|
|
1541
|
+
}
|
|
1542
|
+
/**
|
|
1543
|
+
* One entry of {@link BlockWorkflowSnapshot.model3dOutputs} /
|
|
1544
|
+
* {@link AppWorkflow.model3dOutputs} — the label for one 3D model url.
|
|
1545
|
+
*/
|
|
1546
|
+
export interface BlockModel3dOutput {
|
|
1547
|
+
/** The model's url. Also present in the snapshot's `imageUrls` (an `AppWorkflow`'s `images`). */
|
|
1548
|
+
url: string;
|
|
1549
|
+
/**
|
|
1550
|
+
* The model's own file format as the orchestrator reports it — `'glb'`,
|
|
1551
|
+
* `'fbx'`, …. An open set: compare against the formats your viewer loads and
|
|
1552
|
+
* ignore the rest.
|
|
1553
|
+
*/
|
|
1554
|
+
format: string;
|
|
1555
|
+
/**
|
|
1556
|
+
* Which output of the step this model is: the output field it came from,
|
|
1557
|
+
* dot-joined when nested — `'model'`, `'riggedFbxModel'`,
|
|
1558
|
+
* `'basicAnimations.walkingModel'`, …. An array index is a segment
|
|
1559
|
+
* (`'models.0'`), and it is `''` when the step's whole output is the model.
|
|
1560
|
+
* The names are the orchestrator's, per `$type`; nothing here lists them.
|
|
1561
|
+
*/
|
|
1562
|
+
variant: string;
|
|
1563
|
+
}
|
|
1564
|
+
/**
|
|
1565
|
+
* Why the host substituted a checkpoint version — see
|
|
1566
|
+
* {@link BlockWorkflowSnapshot.modelSubstitutions}.
|
|
1567
|
+
*
|
|
1568
|
+
* - `'wrong-workflow'` — the id IS one of this ecosystem's workflow-scoped
|
|
1569
|
+
* versions, but for a DIFFERENT workflow (e.g. an edit-only version sent to
|
|
1570
|
+
* text-to-image).
|
|
1571
|
+
* - `'unrecognized'` — the id is in no scoped list for this ecosystem: a
|
|
1572
|
+
* community checkpoint, or a version retired since the app shipped.
|
|
1573
|
+
* - `'gated'` — the id IS offered for this workflow, but a gate rule hid it
|
|
1574
|
+
* from THIS viewer. The app is not wrong; the viewer may not use it.
|
|
1575
|
+
*
|
|
1576
|
+
* The host may add a reason in a later version, and the SDK's inbound
|
|
1577
|
+
* validator does not reject an unrecognised one — handle an unknown value as a
|
|
1578
|
+
* generic substitution rather than assuming the list is closed.
|
|
1579
|
+
*/
|
|
1580
|
+
export type ModelSubstitutionReason = 'wrong-workflow' | 'unrecognized' | 'gated';
|
|
1581
|
+
/**
|
|
1582
|
+
* The {@link BlockWorkflowSnapshot.errorCode} values the host sends today — see
|
|
1583
|
+
* that field for what each means and which replies carry it.
|
|
1584
|
+
*
|
|
1585
|
+
* NOT a closed list: the field itself is typed `KnownBlockWorkflowErrorCode |
|
|
1586
|
+
* (string & {})` (the union is an autocomplete nudge), the host may add codes,
|
|
1587
|
+
* and the SDK's inbound validator accepts any string. Handle an unrecognised
|
|
1588
|
+
* code as a generic failure.
|
|
1589
|
+
*/
|
|
1590
|
+
export type KnownBlockWorkflowErrorCode = 'training-quote-rejected' | 'training-quote-unavailable' | 'training-quote-unpriced' | 'daily-limit-exceeded' | 'workflow_step_denied' | 'workflow_ref_invalid' | 'workflow_too_many_steps' | 'workflow_repeat_unbounded' | 'workflow_quote_rejected' | 'workflow_quote_unavailable' | 'workflow_quote_unpriced' | 'step_quote_rejected' | 'step_quote_unavailable' | 'step_quote_unpriced';
|
|
1591
|
+
/** One entry of {@link BlockWorkflowSnapshot.modelSubstitutions}. */
|
|
1592
|
+
export interface BlockModelSubstitution {
|
|
1593
|
+
/** The checkpoint version id the block asked for. */
|
|
1594
|
+
requested: number;
|
|
1595
|
+
/** The checkpoint version id that actually ran. */
|
|
1596
|
+
applied: number;
|
|
1597
|
+
reason: ModelSubstitutionReason;
|
|
1598
|
+
}
|
|
1599
|
+
/** One entry of {@link BlockWorkflowSnapshot.toolCalls}. */
|
|
1600
|
+
export interface BlockStepToolCall {
|
|
1601
|
+
id: string;
|
|
1602
|
+
/**
|
|
1603
|
+
* The SDK's inbound validator checks only that it is a string, so a value
|
|
1604
|
+
* the host adds later can arrive — do not assume `'function'` exhaustively.
|
|
1605
|
+
*/
|
|
1606
|
+
type: 'function';
|
|
1607
|
+
function: {
|
|
1608
|
+
/** Restricted to `[A-Za-z0-9_-]`. */
|
|
1609
|
+
name: string;
|
|
1610
|
+
/** The model's arguments, as the JSON string it produced. Scanned. */
|
|
1611
|
+
arguments: string;
|
|
1612
|
+
};
|
|
1613
|
+
}
|
|
1614
|
+
/** One entry of {@link BlockWorkflowSnapshot.trainedEpochs} / {@link AppWorkflow.trainedEpochs}. */
|
|
1615
|
+
export interface BlockTrainedEpoch {
|
|
1616
|
+
/**
|
|
1617
|
+
* Which pass-through training `$type` produced this epoch. The SDK's inbound
|
|
1618
|
+
* validator checks only that it is a string, so a value the host adds later
|
|
1619
|
+
* can arrive — give an exhaustive `switch` a default branch.
|
|
1620
|
+
*/
|
|
1621
|
+
$type: 'training' | 'imageResourceTraining';
|
|
1622
|
+
/** Pass as `epoch` to the `/models/train/from-orchestrator` wizard route. */
|
|
1623
|
+
epochNumber: number;
|
|
1624
|
+
}
|
|
1625
|
+
/**
|
|
1626
|
+
* The model a training run was published as —
|
|
1627
|
+
* {@link BlockWorkflowSnapshot.publishedModel} / {@link AppWorkflow.publishedModel}.
|
|
1628
|
+
*/
|
|
1629
|
+
export interface BlockPublishedModel {
|
|
1630
|
+
modelId: number;
|
|
1631
|
+
modelVersionId: number;
|
|
1632
|
+
/** `false` while the model is still a draft in the wizard; `true` once published. */
|
|
1633
|
+
published: boolean;
|
|
1634
|
+
}
|
|
1635
|
+
/**
|
|
1636
|
+
* One result image on an {@link AppWorkflow}. Mirrors civitai/civitai's
|
|
1637
|
+
* `AppWorkflowImage` projection (`src/server/services/blocks/workflow.service.ts`,
|
|
1638
|
+
* PR #3164) — keep in lockstep. Only `available` blobs with a non-null url are
|
|
1639
|
+
* surfaced by the host (pending/blocked blobs are dropped rather than handing the
|
|
1640
|
+
* block dead links). `width`/`height` are `null` until the orchestrator populates
|
|
1641
|
+
* them; `nsfwLevel` is the numeric civitai browsing-level bitflag (1/2/4/8/16),
|
|
1642
|
+
* `null` for an unrated blob.
|
|
1643
|
+
*/
|
|
1644
|
+
export interface AppWorkflowImage {
|
|
1645
|
+
url: string;
|
|
1646
|
+
width: number | null;
|
|
1647
|
+
height: number | null;
|
|
1648
|
+
nsfwLevel: number | null;
|
|
1649
|
+
}
|
|
1650
|
+
/**
|
|
1651
|
+
* The clean, wire-stable projection of ONE orchestrator workflow in the calling
|
|
1652
|
+
* app's own generator SUBQUEUE — what `QUERY_APP_WORKFLOWS` / `CANCEL_APP_WORKFLOW`
|
|
1653
|
+
* return. Mirrors civitai/civitai's `AppWorkflow` / `projectAppWorkflow`
|
|
1654
|
+
* (`src/server/services/blocks/workflow.service.ts`, PR #3164) EXACTLY — KEEP IN
|
|
1655
|
+
* LOCKSTEP; a drift here silently strands the reply's transport validator.
|
|
1656
|
+
*
|
|
1657
|
+
* The host deliberately DROPS every internal/sensitive workflow field (steps,
|
|
1658
|
+
* params, prompts, resources, tokens, transactions, metadata, tags) so a block
|
|
1659
|
+
* can never read generation internals of a queue it only owns by tag.
|
|
1660
|
+
*
|
|
1661
|
+
* status: the block-contract status — the orchestrator's
|
|
1662
|
+
* `unassigned`/`preparing`/`scheduled` all collapse to `pending`.
|
|
1663
|
+
* images: only `available` blobs with a non-null url (see {@link AppWorkflowImage}).
|
|
1664
|
+
* cost: the workflow's realized/estimated buzz total, or `null` when absent.
|
|
1665
|
+
* createdAt: ISO-8601 string.
|
|
1666
|
+
*/
|
|
1667
|
+
export interface AppWorkflow {
|
|
1668
|
+
workflowId: string;
|
|
1669
|
+
status: 'pending' | 'processing' | 'succeeded' | 'failed' | 'expired' | 'canceled';
|
|
1670
|
+
images: AppWorkflowImage[];
|
|
1671
|
+
cost: number | null;
|
|
1672
|
+
/** ISO-8601. */
|
|
1673
|
+
createdAt: string;
|
|
1674
|
+
/**
|
|
1675
|
+
* The Civitai model a training run in this workflow was published as —
|
|
1676
|
+
* present once the viewer has started or finished publishing it through the
|
|
1677
|
+
* model wizard. Same shape and meaning as
|
|
1678
|
+
* {@link BlockWorkflowSnapshot.publishedModel}.
|
|
1679
|
+
*
|
|
1680
|
+
* 🔴 REQUIRES A HOST VERSION THAT EMITS IT. Absent on older hosts, and on
|
|
1681
|
+
* every workflow nobody has published.
|
|
1682
|
+
*/
|
|
1683
|
+
publishedModel?: BlockPublishedModel;
|
|
1684
|
+
/**
|
|
1685
|
+
* The epochs of a pass-through `training` / `imageResourceTraining` step in
|
|
1686
|
+
* this workflow that produced a checkpoint. Same shape and rule as
|
|
1687
|
+
* {@link BlockWorkflowSnapshot.trainedEpochs}: only runs whose training
|
|
1688
|
+
* moderation status is approved are listed, the checkpoint itself is not
|
|
1689
|
+
* exposed, and the field is OMITTED when there are no epochs to list. Lets a
|
|
1690
|
+
* block offer "publish epoch n" from its queue without polling each run.
|
|
1691
|
+
*
|
|
1692
|
+
* 🔴 REQUIRES A HOST VERSION THAT EMITS IT. Absent on older hosts — treat
|
|
1693
|
+
* absence as "unknown", not as "no checkpoint".
|
|
1694
|
+
*/
|
|
1695
|
+
trainedEpochs?: BlockTrainedEpoch[];
|
|
1696
|
+
/**
|
|
1697
|
+
* Labels for the 3D models among this workflow's {@link AppWorkflow.images} —
|
|
1698
|
+
* same shape and rule as {@link BlockWorkflowSnapshot.model3dOutputs}: every
|
|
1699
|
+
* labelled `url` is also an entry of `images`, and the field is OMITTED when
|
|
1700
|
+
* there are none (and on hosts that predate it).
|
|
1701
|
+
*/
|
|
1702
|
+
model3dOutputs?: BlockModel3dOutput[];
|
|
1703
|
+
}
|