@civitai/sdk 0.19.1 → 0.20.1

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 CHANGED
@@ -559,7 +559,7 @@ A `kind: 'workflow'` body runs several steps as **one** workflow, and a later
559
559
  step can consume an earlier step's result through a `{ $ref, path }` reference:
560
560
 
561
561
  ```ts
562
- import { initialize, type WorkflowBody } from '@civitai/sdk';
562
+ import { estimatedCost, initialize, type WorkflowBody } from '@civitai/sdk';
563
563
 
564
564
  const app = await initialize();
565
565
 
@@ -587,11 +587,13 @@ const body: WorkflowBody = {
587
587
 
588
588
  // 1. Price it. Nothing is charged or reserved.
589
589
  const estimate = await app.workflows.estimate(body);
590
- if (estimate.status === 'failed') {
590
+ // A priced estimate is not 'succeeded' ('pending' in practice): read the price, not the status.
591
+ const price = estimatedCost(estimate);
592
+ if (price === null) {
591
593
  // A refusal RESOLVES — see below.
592
594
  throw new Error(estimate.error ?? 'This workflow could not be priced.');
593
595
  }
594
- if (!(await confirmSpend(estimate.cost?.total))) throw new Error('Not confirmed.');
596
+ if (!(await confirmSpend(price))) throw new Error('Not confirmed.');
595
597
 
596
598
  // 2. Run it. One key per press of the button, reused on every retry.
597
599
  const idempotencyKey = crypto.randomUUID();
@@ -617,7 +619,7 @@ submitted. `app.workflows` has no call for that confirmation step.
617
619
 
618
620
  | Call | Does |
619
621
  |---|---|
620
- | `estimate(body)` | Prices the body. `snapshot.cost.total` is whole Buzz |
622
+ | `estimate(body)` | Prices the body. `estimatedCost(snapshot)` is the price in whole Buzz, or `null` when there is none |
621
623
  | `submit(body, { idempotencyKey })` | Runs it, spending the viewer's Buzz |
622
624
  | `get(workflowId)` | The workflow as it stands now |
623
625
  | `watch(workflowId)` | An async iterator: the workflow now, then each time it changes, until a final status |
@@ -633,13 +635,19 @@ branch on the snapshot rather than wrapping the call in `try`:
633
635
 
634
636
  ```ts
635
637
  const estimate = await app.workflows.estimate(body);
