@civitai/app-sdk 0.56.1 → 0.58.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.
@@ -1146,17 +1146,30 @@ export type WorkflowBodyStep = {
1146
1146
  * 3. **No resource policy / `urn:air:` scan.** AIR resources are ALLOWED here.
1147
1147
  * Spend, not entitlement, is the binding control.
1148
1148
  * 4. **No `billingMode` and no load-time price invariant.** {@link maxBuzz}
1149
- * replaces them, exactly as on {@link WorkflowBodyCustomComfyInline}.
1149
+ * and a per-request orchestrator `whatif` quote replace them — see
1150
+ * `maxBuzz` below for how the two combine.
1150
1151
  *
1151
1152
  * WHAT STILL BOUNDS IT, all server-side:
1152
1153
  * - A **denylist** of platform-internal `$type`s (scanners, moderation
1153
1154
  * classifiers, hashing/model-ingestion, web egress) is refused by the host
1154
1155
  * router before any spend reservation or orchestrator call. It is a DENYLIST,
1155
1156
  * not an allowlist: a `$type` the host has never heard of is allowed through
1156
- * by construction, which is the point of this arm.
1157
+ * by construction, which is the point of this arm. `training` and
1158
+ * `imageResourceTraining` are deliberately NOT on it (a host operator
1159
+ * decision): both may be submitted here, on the same terms as every other
1160
+ * `$type`. If the orchestrator quotes the run, the host reserves the quote
1161
+ * (or `maxBuzz`, if larger), gated by the token's per-call budget, and
1162
+ * stamps no timeout; if it does
1163
+ * not, `maxBuzz` is both the reservation and the timeout — see `maxBuzz`
1164
+ * below. Whether a given training input is quoted, and at what price, is
1165
+ * the orchestrator's answer, not this package's: read it from `estimate`.
1166
+ * A host that supports it reports the trained epochs on
1167
+ * {@link BlockWorkflowSnapshot.trainedEpochs} without exposing the
1168
+ * checkpoint itself.
1157
1169
  * - `$type` is bounded to **1…64 characters** and `input` to **262144 bytes**
1158
1170
  * (256 KB) serialized. Both REJECT; neither truncates.
1159
- * - `maxBuzz` is the single spend knob — see its own note below.
1171
+ * - `maxBuzz` is the app's spend knob, and the host's quote can raise the
1172
+ * reservation above it — see its own note below.
1160
1173
  *
1161
1174
  * @example A pass-through body for an orchestrator step the registry does not
1162
1175
  * carry. Note there is no `step` key.
@@ -1205,26 +1218,197 @@ export type WorkflowBodyPassThroughStep = {
1205
1218
  */
1206
1219
  input: Record<string, unknown>;
1207
1220
  /**
1208
- * The per-job Buzz ceiling. Required, an integer in **1…250**.
1209
- *
1210
- * 🔴 **IT IS ALSO THE STEP TIMEOUT, IN SECONDS** — identical mechanism to
1211
- * {@link WorkflowBodyCustomComfyInline.maxBuzz}. The host stamps
1212
- * `stepTimeoutSeconds = maxBuzz`; there is only one number, which is what makes
1213
- * the ceiling physically enforceable rather than merely asserted. So
1214
- * `maxBuzz: 10` does not buy a cheap job; it buys one that is KILLED after 10
1215
- * seconds and comes back `expired`. Size it to the wall-clock time the step
1216
- * actually needs.
1221
+ * The app's declared per-job Buzz amount: the least the host reserves, and —
1222
+ * only when unquoted — the step timeout in seconds. NOT a spend ceiling (see
1223
+ * below). Required, an integer in **1…250**.
1217
1224
  *
1218
- * You are billed the REAL cost: post-paid against measured GPU seconds,
1219
- * refunding the unused remainder of the ceiling, so a generous `maxBuzz` costs
1220
- * nothing extra when the job finishes early. `estimate` on a pass-through body
1221
- * echoes this number back as `cost.total` — an upper bound, not a price;
1222
- * surface it as "up to N Buzz".
1225
+ * 🔴 **WHAT IT BOUNDS DEPENDS ON WHETHER THE HOST GOT A QUOTE** — this is NOT
1226
+ * the mechanism of {@link WorkflowBodyCustomComfyInline.maxBuzz}, which is
1227
+ * always the timeout. On both `estimate` and `submit` the host first asks the
1228
+ * orchestrator for a `whatif` quote of this exact step, then:
1229
+ * - **Quoted** — the host reserves `max(maxBuzz, quote)` and stamps NO step
1230
+ * timeout. That reservation can exceed 250: it is gated against the token's
1231
+ * per-call budget (`token.buzzBudget` — the app's per-generation budget,
1232
+ * capped at 1000 when the host mints a production token), and a submit
1233
+ * whose reservation exceeds that budget comes back `failed` with an
1234
+ * `insufficient buzz budget` error before any Buzz is reserved.
1235
+ * - **Unquoted** (the orchestrator returned no price, or the quote could not
1236
+ * be had) — the host reserves `maxBuzz` and stamps it as the step timeout,
1237
+ * in seconds. `maxBuzz: 10` then buys a job that is KILLED after 10
1238
+ * seconds and comes back `expired`, so size it to the wall-clock time the
1239
+ * step actually needs. The same per-call budget gate applies. The timeout
1240
+ * bounds wall-clock time; it bounds spend only for a step billed by
1241
+ * compute time. A step priced per unit can bill ABOVE `maxBuzz`.
1223
1242
  *
1224
- * The host additionally requires `maxBuzz <= token.buzzBudget` before submit.
1243
+ * `estimate` on a pass-through body returns, as `cost.total`, the number the
1244
+ * submit would reserve: `max(maxBuzz, quote)`, or `maxBuzz` when unquoted.
1245
+ * It is a reservation, NOT a guaranteed ceiling: the terminal settle refunds
1246
+ * any unused remainder, but nothing on this arm stops a job billing above
1247
+ * the reservation (see the unquoted case above). Do not present it to the
1248
+ * viewer as a maximum.
1225
1249
  */
1226
1250
  maxBuzz: number;
1227
1251
  };
1252
+ /** Ceiling on what ONE confirmed `kind: 'training'` run may cost, in Buzz. Host constant `BLOCK_TRAINING_MAX_BUZZ_PER_RUN`. */
1253
+ export declare const BLOCK_TRAINING_MAX_BUZZ_PER_RUN = 5000;
1254
+ /** Max images in one prepared training dataset. Host constant `BLOCK_TRAINING_DATASET_MAX_ITEMS`. */
1255
+ export declare const BLOCK_TRAINING_DATASET_MAX_ITEMS = 50;
1256
+ /** Max characters of one dataset caption. Host constant `BLOCK_TRAINING_CAPTION_MAX_CHARS`. */
1257
+ export declare const BLOCK_TRAINING_CAPTION_MAX_CHARS = 1000;
1258
+ /** Max sample prompts on a training body. Host constant `BLOCK_TRAINING_SAMPLE_PROMPTS_MAX`. */
1259
+ export declare const BLOCK_TRAINING_SAMPLE_PROMPTS_MAX = 6;
1260
+ /** Max characters of one sample prompt. Host constant `BLOCK_TRAINING_SAMPLE_PROMPT_MAX_CHARS`. */
1261
+ export declare const BLOCK_TRAINING_SAMPLE_PROMPT_MAX_CHARS = 1000;
1262
+ /** Max characters of the trigger word. Host constant `BLOCK_TRAINING_TRIGGER_WORD_MAX_CHARS`. */
1263
+ export declare const BLOCK_TRAINING_TRIGGER_WORD_MAX_CHARS = 64;
1264
+ /** Max characters of the base-model key. Host constant `BLOCK_TRAINING_MODEL_KEY_MAX_CHARS`. */
1265
+ export declare const BLOCK_TRAINING_MODEL_KEY_MAX_CHARS = 64;
1266
+ /** `lrScheduler` values the host's ai-toolkit schema accepts. */
1267
+ export type AiToolkitLrScheduler = 'constant' | 'constant_with_warmup' | 'cosine' | 'linear' | 'step';
1268
+ /** `optimizerType` values the host's ai-toolkit schema accepts. */
1269
+ export type AiToolkitOptimizerType = 'adamw' | 'adamw8bit' | 'adam8bit' | 'lion' | 'lion8bit' | 'adafactor' | 'adagrad' | 'prodigy' | 'prodigy8bit' | 'automagic';
1270
+ /** The fields every ai-toolkit ecosystem shares (`aiToolkitBaseParams` on the host). */
1271
+ export interface AiToolkitTrainingParamsBase {
1272
+ engine: 'ai-toolkit';
1273
+ /** Saved checkpoint count, integer 1…50. With `steps` absent it is also the length knob. */
1274
+ epochs?: number;
1275
+ /** Total training steps, integer ≥ 1 — the primary length (and price) knob. */
1276
+ steps?: number;
1277
+ /** Integer ≥ 1. Exactly `1` on the `qwen21` / `ming` / `yue2` ecosystems. */
1278
+ batchSize?: number;
1279
+ sampleCfgScale?: number;
1280
+ sampleStrength?: number;
1281
+ continueFrom?: string;
1282
+ resolution: number | null;
1283
+ /**
1284
+ * Learning rate. 🔴 Must be BELOW `0.1` unless `optimizerType` is `prodigy` or
1285
+ * `prodigy8bit` — a host refinement this type cannot express; a body that
1286
+ * breaks it is refused at estimate.
1287
+ */
1288
+ lr: number;
1289
+ textEncoderLr: number | null;
1290
+ trainTextEncoder: boolean;
1291
+ lrScheduler: AiToolkitLrScheduler;
1292
+ optimizerType: AiToolkitOptimizerType;
1293
+ networkDim: number | null;
1294
+ networkAlpha: number | null;
1295
+ noiseOffset: number | null;
1296
+ minSnrGamma: number | null;
1297
+ flipAugmentation: boolean;
1298
+ shuffleTokens: boolean;
1299
+ keepTokens: number;
1300
+ numRepeats?: number;
1301
+ }
1302
+ /**
1303
+ * The ai-toolkit ecosystems that take NO `modelVariant` — send the key absent.
1304
+ * (`qwen21` / `ming` / `yue2` also take none; they live on their own arm of
1305
+ * {@link AiToolkitTrainingParams} because they pin `batchSize` to `1`.)
1306
+ */
1307
+ export type AiToolkitPlainEcosystem = 'sd1' | 'sdxl' | 'chroma' | 'qwen' | 'zimageturbo' | 'zimagebase' | 'ltx2' | 'ltx23' | 'ltx25' | 'minimaxh3' | 'ernie' | 'anima' | 'boogu' | 'krea2' | 'mageflow' | 'ideogram4' | 'ace_step_15';
1308
+ /**
1309
+ * `params` of a {@link WorkflowBodyTraining} — the host's
1310
+ * `aiToolkitTrainingParamsSchema`, the same schema Civitai's own training form
1311
+ * validates against, discriminated on `ecosystem`.
1312
+ *
1313
+ * ⚠️ THE ECOSYSTEM LIST IS CLOSED HERE AND OPEN ON THE HOST. The host adds
1314
+ * ecosystems over time; until this package mirrors one, a body naming it does not
1315
+ * type-check. That is the cost of a typed copy, paid deliberately: a free
1316
+ * `string` would let a typo through to a refused estimate.
1317
+ */
1318
+ export type AiToolkitTrainingParams = AiToolkitTrainingParamsBase & ({
1319
+ ecosystem: AiToolkitPlainEcosystem;
1320
+ modelVariant?: undefined;
1321
+ } | {
1322
+ ecosystem: 'sd3';
1323
+ modelVariant: 'large' | 'medium';
1324
+ } | {
1325
+ ecosystem: 'flux1';
1326
+ modelVariant: 'dev' | 'schnell';
1327
+ } | {
1328
+ ecosystem: 'wan';
1329
+ modelVariant: '2.1' | '2.2';
1330
+ } | {
1331
+ ecosystem: 'flux2klein';
1332
+ modelVariant: '4b' | '9b';
1333
+ } | {
1334
+ ecosystem: 'qwen21' | 'ming' | 'yue2';
1335
+ modelVariant?: undefined;
1336
+ batchSize?: 1;
1337
+ } | {
1338
+ ecosystem: 'ace_step_15_xl';
1339
+ modelVariant: 'base' | 'sft';
1340
+ });
1341
+ /**
1342
+ * Train a LoRA with the ai-toolkit engine on a dataset Civitai prepared from the
1343
+ * VIEWER'S OWN images — the App Blocks `kind: 'training'` workflow body.
1344
+ *
1345
+ * ## Availability — read before building on it
1346
+ *
1347
+ * - 🔴 Behind the host flag **`app-blocks-training-kind`**, which ships OFF and is
1348
+ * evaluated per viewer. Where it is off, every training call is refused
1349
+ * (`training from apps is not enabled`).
1350
+ * - **Page apps only** (`/apps/run/<slug>`). The model slot answers both training
1351
+ * messages with an error, and the server refuses a non-page token.
1352
+ * - The manifest must declare **`ai:write:budgeted`** and the viewer must have
1353
+ * granted it; the viewer must be signed in.
1354
+ * - **Not from a development or review session**: dev tokens (`dev:live`, dev
1355
+ * tunnel) and review-sandbox tokens are refused, run-for-real included. Use the
1356
+ * mock host (`createMockHost` / `Harness`) to build the flow.
1357
+ *
1358
+ * ## The flow
1359
+ *
1360
+ * 1. **Dataset** — `usePrepareTrainingDataset()` (`PREPARE_TRAINING_DATASET`)
1361
+ * with `[{ imageId, caption }]`. Only the viewer's OWN scanned, unflagged
1362
+ * images within the page's maturity ceiling are admitted; the rest come back
1363
+ * in `rejected`. You get an opaque {@link WorkflowBodyTraining.datasetId}.
1364
+ * 2. **Quote** — `useBuzzWorkflow().estimate(body)` with this body, `quoteId`
1365
+ * ABSENT. The reply's {@link BlockWorkflowSnapshot.trainingQuote} is the
1366
+ * orchestrator's own price for exactly this run, stored server-side.
1367
+ * 3. **Run** — `useRunTraining().runTraining({ ...body, quoteId })`
1368
+ * (`RUN_TRAINING`). Civitai shows the viewer a consent dialog in ITS chrome —
1369
+ * price, base model, length, dataset size, all read back from the server,
1370
+ * never from your body — and submits only on their click.
1371
+ * 4. **Follow** — `useBuzzWorkflow().watch(snapshot.workflowId)`.
1372
+ *
1373
+ * 🔴 `useBuzzWorkflow().submit()` REFUSES THIS BODY before sending it. The server
1374
+ * charges a training run only against a quote the viewer confirmed from a
1375
+ * signed-in session, which only `RUN_TRAINING`'s dialog can record.
1376
+ *
1377
+ * ## Money
1378
+ *
1379
+ * There is no `maxBuzz` and no timeout knob, on purpose: the price is the quote.
1380
+ * A run may cost more than the token's per-call `buzzBudget` (the viewer confirms
1381
+ * the exact price), up to {@link BLOCK_TRAINING_MAX_BUZZ_PER_RUN} (5,000 Buzz);
1382
+ * an estimate above that is refused.
1383
+ */
1384
+ export interface WorkflowBodyTraining {
1385
+ kind: 'training';
1386
+ /** The handle `usePrepareTrainingDataset()` returned — `tds_` + 32 hex characters. */
1387
+ datasetId: string;
1388
+ /** The only engine accepted. */
1389
+ engine: 'ai-toolkit';
1390
+ /**
1391
+ * A base-model key from Civitai's training catalog (1…64 characters), resolved
1392
+ * and gated server-side. Custom AIRs are not accepted.
1393
+ */
1394
+ model: string;
1395
+ params: AiToolkitTrainingParams;
1396
+ /** At most {@link BLOCK_TRAINING_TRIGGER_WORD_MAX_CHARS} characters. Moderated like a prompt. */
1397
+ triggerWord: string;
1398
+ /**
1399
+ * At most {@link BLOCK_TRAINING_SAMPLE_PROMPTS_MAX} prompts of at most
1400
+ * {@link BLOCK_TRAINING_SAMPLE_PROMPT_MAX_CHARS} characters each. Moderated like
1401
+ * prompts.
1402
+ */
1403
+ samplePrompts: string[];
1404
+ /**
1405
+ * The quote this run is charged against — {@link BlockTrainingQuote.quoteId}
1406
+ * from the estimate (`tq_` + 32 hex characters). OMIT it on the estimate;
1407
+ * REQUIRED on `RUN_TRAINING`. The server checks the rest of the body against
1408
+ * the body that was quoted, so change nothing else between the two.
1409
+ */
1410
+ quoteId?: string;
1411
+ }
1228
1412
  /**
1229
1413
  * Body the block sends to `useBuzzWorkflow().{submit,estimate}`. A real
1230
1414
  * discriminated union keyed by `kind`:
@@ -1243,6 +1427,10 @@ export type WorkflowBodyPassThroughStep = {
1243
1427
  * an orchestrator `$type` directly and has the host forward `input`
1244
1428
  * unmodified. Bounded by a platform-internal denylist and by `maxBuzz`, not
1245
1429
  * by a registry.
1430
+ * - {@link WorkflowBodyTraining} (`kind: 'training'`) — an ai-toolkit LoRA run
1431
+ * on a server-prepared dataset of the viewer's own images. ESTIMATE ONLY
1432
+ * through `useBuzzWorkflow`; it runs through `useRunTraining()`, whose
1433
+ * host-chrome consent dialog is the only way to confirm its quote.
1246
1434
  *
1247
1435
  * `kind: 'step'` is therefore itself a union, discriminated on the PRESENCE of
1248
1436
  * `step` — the same nesting {@link WorkflowBodyCustomComfy} has on `mode`.
@@ -1259,7 +1447,7 @@ export type WorkflowBodyPassThroughStep = {
1259
1447
  * over `kind`. Host code that must handle every member gets a compile error
1260
1448
  * pointing at the new one, which is the intended behaviour.
1261
1449
  */
1262
- export type WorkflowBody = WorkflowBodyTextToImage | WorkflowBodyCustomComfy | WorkflowBodyStep | WorkflowBodyPassThroughStep;
1450
+ export type WorkflowBody = WorkflowBodyTextToImage | WorkflowBodyCustomComfy | WorkflowBodyStep | WorkflowBodyPassThroughStep | WorkflowBodyTraining;
1263
1451
  /**
1264
1452
  * The host-mediated view of an orchestrator workflow that an iframe block
1265
1453
  * receives over `postMessage`.
@@ -1310,6 +1498,253 @@ export interface BlockWorkflowSnapshot {
1310
1498
  amount: number;
1311
1499
  accountType: 'yellow' | 'blue' | 'red' | 'green';
1312
1500
  };
1501
+ /**
1502
+ * Checkpoint versions the host SILENTLY replaced before running the
1503
+ * generation. `requested` is the version id the block asked for, `applied` is
1504
+ * the one that actually ran (and was billed).
1505
+ *
1506
+ * On an ecosystem whose model is locked to a workflow, the host swaps a
1507
+ * version id that is not offered for the current workflow for that workflow's
1508
+ * default and still succeeds — graceful degradation that keeps an app pinned
1509
+ * to a since-retired version working. This field only REPORTS the swap; the
1510
+ * behaviour is unchanged.
1511
+ *
1512
+ * WHERE IT APPEARS: on the `kind: 'textToImage'` path only — the submit reply,
1513
+ * the estimate reply, every submit reply that quotes a cost without
1514
+ * submitting, and every later poll/cancel of that workflow (the record is
1515
+ * persisted with it, so it sits next to the `imageUrls` it describes). An
1516
+ * estimate creates no persisted workflow, so there it exists only on that
1517
+ * reply. It does NOT appear for `customComfy` or `step` bodies: neither
1518
+ * resolves a checkpoint, so there is nothing to substitute.
1519
+ *
1520
+ * OMITTED entirely when nothing was substituted. Absent on hosts that predate
1521
+ * it.
1522
+ */
1523
+ modelSubstitutions?: BlockModelSubstitution[];
1524
+ /**
1525
+ * Generated FREE TEXT produced by a registered text-output step (e.g.
1526
+ * `chat-completion`) — every string here has PASSED the host's output
1527
+ * moderation scan.
1528
+ *
1529
+ * WHERE IT APPEARS: the poll and cancel replies (`poll()` / `cancel()` /
1530
+ * `watch()` on `useBuzzWorkflow`). NOT on the submit reply — a submit returns
1531
+ * a freshly-queued workflow with no output yet, so poll for the text. Also
1532
+ * never on {@link AppWorkflow}, which has no text field.
1533
+ *
1534
+ * OMITTED entirely when there is no text. Independent of
1535
+ * {@link BlockWorkflowSnapshot.textOutputWithheld}: a workflow with two text
1536
+ * steps can release one and withhold the other, and both fields then appear.
1537
+ */
1538
+ textOutputs?: string[];
1539
+ /**
1540
+ * Set when a text-output step produced nothing the block can be given, with a
1541
+ * user-facing sentence saying why. TWO distinct causes land here:
1542
+ *
1543
+ * 1. WITHHELD — text WAS produced and is being kept back: a content-policy
1544
+ * hit, or any other fail-CLOSED scan outcome (scanner error or timeout,
1545
+ * no verdict at all, an over-cap payload, or requested labels that came
1546
+ * back missing or errored). Every one of those paths returns the SAME
1547
+ * message, so this cause is one string with several origins.
1548
+ * 2. UNPUBLISHABLE — the step SUCCEEDED and the scan RELEASED, but none of
1549
+ * what came back could be published (e.g. a provider tool-call `id`
1550
+ * outside the published charset). Nothing was moderated in this case,
1551
+ * and the message says so explicitly.
1552
+ *
1553
+ * `reason` is deliberately generic: it does not name the labels that
1554
+ * triggered.
1555
+ *
1556
+ * 🔴 THE TWO CAUSES ARE NOT MACHINE-SEPARABLE without matching on the string,
1557
+ * which is NOT a contract. Do not branch on the text of `reason`.
1558
+ *
1559
+ * 🔴 ONLY CAUSE 2 IS STATUS-GATED, and that gate reads the STEP's own
1560
+ * `succeeded` status — never the workflow's. Cause 1 is not gated at all, so a
1561
+ * CANCELLED generation CAN still set this field: the orchestrator returns
1562
+ * whatever text it had already produced, that text is still scanned, and a
1563
+ * hit still withholds it. A block that skips rendering `reason` on cancel
1564
+ * would suppress a real content-policy explanation.
1565
+ *
1566
+ * A block polling a healthy in-flight generation sees the field absent — not
1567
+ * because of its status, but because a step that has produced no text yet can
1568
+ * reach neither cause.
1569
+ *
1570
+ * Independent of {@link BlockWorkflowSnapshot.textOutputs} rather than
1571
+ * mutually exclusive, so `reason` is scoped to "a step in this response",
1572
+ * never to the response as a whole. Same appearance rules as `textOutputs`.
1573
+ */
1574
+ textOutputWithheld?: {
1575
+ reason: string;
1576
+ };
1577
+ /**
1578
+ * Structured tool calls the model requested on a registered text-output step
1579
+ * (e.g. `chat-completion` with tools), which have PASSED the same output
1580
+ * moderation scan as {@link BlockWorkflowSnapshot.textOutputs}: every
1581
+ * `arguments` string here was part of the text the scan released. The tool
1582
+ * `name` is restricted to `[A-Za-z0-9_-]`, so it cannot carry prose.
1583
+ *
1584
+ * Each entry is fully populated — the host drops a call it cannot fill
1585
+ * completely rather than publishing a partial one.
1586
+ *
1587
+ * Same appearance rules as `textOutputs` (poll and cancel replies only).
1588
+ * OMITTED entirely when there are none.
1589
+ */
1590
+ toolCalls?: BlockStepToolCall[];
1591
+ /**
1592
+ * The raw orchestrator output of each PASS-THROUGH step
1593
+ * ({@link WorkflowBodyPassThroughStep}: `kind: 'step'` with a `$type`), as
1594
+ * `{ $type, output }`, forwarded as-is — minus its blobs.
1595
+ *
1596
+ * Blob-SHAPED values are removed from `output`; their available urls go to
1597
+ * {@link BlockWorkflowSnapshot.imageUrls} (and to {@link AppWorkflow.images})
1598
+ * instead, so a pass-through step does not open a second image channel.
1599
+ * "Blob-shaped" is the exact scope of that: a url held in a plain string
1600
+ * field of the output is not a blob and can still appear here.
1601
+ *
1602
+ * 🔴 UNSCANNED. Unlike `textOutputs`, no moderation scan runs over this
1603
+ * field; moderation for the pass-through arm happens at the publish boundary.
1604
+ * Treat any text in it accordingly.
1605
+ *
1606
+ * 🔴 A `$type` that is ALSO a registered step's orchestrator type (e.g.
1607
+ * `chatCompletion`, which `chat-completion` uses) is handled by that
1608
+ * registered step instead — its text arrives on `textOutputs`, scanned, and
1609
+ * it does not appear here. Which channel a pass-through `$type` lands on can
1610
+ * therefore change the day the host registers a step for it.
1611
+ *
1612
+ * `output` is `unknown`: its shape is whatever the orchestrator returns for
1613
+ * that `$type`. OMITTED entirely when the workflow has no pass-through step
1614
+ * with an unregistered `$type`.
1615
+ */
1616
+ stepOutputs?: Array<{
1617
+ $type: string;
1618
+ output: unknown;
1619
+ }>;
1620
+ /**
1621
+ * The epochs of a pass-through `training` / `imageResourceTraining` step
1622
+ * (see {@link WorkflowBodyPassThroughStep}) that PRODUCED A CHECKPOINT — one
1623
+ * entry per epoch, each naming the step's `$type` and its `epochNumber`.
1624
+ * Only runs whose training moderation status is approved are listed; an
1625
+ * unmoderated or refused run reports no epochs.
1626
+ *
1627
+ * 🔴 THE CHECKPOINT ITSELF IS DELIBERATELY NOT EXPOSED — this field carries
1628
+ * no download url. (An older host may still surface checkpoint urls in
1629
+ * `imageUrls` or `stepOutputs` for a training step; that is a host defect,
1630
+ * not a contract — never treat an `imageUrls` entry as a checkpoint.) To
1631
+ * publish a trained epoch, navigate the viewer to Civitai's own model wizard:
1632
+ *
1633
+ * ```ts
1634
+ * const { navigate } = useCivitaiNavigate();
1635
+ * navigate(
1636
+ * `models/train/from-orchestrator?workflowId=${snapshot.workflowId}&epoch=${epoch.epochNumber}`,
1637
+ * { scope: 'site' },
1638
+ * );
1639
+ * ```
1640
+ *
1641
+ * The viewer completes (or abandons) the publish there, on Civitai's surface;
1642
+ * {@link BlockWorkflowSnapshot.publishedModel} then reports the result.
1643
+ *
1644
+ * 🔴 REQUIRES A HOST VERSION THAT EMITS IT. Absent on older hosts — treat
1645
+ * absence as "unknown", not as "no checkpoint". The run's spend and timeout
1646
+ * follow the pass-through arm's rules — see
1647
+ * {@link WorkflowBodyPassThroughStep.maxBuzz}.
1648
+ */
1649
+ trainedEpochs?: BlockTrainedEpoch[];
1650
+ /**
1651
+ * The Civitai model this run has been published as — present once the viewer
1652
+ * has STARTED or FINISHED publishing it through the model wizard reached from
1653
+ * {@link BlockWorkflowSnapshot.trainedEpochs}. `published` is `false` while the
1654
+ * model is still a draft and `true` once it is published.
1655
+ *
1656
+ * 🔴 REQUIRES A HOST VERSION THAT EMITS IT. Absent on older hosts, and absent
1657
+ * on any run nobody has started publishing.
1658
+ */
1659
+ publishedModel?: BlockPublishedModel;
1660
+ /**
1661
+ * The quote a {@link WorkflowBodyTraining} ESTIMATE produced — the orchestrator's
1662
+ * own price for exactly that run, stored server-side. Pass
1663
+ * `trainingQuote.quoteId` back as the body's `quoteId` on
1664
+ * `useRunTraining().runTraining()`; the consent dialog then shows the viewer
1665
+ * this price, read back from the server.
1666
+ *
1667
+ * `cost.total` on the same snapshot equals `trainingQuote.total`.
1668
+ * `expiresAt` is an ISO-8601 timestamp: after it the run is refused and you
1669
+ * estimate again. `imageCount` is the server-derived size of the prepared
1670
+ * dataset, never a number the block sent.
1671
+ *
1672
+ * Present ONLY on the reply to a `kind: 'training'` estimate. Absent on hosts
1673
+ * that predate civitai/civitai#5434.
1674
+ */
1675
+ trainingQuote?: BlockTrainingQuote;
1676
+ }
1677
+ /** {@link BlockWorkflowSnapshot.trainingQuote}. */
1678
+ export interface BlockTrainingQuote {
1679
+ /** `tq_` + 32 hex characters. Opaque — pass it back verbatim. */
1680
+ quoteId: string;
1681
+ /** The quoted price, in Buzz. */
1682
+ total: number;
1683
+ /** How many images the prepared dataset holds. */
1684
+ imageCount: number;
1685
+ /** ISO-8601. The quote cannot be run after this. */
1686
+ expiresAt: string;
1687
+ }
1688
+ /**
1689
+ * Why the host substituted a checkpoint version — see
1690
+ * {@link BlockWorkflowSnapshot.modelSubstitutions}.
1691
+ *
1692
+ * - `'wrong-workflow'` — the id IS one of this ecosystem's workflow-scoped
1693
+ * versions, but for a DIFFERENT workflow (e.g. an edit-only version sent to
1694
+ * text-to-image).
1695
+ * - `'unrecognized'` — the id is in no scoped list for this ecosystem: a
1696
+ * community checkpoint, or a version retired since the app shipped.
1697
+ * - `'gated'` — the id IS offered for this workflow, but a gate rule hid it
1698
+ * from THIS viewer. The app is not wrong; the viewer may not use it.
1699
+ *
1700
+ * The host may add a reason in a later version, and the SDK's inbound
1701
+ * validator does not reject an unrecognised one — handle an unknown value as a
1702
+ * generic substitution rather than assuming the list is closed.
1703
+ */
1704
+ export type ModelSubstitutionReason = 'wrong-workflow' | 'unrecognized' | 'gated';
1705
+ /** One entry of {@link BlockWorkflowSnapshot.modelSubstitutions}. */
1706
+ export interface BlockModelSubstitution {
1707
+ /** The checkpoint version id the block asked for. */
1708
+ requested: number;
1709
+ /** The checkpoint version id that actually ran. */
1710
+ applied: number;
1711
+ reason: ModelSubstitutionReason;
1712
+ }
1713
+ /** One entry of {@link BlockWorkflowSnapshot.toolCalls}. */
1714
+ export interface BlockStepToolCall {
1715
+ id: string;
1716
+ /**
1717
+ * The SDK's inbound validator checks only that it is a string, so a value
1718
+ * the host adds later can arrive — do not assume `'function'` exhaustively.
1719
+ */
1720
+ type: 'function';
1721
+ function: {
1722
+ /** Restricted to `[A-Za-z0-9_-]`. */
1723
+ name: string;
1724
+ /** The model's arguments, as the JSON string it produced. Scanned. */
1725
+ arguments: string;
1726
+ };
1727
+ }
1728
+ /** One entry of {@link BlockWorkflowSnapshot.trainedEpochs} / {@link AppWorkflow.trainedEpochs}. */
1729
+ export interface BlockTrainedEpoch {
1730
+ /**
1731
+ * Which pass-through training `$type` produced this epoch. The SDK's inbound
1732
+ * validator checks only that it is a string, so a value the host adds later
1733
+ * can arrive — give an exhaustive `switch` a default branch.
1734
+ */
1735
+ $type: 'training' | 'imageResourceTraining';
1736
+ /** Pass as `epoch` to the `/models/train/from-orchestrator` wizard route. */
1737
+ epochNumber: number;
1738
+ }
1739
+ /**
1740
+ * The model a training run was published as —
1741
+ * {@link BlockWorkflowSnapshot.publishedModel} / {@link AppWorkflow.publishedModel}.
1742
+ */
1743
+ export interface BlockPublishedModel {
1744
+ modelId: number;
1745
+ modelVersionId: number;
1746
+ /** `false` while the model is still a draft in the wizard; `true` once published. */
1747
+ published: boolean;
1313
1748
  }
