@civitai/blocks-react 0.69.1 → 0.71.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/README.md +227 -2
- package/dist/hooks/useBuzzWorkflow.d.ts +16 -0
- package/dist/hooks/useBuzzWorkflow.js +16 -0
- package/dist/internal/mockHost.d.ts +81 -7
- package/dist/internal/mockHost.js +267 -32
- package/dist/internal/mockWorkflowGraph.d.ts +104 -0
- package/dist/internal/mockWorkflowGraph.js +424 -0
- package/dist/testing.d.ts +1 -1
- package/dist/transport/validate.js +35 -0
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -482,7 +482,7 @@ if (quote.cost) showPrice(priceLine(quote.cost));
|
|
|
482
482
|
|
|
483
483
|
| `cost.authorFee` | Means | Where |
|
|
484
484
|
|---|---|---|
|
|
485
|
-
| a number `> 0` | that much of `total` is the app fee | `estimate()` for a `textToImage
|
|
485
|
+
| a number `> 0` | that much of `total` is the app fee | `estimate()` for a `textToImage`, registered-`step` or `workflow` body (one fee for the whole workflow), and the refusal `submit()` resolves for those kinds when a budget, spend cap or rate limit refuses the quoted price |
|
|
486
486
|
| `0` | a fee was looked up and none applies to this request | the same replies |
|
|
487
487
|
| absent | the total is **not itemised** | a host that predates the field; every snapshot of a submitted workflow (`submit` success, `poll`, `cancel`), whose `total` is the generation's realized cost with the fee charged separately; the registered-`step` `submit()` refusal for a missing orchestrator price quote, which refuses before any fee is looked up; and `customComfy`, pass-through `step` and `training` quotes, which price no fee |
|
|
488
488
|
|
|
@@ -541,6 +541,164 @@ try {
|
|
|
541
541
|
(`KnownBlockWorkflowErrorCode | (string & {})`), the host may add codes, and
|
|
542
542
|
the inbound validator accepts any string. Keep a `default` branch.
|
|
543
543
|
- The `kind: 'training'` arm does not set it.
|
|
544
|
+
- A refused `kind: 'workflow'` body sets its own three codes, in the same
|
|
545
|
+
shape — see the next section.
|
|
546
|
+
|
|
547
|
+
#### Several steps as one workflow: `kind: 'workflow'`
|
|
548
|
+
|
|
549
|
+
A `kind: 'workflow'` body submits **several orchestrator steps as one
|
|
550
|
+
workflow**, where a later step can consume an earlier step's result. It goes
|
|
551
|
+
through the hooks you already use: `useBuzzWorkflow().estimate / submit /
|
|
552
|
+
watch / cancel`, a `useBatchEstimate()` cell, and `useWorkflowBatch()`. There
|
|
553
|
+
is no new hook and no new message.
|
|
554
|
+
|
|
555
|
+
```tsx
|
|
556
|
+
import { useBuzzWorkflow } from '@civitai/blocks-react';
|
|
557
|
+
import type { WorkflowBody } from '@civitai/app-sdk/blocks';
|
|
558
|
+
|
|
559
|
+
const { estimate, submit, watch } = useBuzzWorkflow();
|
|
560
|
+
|
|
561
|
+
// Step 1 draws an image; step 2 removes the background from step 1's result.
|
|
562
|
+
const body: WorkflowBody = {
|
|
563
|
+
kind: 'workflow',
|
|
564
|
+
arguments: { prompt: 'a lighthouse at dusk' },
|
|
565
|
+
steps: [
|
|
566
|
+
{
|
|
567
|
+
name: 'draw',
|
|
568
|
+
$type: 'imageGen',
|
|
569
|
+
input: {
|
|
570
|
+
engine: 'google',
|
|
571
|
+
model: 'nano-banana-2',
|
|
572
|
+
// Read from `arguments` above.
|
|
573
|
+
prompt: { $ref: '$arguments', path: 'prompt' },
|
|
574
|
+
},
|
|
575
|
+
// An intermediate step: its image is not surfaced.
|
|
576
|
+
suppressOutput: true,
|
|
577
|
+
},
|
|
578
|
+
{
|
|
579
|
+
name: 'cutout',
|
|
580
|
+
$type: 'imageBackgroundRemoval',
|
|
581
|
+
// Read from the EARLIER step named `draw`.
|
|
582
|
+
input: { image: { $ref: 'draw', path: 'output.images[0].url' } },
|
|
583
|
+
},
|
|
584
|
+
],
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
const quote = await estimate(body);
|
|
588
|
+
showPrice(`About ${quote.cost?.total} Buzz`); // an estimate, not a maximum
|
|
589
|
+
|
|
590
|
+
const submitted = await submit(body);
|
|
591
|
+
const done = await watch(submitted.workflowId);
|
|
592
|
+
|
|
593
|
+
for (const step of done.steps ?? []) {
|
|
594
|
+
if (step.status === 'failed') showError(`${step.name} failed`);
|
|
595
|
+
}
|
|
596
|
+
// Even when `done.status === 'failed'`, earlier steps may have left images.
|
|
597
|
+
showImages(done.imageUrls ?? []);
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
**The body**
|
|
601
|
+
|
|
602
|
+
- `steps`: 1 to 8 (`BLOCK_WORKFLOW_MAX_STEPS`). Each is `{ name?, $type, input,
|
|
603
|
+
suppressOutput? }`, where `$type` is an orchestrator step type and `input` is
|
|
604
|
+
that step's own input.
|
|
605
|
+
- **A step `name`** is optional. It is a lowercase letter followed by up to 31
|
|
606
|
+
of `a-z`, `0-9`, `_` or `-`, unique within the body, and not `block-step`
|
|
607
|
+
(reserved). A step you do not name is named by its position: `$0`, `$1`, ….
|
|
608
|
+
- **A reference** is `{ $ref, path, default?, format? }` and may stand in for
|
|
609
|
+
any value in `input`, at any depth. `$ref` is `'$arguments'`, the `name` of an
|
|
610
|
+
**earlier** step, or — for an earlier step you did not name — its position
|
|
611
|
+
(`'$0'` is the first step). A step can never reference itself or a later
|
|
612
|
+
step. An object is a reference only when it has both `$ref` and `path` and no
|
|
613
|
+
other key but `default` / `format`.
|
|
614
|
+
- **Limits**: 64 references per body, inputs nested at most 32 levels, `repeat`
|
|
615
|
+
templates nested at most 4 deep, a `$type` of at most 64 characters, and 256
|
|
616
|
+
KB (262144 bytes) for every step's `input` plus `arguments` together. An
|
|
617
|
+
unknown key on the body or on a step is rejected. The app's velocity limit
|
|
618
|
+
counts one unit per step, and a `repeat` step one per iteration instead.
|
|
619
|
+
- `suppressOutput: true` marks an intermediate step. Its images are left out of
|
|
620
|
+
`snapshot.imageUrls` and out of its own `steps[i].imageUrls`. It still runs
|
|
621
|
+
and is still charged.
|
|
622
|
+
- There is **no `maxBuzz`**. The spend ceiling is the app's per-generation
|
|
623
|
+
budget × the number of steps, enforced by the host.
|
|
624
|
+
- Any orchestrator `$type` is accepted except the platform-internal ones:
|
|
625
|
+
scanners, moderation classifiers, hashing, web scrape/search, and the retired
|
|
626
|
+
`textToImage` step type.
|
|
627
|
+
- **`repeat` over `arguments` only, at most 16 iterations**
|
|
628
|
+
(`BLOCK_WORKFLOW_REPEAT_MAX_ITERATIONS`, nested repeats multiplied). Its input
|
|
629
|
+
is exactly `{ for: { $ref: '$arguments', path, as }, template: { $type,
|
|
630
|
+
name?, input? } }`, where `path` names an array in `arguments`. Inside the
|
|
631
|
+
template, `as` is a source a reference may name; it may not equal a step
|
|
632
|
+
name. The template's `$type` is held to the denylist too. A `repeat` over
|
|
633
|
+
another step's output is refused, because its length is unknown until that
|
|
634
|
+
step has run. The single-step pass-through `kind: 'step'` arm refuses
|
|
635
|
+
`repeat` outright.
|
|
636
|
+
- **`merge`**: every entry of `input.fields` needs a string `$ref` (`$arguments`
|
|
637
|
+
or an earlier step, never a repeat variable) and a `reduce`; each counts
|
|
638
|
+
toward the 64-reference limit.
|
|
639
|
+
- `WithWorkflowStepRefs<T>` (from `@civitai/app-sdk/blocks`) types an input you
|
|
640
|
+
already have a type for while still allowing references in it.
|
|
641
|
+
|
|
642
|
+
**🔴 Three things about the price**
|
|
643
|
+
|
|
644
|
+
1. **The estimate is not a maximum.** When a step reads from an earlier step,
|
|
645
|
+
the orchestrator re-prices it once those values are known and charges the
|
|
646
|
+
difference. Never show the estimate as "you will pay at most".
|
|
647
|
+
2. **A workflow the host cannot price still proceeds.** `submit()` reserves the
|
|
648
|
+
ceiling above instead of a quote. Two exceptions are refused without a
|
|
649
|
+
quote, before any reservation: a workflow with a training step (a
|
|
650
|
+
`training-quote-*` `errorCode`, as on the single-step arm), and a workflow
|
|
651
|
+
with a `repeat` step at any depth (`workflow_repeat_unbounded`; a retry may
|
|
652
|
+
succeed).
|
|
653
|
+
3. **One author fee per workflow**, not one per step, on the quoted base. The
|
|
654
|
+
estimate's `cost.authorFee` is that single fee, already inside `cost.total`.
|
|
655
|
+
If the workflow bills more than the quote, the fee charged is held to the
|
|
656
|
+
fee shown. A workflow that does not succeed has its **whole** fee reversed,
|
|
657
|
+
even if earlier steps delivered output.
|
|
658
|
+
|
|
659
|
+
**The result**
|
|
660
|
+
|
|
661
|
+
`snapshot.steps` lists every step in order, as `{ name, $type, status,
|
|
662
|
+
imageUrls?, output?, error? }`. Read it from `poll()`, `watch()` and
|
|
663
|
+
`cancel()`; do not rely on it being on the `submit()` reply. The steps a
|
|
664
|
+
`repeat` step generates follow it, named `<repeat name>.<n>`. The top-level
|
|
665
|
+
fields keep their meaning: `status` and `cost` are the whole workflow's, and
|
|
666
|
+
`imageUrls` is the union of the steps that did not set `suppressOutput`.
|
|
667
|
+
|
|
668
|
+
🔴 **A `failed` workflow is not an empty one.** Steps that succeeded before the
|
|
669
|
+
failing step stay charged and keep their results, so a snapshot with `status:
|
|
670
|
+
'failed'` can carry `cost.total > 0` and usable `imageUrls`. Show what did
|
|
671
|
+
finish, and do not tell the viewer nothing was charged — though the app fee is
|
|
672
|
+
reversed for any workflow that does not succeed.
|
|
673
|
+
|
|
674
|
+
**Refusals**
|
|
675
|
+
|
|
676
|
+
A body the host will not run is refused before anything is priced or reserved:
|
|
677
|
+
`estimate()` rejects with `WorkflowEstimateError`, `submit()` with
|
|
678
|
+
`WorkflowSubmitError`, and a batch estimate returns it as that cell. Branch on
|
|
679
|
+
`err.snapshot.errorCode`:
|
|
680
|
+
|
|
681
|
+
| `errorCode` | Means |
|
|
682
|
+
|---|---|
|
|
683
|
+
| `'workflow_too_many_steps'` | more than 8 steps |
|
|
684
|
+
| `'workflow_step_denied'` | a step names a platform-internal `$type` |
|
|
685
|
+
| `'workflow_ref_invalid'` | a reference points at the step itself, a later step, or nothing; a step name breaks the rules above; a `repeat` or `merge` is malformed; or a repeat `as` name is also a step name |
|
|
686
|
+
| `'workflow_repeat_unbounded'` | a `repeat` iterates something other than an array in `arguments`, or more than 16 times (nested repeats multiplied); or the workflow has a `repeat` and no price quote could be had (retrying may work) |
|
|
687
|
+
|
|
688
|
+
- **Requires a host that knows the kind.** An older host refuses the body.
|
|
689
|
+
- **Test it locally.** The mock host prices each step with
|
|
690
|
+
`generation.costPerGen` (a function gets the step as its second argument) and
|
|
691
|
+
multiplies it by a `repeat` step's iteration count, then adds
|
|
692
|
+
`generation.authorFee` once; refuses with the four codes above, in the host's
|
|
693
|
+
order; reports
|
|
694
|
+
`steps` on every poll, finishing them in order; and
|
|
695
|
+
`generation.failWorkflowStep: '<name>'` (`'$<index>'` for an unnamed step)
|
|
696
|
+
makes one step fail, so you can render a partial failure. The mock checks
|
|
697
|
+
where a reference points, not whether its `path` exists; it never re-prices;
|
|
698
|
+
and it does not expand a `repeat` step into generated `<name>.<n>` steps. It
|
|
699
|
+
quotes every workflow unless you set `generation.workflowUnpriced: true`,
|
|
700
|
+
which simulates no quote: a workflow with a training step or a `repeat` is
|
|
701
|
+
refused, and any other is reserved at `buzzBudget` × steps with no fee.
|
|
544
702
|
|
|
545
703
|
> **Estimate must mirror submit** (gotcha #59): build the params for `estimate`
|
|
546
704
|
> with the *exact* same logic as `submit` — same seed decision especially. The
|
|
@@ -704,7 +862,10 @@ Any other `'timeout'` is a slow host.
|
|
|
704
862
|
|
|
705
863
|
**Mock host:** `createMockHost` and `Harness` price each cell exactly as they
|
|
706
864
|
price one `estimate()` of that body (`costPerGen` and `failEstimate` apply per
|
|
707
|
-
cell).
|
|
865
|
+
cell). Like the host, they refuse the **whole call** while the token lacks
|
|
866
|
+
`ai:write:budgeted` — `'failed'`, with `hostError` `block lacks
|
|
867
|
+
ai:write:budgeted scope` — so pass `consentGranted: true`, or drive
|
|
868
|
+
`requestConsent()` and estimate again once the scope lands. Set `generation.batchEstimate` to `'unsupported'` to test the fallback,
|
|
708
869
|
or to `'silent'` to test a call that never gets a reply (pass a short
|
|
709
870
|
`timeoutMs` in a test). Set `generation.batchEstimateTimedOutCells` to a list
|
|
710
871
|
of cell indices to answer those cells the way the host does when its time
|
|
@@ -2071,6 +2232,46 @@ on every parameter change. Prompting there would open a consent dialog with no
|
|
|
2071
2232
|
user gesture behind it, once per edit. A failed `estimate()` rejects exactly as
|
|
2072
2233
|
it always has; show no price and let `submit()` do the asking.
|
|
2073
2234
|
|
|
2235
|
+
The same goes for `useBatchEstimate()`. Until the viewer grants
|
|
2236
|
+
`ai:write:budgeted` the host refuses **every** estimate, single and batch, with
|
|
2237
|
+
`block lacks ai:write:budgeted scope` (`WorkflowEstimateError` / `BatchEstimateError`,
|
|
2238
|
+
code `'failed'`). To show a price before the first Generate, ask on a user
|
|
2239
|
+
gesture and estimate again once the grant lands:
|
|
2240
|
+
|
|
2241
|
+
```tsx
|
|
2242
|
+
import { useEffect, useState } from 'react';
|
|
2243
|
+
import { useBlockToken, useBuzzWorkflow, useRequestConsent } from '@civitai/blocks-react';
|
|
2244
|
+
import type { WorkflowBody } from '@civitai/app-sdk/blocks';
|
|
2245
|
+
|
|
2246
|
+
function Price({ body }: { body: WorkflowBody }) {
|
|
2247
|
+
const token = useBlockToken();
|
|
2248
|
+
const { requestConsent } = useRequestConsent();
|
|
2249
|
+
const { estimate } = useBuzzWorkflow();
|
|
2250
|
+
const [price, setPrice] = useState<number | null>(null);
|
|
2251
|
+
const canPrice = token.scopes.includes('ai:write:budgeted');
|
|
2252
|
+
|
|
2253
|
+
// Keyed on `canPrice`, so the quote is fetched again when the grant lands.
|
|
2254
|
+
useEffect(() => {
|
|
2255
|
+
if (!canPrice) return;
|
|
2256
|
+
estimate(body).then((s) => setPrice(s.cost?.total ?? null), () => setPrice(null));
|
|
2257
|
+
}, [canPrice, body, estimate]);
|
|
2258
|
+
|
|
2259
|
+
if (!canPrice) {
|
|
2260
|
+
return (
|
|
2261
|
+
<button onClick={() => requestConsent({ scopes: ['ai:write:budgeted'] })}>
|
|
2262
|
+
Allow generations
|
|
2263
|
+
</button>
|
|
2264
|
+
);
|
|
2265
|
+
}
|
|
2266
|
+
return <span>{price === null ? '…' : `${price} Buzz`}</span>;
|
|
2267
|
+
}
|
|
2268
|
+
```
|
|
2269
|
+
|
|
2270
|
+
The mock host enforces this too (since `@civitai/blocks-react` 0.70.0, #608): with
|
|
2271
|
+
the default `consentGranted: false` its estimate, batch estimate and submit
|
|
2272
|
+
refuse exactly as the host does, `REQUEST_CONSENT` grants and pushes the
|
|
2273
|
+
refreshed token, and `consentGrantable: false` keeps the refusal in place.
|
|
2274
|
+
|
|
2074
2275
|
```tsx
|
|
2075
2276
|
// This is the whole thing. No try/catch around a consent prompt, no watching
|
|
2076
2277
|
// useBlockToken().scopes, no manual retry.
|
|
@@ -2622,6 +2823,7 @@ MockHostScenarioPatch
|
|
|
2622
2823
|
MockSharedScenario
|
|
2623
2824
|
MockSharedSeed
|
|
2624
2825
|
MockStorageScenario
|
|
2826
|
+
MockWorkflowStepInfo
|
|
2625
2827
|
PopoverShimHandle
|
|
2626
2828
|
PopoverShimOptions
|
|
2627
2829
|
```
|
|
@@ -2653,6 +2855,29 @@ host.setScenario({ failMode: 'none' }); // live-tune mid-test
|
|
|
2653
2855
|
uninstall();
|
|
2654
2856
|
```
|
|
2655
2857
|
|
|
2858
|
+
🔴 **The mock enforces the spend scope, as the host does** (since `0.70.0`,
|
|
2859
|
+
[#608](https://github.com/civitai/civitai-app-starters/issues/608)).
|
|
2860
|
+
`consentGranted` defaults to `false` — production's first token does not carry
|
|
2861
|
+
`ai:write:budgeted` — and while it is `false`, an estimate, a batch estimate and
|
|
2862
|
+
a submit are refused with `block lacks ai:write:budgeted scope`, each in the
|
|
2863
|
+
shape the host sends:
|
|
2864
|
+
|
|
2865
|
+
| Call | What your code sees before consent |
|
|
2866
|
+
|---|---|
|
|
2867
|
+
| `estimate()` | rejects `WorkflowEstimateError`, code `'failed'`, reason on `err.snapshot.error` |
|
|
2868
|
+
| `estimateBatch()` | rejects `BatchEstimateError`, code `'failed'`, reason on `err.hostError` |
|
|
2869
|
+
| `submit()` | prompts for consent, the mock grants it and pushes a refreshed token, and the call is retried once with the **same** idempotency key — it resolves, and is charged once |
|
|
2870
|
+
|
|
2871
|
+
`consentGrantable: false` (or `?consent=ungrantable`) keeps the refusal: the
|
|
2872
|
+
prompt is answered with `CONSENT_UNAVAILABLE` and `submit()` rejects with code
|
|
2873
|
+
`'exception'`.
|
|
2874
|
+
|
|
2875
|
+
**Migrating a test:** one that estimates or submits and is not *about* consent
|
|
2876
|
+
passes `consentGranted: true` (or uses `?consent=granted` in a harness). One that
|
|
2877
|
+
is about consent drives it: estimate, see the refusal, call
|
|
2878
|
+
`requestConsent({ scopes: ['ai:write:budgeted'] })`, wait for
|
|
2879
|
+
`useBlockToken().scopes` to include it, estimate again.
|
|
2880
|
+
|
|
2656
2881
|
### Testing overlay elements
|
|
2657
2882
|
|
|
2658
2883
|
`<civitai-menu>`, and anything else that opens a panel, live in an environment
|
|
@@ -698,6 +698,22 @@ export interface UseBuzzWorkflow {
|
|
|
698
698
|
* the paragraph above records, one member over: an incomplete description of a
|
|
699
699
|
* union, trusted because it read as authoritative.
|
|
700
700
|
*
|
|
701
|
+
* 🔴 A MULTI-STEP `kind: 'workflow'` BODY CHANGES WHAT THE PRICE MEANS — again a
|
|
702
|
+
* capability claim, so it is stated here. `WorkflowBodyWorkflow` submits several
|
|
703
|
+
* orchestrator steps as one workflow through these same calls, and:
|
|
704
|
+
* - `estimate()`'s `cost.total` is NOT a maximum: the orchestrator re-prices a
|
|
705
|
+
* step once the values it references are known and charges the difference;
|
|
706
|
+
* - a workflow the host cannot price still proceeds on `submit()`, reserved at
|
|
707
|
+
* the app's per-generation budget × the number of steps (one with a training
|
|
708
|
+
* step, or with a `repeat` step at any depth, is refused instead);
|
|
709
|
+
* - one author fee applies to the whole workflow, not one per step, and it is
|
|
710
|
+
* reversed whole if the workflow does not succeed;
|
|
711
|
+
* - `repeat` iterates only an array in the body's `arguments`, at most 16
|
|
712
|
+
* times, nested repeats multiplied;
|
|
713
|
+
* - a `failed` result can carry `cost.total > 0` and usable `imageUrls`, because
|
|
714
|
+
* steps that succeeded before the failing one stay charged — read
|
|
715
|
+
* `result.steps`.
|
|
716
|
+
*
|
|
701
717
|
* @returns `{ estimate, submit, poll, watch, cancel, status, result, error }`.
|
|
702
718
|
*
|
|
703
719
|
* @example
|
|
@@ -523,6 +523,22 @@ export class WorkflowSubmitError extends Error {
|
|
|
523
523
|
* the paragraph above records, one member over: an incomplete description of a
|
|
524
524
|
* union, trusted because it read as authoritative.
|
|
525
525
|
*
|
|
526
|
+
* 🔴 A MULTI-STEP `kind: 'workflow'` BODY CHANGES WHAT THE PRICE MEANS — again a
|
|
527
|
+
* capability claim, so it is stated here. `WorkflowBodyWorkflow` submits several
|
|
528
|
+
* orchestrator steps as one workflow through these same calls, and:
|
|
529
|
+
* - `estimate()`'s `cost.total` is NOT a maximum: the orchestrator re-prices a
|
|
530
|
+
* step once the values it references are known and charges the difference;
|
|
531
|
+
* - a workflow the host cannot price still proceeds on `submit()`, reserved at
|
|
532
|
+
* the app's per-generation budget × the number of steps (one with a training
|
|
533
|
+
* step, or with a `repeat` step at any depth, is refused instead);
|
|
534
|
+
* - one author fee applies to the whole workflow, not one per step, and it is
|
|
535
|
+
* reversed whole if the workflow does not succeed;
|
|
536
|
+
* - `repeat` iterates only an array in the body's `arguments`, at most 16
|
|
537
|
+
* times, nested repeats multiplied;
|
|
538
|
+
* - a `failed` result can carry `cost.total > 0` and usable `imageUrls`, because
|
|
539
|
+
* steps that succeeded before the failing one stay charged — read
|
|
540
|
+
* `result.steps`.
|
|
541
|
+
*
|
|
526
542
|
* @returns `{ estimate, submit, poll, watch, cancel, status, result, error }`.
|
|
527
543
|
*
|
|
528
544
|
* @example
|
|
@@ -77,7 +77,21 @@ export type MockCannedImageScan = 'scanned' | {
|
|
|
77
77
|
* A per-generation cost: a fixed number, or a function of the submitted
|
|
78
78
|
* {@link WorkflowBody} (so a dev can vary cost by model / step count).
|
|
79
79
|
*/
|
|
80
|
-
export type CostSpec = number | ((req: WorkflowBody) => number);
|
|
80
|
+
export type CostSpec = number | ((req: WorkflowBody, step?: MockWorkflowStepInfo) => number);
|
|
81
|
+
/**
|
|
82
|
+
* The step a {@link CostSpec} function is pricing, passed as its SECOND
|
|
83
|
+
* argument when the body is `kind: 'workflow'`: the mock calls
|
|
84
|
+
* {@link MockGenerationScenario.costPerGen} once per step and sums the results.
|
|
85
|
+
* `undefined` for every other kind, which is priced with one call.
|
|
86
|
+
*/
|
|
87
|
+
export interface MockWorkflowStepInfo {
|
|
88
|
+
/** Zero-based position in the body's `steps`. */
|
|
89
|
+
index: number;
|
|
90
|
+
/** The step's `name`, or `'$<index>'` for an unnamed step. */
|
|
91
|
+
name: string;
|
|
92
|
+
$type: string;
|
|
93
|
+
input: Record<string, unknown>;
|
|
94
|
+
}
|
|
81
95
|
/**
|
|
82
96
|
* A result image url: a fixed string, or a function of the submitted body
|
|
83
97
|
* (so a dev can echo the prompt into a placeholder).
|
|
@@ -91,6 +105,12 @@ export interface MockGenerationScenario {
|
|
|
91
105
|
/**
|
|
92
106
|
* Cost reported on `ESTIMATE_RESULT` + the succeeded snapshot. A number, or
|
|
93
107
|
* a `(body) => number`. Overrides the legacy top-level `cost`. Default `8`.
|
|
108
|
+
*
|
|
109
|
+
* A `kind: 'workflow'` body is priced PER STEP: this is called once for each
|
|
110
|
+
* step (a function receives the step as its second argument, see
|
|
111
|
+
* {@link MockWorkflowStepInfo}), multiplied by that step's generations (a
|
|
112
|
+
* `repeat` counts its iterations, nested repeats multiplied), and summed, so a
|
|
113
|
+
* plain number prices an N-step workflow with no `repeat` at N times it.
|
|
94
114
|
*/
|
|
95
115
|
costPerGen?: CostSpec;
|
|
96
116
|
/**
|
|
@@ -100,9 +120,10 @@ export interface MockGenerationScenario {
|
|
|
100
120
|
* real host does. A number, or a `(body) => number`. Default `0`, which is
|
|
101
121
|
* reported as `authorFee: 0` ("no fee on this request").
|
|
102
122
|
*
|
|
103
|
-
* Like the real host, only a `textToImage
|
|
104
|
-
* itemised; `customComfy`, pass-through `step` and `training` quotes
|
|
105
|
-
* `authorFee`.
|
|
123
|
+
* Like the real host, only a `textToImage`, registered-`step` or `workflow`
|
|
124
|
+
* body is itemised; `customComfy`, pass-through `step` and `training` quotes
|
|
125
|
+
* carry no `authorFee`. A `workflow` body is charged this ONCE, however many
|
|
126
|
+
* steps it has. The succeeded snapshot's `cost.total` stays {@link costPerGen}
|
|
106
127
|
* (a submitted workflow reports the generation's own cost), while a simulated
|
|
107
128
|
* {@link MockBuzzScenario.balance} is debited `costPerGen + authorFee`.
|
|
108
129
|
*
|
|
@@ -265,7 +286,44 @@ export interface MockGenerationScenario {
|
|
|
265
286
|
* Default: unset.
|
|
266
287
|
*/
|
|
267
288
|
submitCapRefusal?: string | true;
|
|
268
|
-
/**
|
|
289
|
+
/**
|
|
290
|
+
* Make ONE step of a `kind: 'workflow'` run fail, so a block can exercise a
|
|
291
|
+
* PARTIAL failure. Name the step as its snapshot entry names it: its `name`,
|
|
292
|
+
* or `'$<index>'` for a step the body left unnamed. The poll that finishes
|
|
293
|
+
* the workflow then answers `status: 'failed'` with:
|
|
294
|
+
* - `steps`: every step before it `'succeeded'`, the named step `'failed'`
|
|
295
|
+
* with an `error`, every step after it `'canceled'`;
|
|
296
|
+
* - `cost.total`: the sum of the steps that succeeded (they stay charged), so
|
|
297
|
+
* it is `> 0` unless the FIRST step is the one that fails;
|
|
298
|
+
* - `imageUrls`: the images of the succeeded steps that did not set
|
|
299
|
+
* `suppressOutput`.
|
|
300
|
+
*
|
|
301
|
+
* What the real orchestrator reports for the steps AFTER a failed one is its
|
|
302
|
+
* own business; `'canceled'` here is the mock's stand-in for "did not run".
|
|
303
|
+
*
|
|
304
|
+
* Ignored by every other kind, and when no step matches. Takes precedence
|
|
305
|
+
* over {@link workflowOutcome} for the workflow it applies to. Live-tunable
|
|
306
|
+
* via `setScenario({ generation: { failWorkflowStep } })`. Default: unset.
|
|
307
|
+
*/
|
|
308
|
+
failWorkflowStep?: string;
|
|
309
|
+
/**
|
|
310
|
+
* Simulate the host getting NO price quote for a `kind: 'workflow'` body. The
|
|
311
|
+
* host then refuses, before any reservation, a workflow with a training step
|
|
312
|
+
* (`errorCode: 'training-quote-unavailable'`) or a `repeat` step at any depth
|
|
313
|
+
* (`'workflow_repeat_unbounded'`); any other workflow proceeds reserved at the
|
|
314
|
+
* CEILING — {@link MockHostOptions.buzzBudget} × the number of steps — which
|
|
315
|
+
* the estimate reports as `cost.total`, with `authorFee: 0` (no quote, no
|
|
316
|
+
* fee base). Ignored by every other kind. Live-tunable via
|
|
317
|
+
* `setScenario({ generation: { workflowUnpriced } })`. Default: unset (every
|
|
318
|
+
* workflow is quoted at its per-step `costPerGen`).
|
|
319
|
+
*/
|
|
320
|
+
workflowUnpriced?: boolean;
|
|
321
|
+
/**
|
|
322
|
+
* A single result image url (or `(body) => url`). For a `kind: 'workflow'`
|
|
323
|
+
* run this (and {@link images}) is the LAST step that did not set
|
|
324
|
+
* `suppressOutput`; each earlier visible step gets its own labelled
|
|
325
|
+
* placeholder.
|
|
326
|
+
*/
|
|
269
327
|
image?: ImageSpec;
|
|
270
328
|
/**
|
|
271
329
|
* Multiple result image urls (or `(body) => url[]`). Takes precedence over
|
|
@@ -327,8 +385,14 @@ export declare const MOCK_TRAINING_BOUNDS: {
|
|
|
327
385
|
readonly captionMaxChars: 1000;
|
|
328
386
|
readonly maxBuzzPerRun: 5000;
|
|
329
387
|
};
|
|
330
|
-
/**
|
|
331
|
-
|
|
388
|
+
/**
|
|
389
|
+
* The real server's refusal for ANY money call from a token without the spend
|
|
390
|
+
* scope: civitai's `estimateWorkflow`, `estimateWorkflowBatch`, `submitWorkflow`
|
|
391
|
+
* and the training procedures all throw `FORBIDDEN` with exactly this message
|
|
392
|
+
* BEFORE any pricing, rate-limit or budget work. ONE constant for every mock
|
|
393
|
+
* path that refuses on `consentGranted === false`, so the copies cannot drift.
|
|
394
|
+
*/
|
|
395
|
+
export declare const MOCK_SPEND_SCOPE_ERROR = "block lacks ai:write:budgeted scope";
|
|
332
396
|
/** The real server's refusal for an unknown, expired or spent quote on `RUN_TRAINING`. */
|
|
333
397
|
export declare const MOCK_TRAINING_QUOTE_GONE_ERROR = "training quote not found, expired or already used \u2014 estimate again";
|
|
334
398
|
/** The real server's refusal for a training estimate naming a dataset it does not hold. */
|
|
@@ -535,6 +599,16 @@ export interface MockHostOptions {
|
|
|
535
599
|
* real mint WITHHOLDS it until the viewer consents, so this defaults to
|
|
536
600
|
* `false` — the first token carries NO budgeted scope, and `REQUEST_CONSENT`
|
|
537
601
|
* grants it + pushes a `TOKEN_REFRESH` (the lazy-consent round-trip).
|
|
602
|
+
*
|
|
603
|
+
* 🔴 WHILE IT IS `false` THE MOCK ENFORCES THE SCOPE, as the real server does:
|
|
604
|
+
* `ESTIMATE_WORKFLOW`, `ESTIMATE_WORKFLOW_BATCH` and `SUBMIT_WORKFLOW` (and the
|
|
605
|
+
* training paths) refuse the whole call with `block lacks ai:write:budgeted
|
|
606
|
+
* scope`, in the shape the page host sends for each. `submit()` recovers on
|
|
607
|
+
* its own (its consent prompt-and-retry grants and re-sends with the same
|
|
608
|
+
* idempotency key); `estimate()` and the batch estimate do NOT retry, so a
|
|
609
|
+
* block must call `requestConsent({ scopes: ['ai:write:budgeted'] })` and
|
|
610
|
+
* estimate again once the grant lands. A test that is not ABOUT consent sets
|
|
611
|
+
* this to `true`.
|
|
538
612
|
*/
|
|
539
613
|
consentGranted?: boolean;
|
|
540
614
|
/**
|