636
- if (estimate.status === 'failed') {
638
+ if (estimatedCost(estimate) === null) {
637
639
  estimate.error; // a sentence for the viewer, e.g. which step is at fault
638
640
  estimate.errorCode; // a stable code to branch on, when the refusal defines one
639
- estimate.cost; // absent: nothing was priced
640
641
  }
641
642
  ```
642
643
 
644
+ `estimatedCost` is the check to use: the price when the estimate has one, else
645
+ `null`. An estimate is priced when its `status` is not `'failed'` and it carries
646
+ a numeric `cost.total`. 🔴 **Do not require `'succeeded'`**: nothing runs on an
647
+ estimate, so a priced one comes back with a status that has not finished
648
+ (`'pending'` in practice), and a check for `'succeeded'` treats every price as
649
+ missing. `0` is a price.
650
+
643
651
  Three things follow from that shape:
644
652
 
645
653
  - **A refused estimate has no `cost`.** An estimate the host could not price
@@ -1038,9 +1046,15 @@ Where it differs from the server on purpose:
1038
1046
 
1039
1047
  `fake.calls` records every request and answer. Options also cover smaller `limits`,
1040
1048
  `consent` (revoked scopes, or `'unavailable'`), `appStatus`, `disabled` and a custom
1041
- `moderate`; by default the scanner refuses the words in `FAKE_STORAGE_MODERATION`. It
1042
- does not model moderator hides or the review-preview namespace, and at the `Http`
1043
- seam there is no URL resolution — the client refuses the segments a URL would
1049
+ `moderate`; by default the scanner refuses the words in `FAKE_STORAGE_MODERATION`.
1050
+ That default is the server's enforced moderation. The host picks each app's
1051
+ moderation mode for shared data, and enforce is not the default there: expect
1052
+ shadow (scanned after the write) or off, where neither a hit nor a scanner outage
1053
+ refuses a write. So never rely on a refusal to keep content out of shared data:
1054
+ a write the fake refuses may land on the server, and other viewers read it. Pass
1055
+ `moderate: () => null` to test what such an app sees. The fake does not model
1056
+ moderator hides or the review-preview namespace, and at the `Http` seam there is no
1057
+ URL resolution — the client refuses the segments a URL would
1044
1058
  rewrite (`''`, `.`, `..`) before sending.
1045
1059
 
1046
1060
  ### The legacy key/value stores
@@ -22,6 +22,13 @@ import { createDocumentStorage } from './index.js';
22
22
  * versions of its own, so which ones a viewer owns is `FakeStorageViewer.modelVersions`
23
23
  * (default none).
24
24
  *
25
+ * Moderation is the server's ENFORCE mode, which the contract's moderation cases
26
+ * describe: a hit refuses `content_rejected` and a scanner outage
27
+ * `moderation_unavailable`. The host picks each app's shared-data moderation mode,
28
+ * and enforce is not its default: in `shadow` (the scan runs after the write) or `off`
29
+ * (no scan) neither a hit nor an outage refuses the write. The fake has no mode of its
30
+ * own: `moderate: () => null` is what a shadow or off app sees.
31
+ *
25
32
  * NOT MODELLED: moderator hides (a hidden document reads as not found on the server;
26
33
  * nothing here hides one), moderator exemption from the scanner, the review-preview
27
34
  * namespace, the token middleware's other refusals (revoked instance, context
package/dist/index.d.ts CHANGED
@@ -15,7 +15,7 @@ export { SubListingError } from './sub-listings/index.js';
15
15
  export type { MySubListing, SubListingCallOptions, SubListingContentRating, SubListingErrorCode, SubListingStatus, SubListingsClient, SubListingUpsertInput, SubListingUpsertResult, SubListingWithdrawResult, } from './sub-listings/index.js';
16
16
  export { StorageError } from './doc-store/index.js';
17
17
  export type { DocumentAuthor, DocumentCallOptions, DocumentListQuery, DocumentPage, DocumentStorage, DocumentWhereValue, StorageCollection, StorageCollectionInfo, StorageDocument, StorageErrorCode, StorageIndexType, StorageInfo, StorageLimits, StorageUsage, WriteOptions, } from './doc-store/index.js';
18
- export { SubmissionUnconfirmedError } from './workflows/index.js';
18
+ export { estimatedCost, SubmissionUnconfirmedError } from './workflows/index.js';
19
19
  export type { WorkflowCallOptions, WorkflowGetOptions, WorkflowListQuery, WorkflowListResult, WorkflowSubmitOptions, WorkflowWatchOptions, WorkflowsClient, } from './workflows/index.js';
20
20
  export type { AiToolkitLrScheduler, AiToolkitOptimizerType, AiToolkitPlainEcosystem, AiToolkitTrainingParams, AiToolkitTrainingParamsBase, AppWorkflow, AppWorkflowImage, BlockModel3dOutput, BlockModelSubstitution, BlockPublishedModel, BlockSourceImage, BlockStepQuote, BlockStepToolCall, BlockTextToImageParams, BlockTrainedEpoch, BlockTrainingDatasetItem, BlockTrainingQuote, BlockWorkflowSnapshot, BlockWorkflowStepSnapshot, BuzzAccountType, InlineComfyNode, KnownBlockWorkflowErrorCode, ModelSubstitutionReason, WithWorkflowStepRefs, WorkflowBody, WorkflowBodyCustomComfy, WorkflowBodyCustomComfyInline, WorkflowBodyCustomComfyRecipe, WorkflowBodyPassThroughStep, WorkflowBodyStep, WorkflowBodyTextToImage, WorkflowBodyTraining, WorkflowBodyWorkflow, WorkflowBodyWorkflowStep, WorkflowStepRef, } from './workflows/types.generated.js';
21
21
  export { isTerminal, isTerminalStatus } from './orchestration/index.js';
package/dist/index.js CHANGED
@@ -18,7 +18,7 @@ export { createSignIn, SignInError } from './sign-in/index.js';
18
18
  export { SCOPES } from './session/index.js';
19
19
  export { SubListingError } from './sub-listings/index.js';
20
20
  export { StorageError } from './doc-store/index.js';
21
- export { SubmissionUnconfirmedError } from './workflows/index.js';
21
+ export { estimatedCost, SubmissionUnconfirmedError } from './workflows/index.js';
22
22
  export { isTerminal, isTerminalStatus } from './orchestration/index.js';
23
23
  export { createHost } from './host/index.js';
24
24
  export { ApiError } from './http/index.js';
@@ -1,2 +1,2 @@
1
1
  /** This package's version, reported to the host as `sdkVersion` in `BLOCK_READY`. */
2
- export declare const VERSION = "0.19.1";
2
+ export declare const VERSION = "0.20.1";
@@ -1,3 +1,3 @@
1
1
  /* AUTOGENERATED from package.json by scripts/gen-version.mjs. DO NOT EDIT. */
2
2
  /** This package's version, reported to the host as `sdkVersion` in `BLOCK_READY`. */
3
- export const VERSION = "0.19.1";
3
+ export const VERSION = "0.20.1";
@@ -28,6 +28,25 @@ export declare class SubmissionUnconfirmedError extends CivitaiError {
28
28
  readonly snapshot: BlockWorkflowSnapshot | undefined;
29
29
  constructor(snapshot: BlockWorkflowSnapshot | undefined);
30
30
  }
31
+ /**
32
+ * The price an {@link WorkflowsClient.estimate} reply quotes, in whole Buzz, or
33
+ * `null` when it quotes none.
34
+ *
35
+ * An estimate is priced when its `status` is not `'failed'` AND it carries a
36
+ * numeric `cost.total`. That is the whole rule, and it is the one to branch on:
37
+ * - Do not require `'succeeded'`. Nothing runs on an estimate, so a priced one
38
+ * comes back with a status that has not finished (`'pending'` in practice),
39
+ * and a check for `'succeeded'` reads every price as missing.
40
+ * - `0` is a price.
41
+ * - A `'failed'` snapshot is never priced, even one carrying a `cost.total`: a
42
+ * budget refusal carries the total it would not charge.
43
+ * - A snapshot with no numeric `cost.total` is not priced, whatever its status.
44
+ *
45
+ * `null` does not predict a refused {@link WorkflowsClient.submit}: a body the
46
+ * host cannot price can still be submitted. Read `error` / `errorCode` on the
47
+ * snapshot for why.
48
+ */
49
+ export declare function estimatedCost(snapshot: Pick<BlockWorkflowSnapshot, 'status' | 'cost'>): number | null;
31
50
  export interface WorkflowCallOptions {
32
51
  signal?: AbortSignal;
33
52
  }
@@ -102,6 +121,16 @@ export interface WorkflowsClient {
102
121
  * The price is `snapshot.cost.total`. A refused estimate resolves with
103
122
  * `status: 'failed'` and no `cost`; a body the host cannot price fails this
104
123
  * way without that predicting a refused {@link WorkflowsClient.submit}.
124
+ *
125
+ * 🔴 DO NOT REQUIRE `'succeeded'`. Nothing runs on an estimate, so a priced
126
+ * one comes back with a status that has not finished (`'pending'` in
127
+ * practice). Read the price with {@link estimatedCost}, which is `null`
128
+ * exactly when there is none to show:
129
+ *
130
+ * ```ts
131
+ * const price = estimatedCost(await app.workflows.estimate(body));
132
+ * if (price === null) showMessage('This generation could not be priced.');
133
+ * ```
105
134
  */
106
135
  estimate(body: WorkflowBody, opts?: WorkflowCallOptions): Promise<BlockWorkflowSnapshot>;
107
136
  /**
@@ -51,6 +51,28 @@ export class SubmissionUnconfirmedError extends CivitaiError {
51
51
  this.snapshot = snapshot;
52
52
  }
53
53
  }
54
+ /**
55
+ * The price an {@link WorkflowsClient.estimate} reply quotes, in whole Buzz, or
56
+ * `null` when it quotes none.
57
+ *
58
+ * An estimate is priced when its `status` is not `'failed'` AND it carries a
59
+ * numeric `cost.total`. That is the whole rule, and it is the one to branch on:
60
+ * - Do not require `'succeeded'`. Nothing runs on an estimate, so a priced one
61
+ * comes back with a status that has not finished (`'pending'` in practice),
62
+ * and a check for `'succeeded'` reads every price as missing.
63
+ * - `0` is a price.
64
+ * - A `'failed'` snapshot is never priced, even one carrying a `cost.total`: a
65
+ * budget refusal carries the total it would not charge.
66
+ * - A snapshot with no numeric `cost.total` is not priced, whatever its status.
67
+ *
68
+ * `null` does not predict a refused {@link WorkflowsClient.submit}: a body the
69
+ * host cannot price can still be submitted. Read `error` / `errorCode` on the
70
+ * snapshot for why.
71
+ */
72
+ export function estimatedCost(snapshot) {
73
+ const total = snapshot.cost?.total;
74
+ return snapshot.status !== 'failed' && typeof total === 'number' ? total : null;
75
+ }
54
76
  const isSnapshot = (value) => value != null && typeof value === 'object' && typeof value.status === 'string';
55
77
  /**
56
78
  * Asserted, not cast: every caller branches on `status`, and a reply without
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@civitai/sdk",
3
- "version": "0.19.1",
3
+ "version": "0.20.1",
4
4
  "description": "The Civitai SDK: an app holds a token and calls the Civitai API and the orchestrator; a block asks its host only to show host UI.",
5
5
  "license": "MIT",
6
6
  "type": "module",