1314
1749
  /**
1315
1750
  * Which side of the settings split a field lives on.
@@ -1827,6 +2262,28 @@ export interface AppWorkflow {
1827
2262
  cost: number | null;
1828
2263
  /** ISO-8601. */
1829
2264
  createdAt: string;
2265
+ /**
2266
+ * The Civitai model a training run in this workflow was published as —
2267
+ * present once the viewer has started or finished publishing it through the
2268
+ * model wizard. Same shape and meaning as
2269
+ * {@link BlockWorkflowSnapshot.publishedModel}.
2270
+ *
2271
+ * 🔴 REQUIRES A HOST VERSION THAT EMITS IT. Absent on older hosts, and on
2272
+ * every workflow nobody has published.
2273
+ */
2274
+ publishedModel?: BlockPublishedModel;
2275
+ /**
2276
+ * The epochs of a pass-through `training` / `imageResourceTraining` step in
2277
+ * this workflow that produced a checkpoint. Same shape and rule as
2278
+ * {@link BlockWorkflowSnapshot.trainedEpochs}: only runs whose training
2279
+ * moderation status is approved are listed, the checkpoint itself is not
2280
+ * exposed, and the field is OMITTED when there are no epochs to list. Lets a
2281
+ * block offer "publish epoch n" from its queue without polling each run.
2282
+ *
2283
+ * 🔴 REQUIRES A HOST VERSION THAT EMITS IT. Absent on older hosts — treat
2284
+ * absence as "unknown", not as "no checkpoint".
2285
+ */
2286
+ trainedEpochs?: BlockTrainedEpoch[];
1830
2287
  }
1831
2288
  /**
1832
2289
  * Per-viewer gated projection of ONE image returned by `GET_IMAGES_BY_IDS`
@@ -2043,5 +2500,102 @@ export interface BlockCreatePostResult {
2043
2500
  * write, so a dismissal mid-write can never claim this code.
2044
2501
  */
2045
2502
  export type BlockCreatePostHostError = 'review-mode' | 'block is not ready' | 'sign in to post' | 'no images to post' | 'no block token' | 'declined';
2503
+ /**
2504
+ * One image of a training dataset request — an `Image` id the VIEWER owns, and
2505
+ * the caption to train it with.
2506
+ *
2507
+ * Host bounds (`blockTrainingDatasetItemsSchema`): `imageId` a positive integer,
2508
+ * `caption` at most {@link BLOCK_TRAINING_CAPTION_MAX_CHARS} characters, no other
2509
+ * keys; 1…{@link BLOCK_TRAINING_DATASET_MAX_ITEMS} items per request. A payload
2510
+ * outside them is refused by the host with `invalid training dataset` before any
2511
+ * server call. Captions are moderated like prompts.
2512
+ */
2513
+ export interface BlockTrainingDatasetItem {
2514
+ imageId: number;
2515
+ caption: string;
2516
+ }
2517
+ /**
2518
+ * Why the server left an image out of a prepared dataset. Mirrors
2519
+ * civitai/civitai's `BlockTrainingRejectionReason`.
2520
+ *
2521
+ * - `unavailable` — not found, OR not the viewer's own image. One reason for
2522
+ * both, on purpose: the server will not tell an app whether someone else's
2523
+ * image id exists.
2524
+ * - `unsupported-media` — a video or audio row.
2525
+ * - `not-eligible` — refused by moderation state or the page's maturity ceiling.
2526
+ * - `pending-scan` — not scanned yet; retry once the scan finishes.
2527
+ * - `import-failed` — the orchestrator did not accept the image.
2528
+ * - `import-unavailable` — the import timed out or the orchestrator was
2529
+ * unavailable; retryable.
2530
+ *
2531
+ * The SDK's inbound validator checks `reason` only as a STRING, so a reason the
2532
+ * host adds later still arrives — give an exhaustive `switch` a default branch.
2533
+ */
2534
+ export type BlockTrainingRejectionReason = 'unavailable' | 'unsupported-media' | 'not-eligible' | 'pending-scan' | 'import-failed' | 'import-unavailable';
2535
+ /**
2536
+ * The success payload of `TRAINING_DATASET_RESULT` — what
2537
+ * `usePrepareTrainingDataset().prepareDataset()` resolves with.
2538
+ */
2539
+ export interface BlockTrainingDatasetResult {
2540
+ /**
2541
+ * Opaque handle for {@link WorkflowBodyTraining.datasetId} (`tds_` + 32 hex
2542
+ * characters), bound to this viewer, app and install.
2543
+ */
2544
+ datasetId: string;
2545
+ /**
2546
+ * How many images were ADMITTED — always at least 1: when nothing is admitted
2547
+ * the server refuses the request instead of returning an empty dataset. The
2548
+ * server derives it; a run trains on exactly these.
2549
+ */
2550
+ count: number;
2551
+ /** The images left out, each with its reason. Empty when every image was admitted. */
2552
+ rejected: Array<{
2553
+ imageId: number;
2554
+ reason: BlockTrainingRejectionReason;
2555
+ }>;
2556
+ }
2557
+ /**
2558
+ * The HOST'S OWN refusal codes on a `TRAINING_DATASET_RESULT`'s `error`. Mirrors
2559
+ * civitai/civitai's `PREPARE_TRAINING_DATASET_HOST_ERRORS`
2560
+ * (`src/components/AppBlocks/prepareTrainingDatasetGate.ts`, civitai/civitai#5438).
2561
+ *
2562
+ * 🔴 THE SET A BLOCK MAY RELY ON, NOT THE SET IT MAY RECEIVE: `error` also carries
2563
+ * any server message verbatim (`training from apps is not enabled`, a rate limit,
2564
+ * a moderation refusal of a caption). Compare by EQUALITY against these members
2565
+ * and treat anything else as an opaque server message.
2566
+ *
2567
+ * - `review-mode` — a mod-review sandbox with "run for real" off.
2568
+ * - `block is not ready` — the host handshake has not completed.
2569
+ * - `sign in to train` — anonymous viewer; there are no images of theirs to read.
2570
+ * - `invalid training dataset` — `items` failed the host's item schema (shape,
2571
+ * count or caption length). Nothing was sent to the server.
2572
+ * - `no block token` — the host holds no block token yet.
2573
+ */
2574
+ export type BlockPrepareTrainingDatasetHostError = 'review-mode' | 'block is not ready' | 'sign in to train' | 'invalid training dataset' | 'no block token';
2575
+ /**
2576
+ * The HOST'S OWN refusal codes on a `TRAINING_RESULT`'s `error`. Mirrors
2577
+ * civitai/civitai's `RUN_TRAINING_HOST_ERRORS`
2578
+ * (`src/components/AppBlocks/runTrainingGate.ts`).
2579
+ *
2580
+ * 🔴 THE SET A BLOCK MAY RELY ON, NOT THE SET IT MAY RECEIVE: server messages
2581
+ * (an expired or already-used quote, a body that differs from the quoted one, an
2582
+ * ineligible dataset image) arrive verbatim in the same field.
2583
+ *
2584
+ * - `review-mode` — a mod-review sandbox with "run for real" off.
2585
+ * - `block is not ready` — the host handshake has not completed.
2586
+ * - `sign in to train` — anonymous viewer; there is no account to charge.
2587
+ * - `invalid training request` — the body is not a `kind: 'training'` body
2588
+ * naming a `quoteId`.
2589
+ * - `no block token` — the host holds no block token yet.
2590
+ * - `declined` — the viewer dismissed the consent dialog. 🔴 GUARANTEED to mean
2591
+ * NO RUN WAS SUBMITTED.
2592
+ * - `submission-unconfirmed` — the submit was SENT but its outcome is unknown
2593
+ * (the connection dropped, or the server attempted the run and could not
2594
+ * confirm it). 🔴 The run MAY BE RUNNING AND CHARGED. Check the viewer's
2595
+ * trainings (`useAppWorkflows()`, or send them to their Training page) before
2596
+ * offering a retry — a retry of the same body after the server did start the
2597
+ * run is a SECOND, separately charged run.
2598
+ */
2599
+ export type BlockRunTrainingHostError = 'review-mode' | 'block is not ready' | 'sign in to train' | 'invalid training request' | 'no block token' | 'declined' | 'submission-unconfirmed';
2046
2600
  export {};
2047
2601
  //# sourceMappingURL=types.d.ts.map