@civitai/blocks-react 0.63.2 → 0.65.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 +188 -6
- package/dist/hooks/useBuzzWorkflow.d.ts +20 -0
- package/dist/hooks/useBuzzWorkflow.js +32 -12
- package/dist/hooks/useCreatePostFromApp.d.ts +7 -2
- package/dist/hooks/useCreatePostFromApp.js +8 -3
- package/dist/hooks/useNestedDocument.d.ts +73 -0
- package/dist/hooks/useNestedDocument.js +160 -0
- package/dist/hooks/usePrepareTrainingDataset.d.ts +99 -0
- package/dist/hooks/usePrepareTrainingDataset.js +157 -0
- package/dist/hooks/usePublishGenerationOutputs.d.ts +6 -0
- package/dist/hooks/usePublishGenerationOutputs.js +6 -0
- package/dist/hooks/useRunTraining.d.ts +151 -0
- package/dist/hooks/useRunTraining.js +212 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/internal/liveHost.d.ts +18 -2
- package/dist/internal/liveHost.js +89 -23
- package/dist/internal/mockHost.d.ts +146 -2
- package/dist/internal/mockHost.js +309 -0
- package/dist/transport/requestTimeouts.d.ts +19 -0
- package/dist/transport/requestTimeouts.js +30 -0
- package/dist/transport/validate.d.ts +27 -0
- package/dist/transport/validate.js +189 -0
- package/package.json +5 -5
|
@@ -41,6 +41,7 @@ import { APP_STORAGE_ERROR_REQUEST_FAILED, APP_STORAGE_ERROR_USER_QUOTA_EXCEEDED
|
|
|
41
41
|
import { consentUnavailablePayload, isKnownBlockScope, resolveUngrantableConsentNotice, } from './consent.js';
|
|
42
42
|
import { requiredStorageScope, storageResultType, storageScopeDeniedMessage, storageScopeDeniedPayload, } from './mockHostScopes.js';
|
|
43
43
|
import { governsIdempotencyKey, idempotencyKeyDeniedMessage, idempotencyKeyRefusal, } from './mockHostIdempotency.js';
|
|
44
|
+
import { RUN_TRAINING_ERROR_CODES } from '../hooks/useRunTraining.js';
|
|
44
45
|
import { hostContextWithTheme } from '../transport/transport.js';
|
|
45
46
|
import { isRoutableRequestId } from '../transport/requestId.js';
|
|
46
47
|
/**
|
|
@@ -79,6 +80,10 @@ const preferredAccountType = (body) => {
|
|
|
79
80
|
return body.mode === 'inline' ? undefined : body.params.accountType;
|
|
80
81
|
case 'step':
|
|
81
82
|
return undefined;
|
|
83
|
+
// A training body names no pool either: the host's `blockTrainingBodySchema`
|
|
84
|
+
// is `.strict()` with no `accountType`.
|
|
85
|
+
case 'training':
|
|
86
|
+
return undefined;
|
|
82
87
|
default: {
|
|
83
88
|
// Exhaustiveness check: a new `WorkflowBody` member makes this assignment
|
|
84
89
|
// fail to compile, right here, rather than defaulting somewhere.
|
|
@@ -112,6 +117,86 @@ const BUDGETED_SCOPE = 'ai:write:budgeted';
|
|
|
112
117
|
const DEFAULT_STORAGE_QUOTA_BYTES = APP_STORAGE_MAX_BYTES;
|
|
113
118
|
const DEFAULT_STORAGE_VALUE_CAP_BYTES = APP_STORAGE_MAX_VALUE_BYTES;
|
|
114
119
|
const DEFAULT_STORAGE_LIMIT_ROWS = APP_STORAGE_MAX_ROWS;
|
|
120
|
+
/**
|
|
121
|
+
* The pass-through training `$type` a body names, if any. Exact match — the
|
|
122
|
+
* orchestrator's `$type` discriminator is case-sensitive. Only the pass-through
|
|
123
|
+
* arm carries a `$type`, so no separate `kind` narrow is needed.
|
|
124
|
+
*/
|
|
125
|
+
const passThroughTrainingType = (body) => {
|
|
126
|
+
const t = body.$type;
|
|
127
|
+
return t === 'training' || t === 'imageResourceTraining' ? t : undefined;
|
|
128
|
+
};
|
|
129
|
+
/**
|
|
130
|
+
* The orchestrator `$type` a body's run trains with, if it trains at all: the
|
|
131
|
+
* pass-through training `$type`s above, and `'training'` for a
|
|
132
|
+
* `kind: 'training'` body — the host builds an ai-toolkit `training` step for it.
|
|
133
|
+
*/
|
|
134
|
+
const trainingStepType = (body) => body.kind === 'training' ? 'training' : passThroughTrainingType(body);
|
|
135
|
+
/**
|
|
136
|
+
* The training bounds the mock enforces — COPIES of `@civitai/app-sdk/blocks`'s
|
|
137
|
+
* `BLOCK_TRAINING_DATASET_MAX_ITEMS`, `BLOCK_TRAINING_CAPTION_MAX_CHARS` and
|
|
138
|
+
* `BLOCK_TRAINING_MAX_BUZZ_PER_RUN`, deliberately not value-imported: a value
|
|
139
|
+
* import of a symbol the newest PUBLISHED app-sdk lacks raises this package's
|
|
140
|
+
* peer floor (`tests/guards/blocks-react-peer-floor.test.mjs`). The copy is held
|
|
141
|
+
* to the SDK's value by `test/trainingKindFlow.test.tsx`, which imports both.
|
|
142
|
+
*/
|
|
143
|
+
export const MOCK_TRAINING_BOUNDS = {
|
|
144
|
+
datasetMaxItems: 50,
|
|
145
|
+
captionMaxChars: 1000,
|
|
146
|
+
maxBuzzPerRun: 5000,
|
|
147
|
+
};
|
|
148
|
+
/**
|
|
149
|
+
* The host's `PREPARE_TRAINING_DATASET` item check, mirrored: the server's
|
|
150
|
+
* `blockTrainingDatasetItemsSchema` — 1…50 items, each EXACTLY `{ imageId:
|
|
151
|
+
* positive integer, caption: string ≤ 1000 }`. The real host runs this BEFORE any
|
|
152
|
+
* server call and refuses with `invalid training dataset`, so the mock does too.
|
|
153
|
+
*/
|
|
154
|
+
function isValidTrainingDatasetItems(items) {
|
|
155
|
+
if (!Array.isArray(items))
|
|
156
|
+
return false;
|
|
157
|
+
if (items.length < 1 || items.length > MOCK_TRAINING_BOUNDS.datasetMaxItems)
|
|
158
|
+
return false;
|
|
159
|
+
return items.every((it) => {
|
|
160
|
+
if (typeof it !== 'object' || it === null || Array.isArray(it))
|
|
161
|
+
return false;
|
|
162
|
+
const keys = Object.keys(it);
|
|
163
|
+
if (keys.length !== 2 || !keys.includes('imageId') || !keys.includes('caption'))
|
|
164
|
+
return false;
|
|
165
|
+
const { imageId, caption } = it;
|
|
166
|
+
return (typeof imageId === 'number' &&
|
|
167
|
+
Number.isInteger(imageId) &&
|
|
168
|
+
imageId > 0 &&
|
|
169
|
+
typeof caption === 'string' &&
|
|
170
|
+
caption.length <= MOCK_TRAINING_BOUNDS.captionMaxChars);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
/** The real server's refusal for a training request from a token without the spend scope. */
|
|
174
|
+
export const MOCK_TRAINING_SCOPE_ERROR = 'block lacks ai:write:budgeted scope';
|
|
175
|
+
/** The real server's refusal for an unknown, expired or spent quote on `RUN_TRAINING`. */
|
|
176
|
+
export const MOCK_TRAINING_QUOTE_GONE_ERROR = 'training quote not found, expired or already used — estimate again';
|
|
177
|
+
/** The real server's refusal for a training estimate naming a dataset it does not hold. */
|
|
178
|
+
export const MOCK_TRAINING_DATASET_GONE_ERROR = 'training dataset not found or expired';
|
|
179
|
+
/**
|
|
180
|
+
* The real server's two refusals for a dataset with NOTHING admitted
|
|
181
|
+
* (`prepareBlockTrainingDataset`): every image failed eligibility, or every
|
|
182
|
+
* eligible image failed its import. The server THROWS rather than returning
|
|
183
|
+
* `count: 0`, so a successful reply always admits at least one image.
|
|
184
|
+
*/
|
|
185
|
+
export const MOCK_TRAINING_NONE_ELIGIBLE_ERROR = 'none of the requested images can be used for training';
|
|
186
|
+
export const MOCK_TRAINING_NONE_IMPORTED_ERROR = 'none of the requested images could be prepared for training';
|
|
187
|
+
/**
|
|
188
|
+
* The forced `RUN_TRAINING` errors that settle BEFORE the server claims (and so
|
|
189
|
+
* consumes) the quote: the host's own gate and dialog codes. Derived from the
|
|
190
|
+
* host-code list — every code except `submission-unconfirmed`, which the host
|
|
191
|
+
* emits only after a submit was sent. Any other forced error (a server message
|
|
192
|
+
* such as "Not enough Buzz for this training run.") stands for a refusal the
|
|
193
|
+
* server made AFTER its claim, so it consumes the quote.
|
|
194
|
+
*/
|
|
195
|
+
const PRE_CLAIM_RUN_TRAINING_ERRORS = new Set(RUN_TRAINING_ERROR_CODES.filter((c) => c !== 'submission-unconfirmed'));
|
|
196
|
+
/** Default quote total for a `kind: 'training'` estimate when {@link MockHostOptions.trainingQuoteTotal} is unset. */
|
|
197
|
+
const DEFAULT_TRAINING_QUOTE_TOTAL = 500;
|
|
198
|
+
/** The real quote lifetime (`BLOCK_TRAINING_QUOTE_TTL_SECONDS`, 15 min). */
|
|
199
|
+
const TRAINING_QUOTE_TTL_MS = 15 * 60_000;
|
|
115
200
|
// The canned picks carry the WIDENED BlockResourceInfo projection (PR-C) — the
|
|
116
201
|
// public recommended settings a real host now returns — so dev:mock mirrors prod
|
|
117
202
|
// (a picked resource seeds a weight slider + trigger words). Defaults match the
|
|
@@ -722,6 +807,13 @@ export function createMockHost(options = {}) {
|
|
|
722
807
|
// App Blocks → Post bridge: the canned post + a forced-refusal knob.
|
|
723
808
|
let createPostResult = options.createPostResult ?? DEFAULT_CREATE_POST_RESULT;
|
|
724
809
|
let createPostError = options.createPostError;
|
|
810
|
+
// App Blocks training bridges: canned dataset rejections, quote total, and
|
|
811
|
+
// forced-refusal knobs. The datasets/quotes themselves live per install.
|
|
812
|
+
let trainingDatasetRejected = options.trainingDatasetRejected ?? [];
|
|
813
|
+
let trainingDatasetError = options.trainingDatasetError;
|
|
814
|
+
let trainingQuoteTotal = options.trainingQuoteTotal ?? DEFAULT_TRAINING_QUOTE_TOTAL;
|
|
815
|
+
let runTrainingError = options.runTrainingError;
|
|
816
|
+
let runTrainingCapRefusal = options.runTrainingCapRefusal;
|
|
725
817
|
// App-subqueue bridge data + forced free-text-error knob. `appWorkflows` is
|
|
726
818
|
// MUTABLE — CANCEL_APP_WORKFLOW marks the matching row canceled in place so a
|
|
727
819
|
// follow-up QUERY reflects it.
|
|
@@ -873,6 +965,16 @@ export function createMockHost(options = {}) {
|
|
|
873
965
|
let submitCount = 0;
|
|
874
966
|
// body + cost remembered per workflow so the succeeded snapshot can echo them.
|
|
875
967
|
const workflows = new Map();
|
|
968
|
+
// Training: datasets this install prepared (id → admitted count) and the
|
|
969
|
+
// quotes its estimates stored (id → what a run is checked against).
|
|
970
|
+
const trainingDatasets = new Map();
|
|
971
|
+
const trainingQuotes = new Map();
|
|
972
|
+
let trainingSerial = 0;
|
|
973
|
+
/** `<prefix>` + 32 hex characters — the real handles' shape. */
|
|
974
|
+
const trainingHandle = (prefix) => {
|
|
975
|
+
trainingSerial += 1;
|
|
976
|
+
return `${prefix}${trainingSerial.toString(16).padStart(32, '0')}`;
|
|
977
|
+
};
|
|
876
978
|
const timers = new Set();
|
|
877
979
|
const dispatchToBlock = (data) => {
|
|
878
980
|
win.dispatchEvent(new MessageEvent('message', { data, origin: parentOrigin }));
|
|
@@ -894,11 +996,36 @@ export function createMockHost(options = {}) {
|
|
|
894
996
|
...(consentGranted ? { buzzBudget } : {}),
|
|
895
997
|
};
|
|
896
998
|
};
|
|
999
|
+
/**
|
|
1000
|
+
* The training-only fields of a succeeded snapshot — see
|
|
1001
|
+
* {@link MockGenerationScenario.trainedEpochs}. KIND-FAITHFUL, unlike the
|
|
1002
|
+
* rest of the money path: only a pass-through `training` /
|
|
1003
|
+
* `imageResourceTraining` body gets them, because only such a step produces
|
|
1004
|
+
* a checkpoint. Empty for every other body, so their snapshots are unchanged.
|
|
1005
|
+
*/
|
|
1006
|
+
const trainingFields = (body) => {
|
|
1007
|
+
const $type = trainingStepType(body);
|
|
1008
|
+
if (!$type)
|
|
1009
|
+
return {};
|
|
1010
|
+
const count = Math.max(0, Math.floor(gen.trainedEpochs ?? 0));
|
|
1011
|
+
return {
|
|
1012
|
+
...(count > 0
|
|
1013
|
+
? {
|
|
1014
|
+
trainedEpochs: Array.from({ length: count }, (_, i) => ({
|
|
1015
|
+
$type,
|
|
1016
|
+
epochNumber: i + 1,
|
|
1017
|
+
})),
|
|
1018
|
+
}
|
|
1019
|
+
: {}),
|
|
1020
|
+
...(gen.trainingPublishedModel ? { publishedModel: { ...gen.trainingPublishedModel } } : {}),
|
|
1021
|
+
};
|
|
1022
|
+
};
|
|
897
1023
|
const succeededSnapshot = (workflowId) => {
|
|
898
1024
|
const wf = workflows.get(workflowId);
|
|
899
1025
|
const cost = wf?.cost ?? legacyCost;
|
|
900
1026
|
const body = wf?.body ?? {};
|
|
901
1027
|
return {
|
|
1028
|
+
...trainingFields(body),
|
|
902
1029
|
workflowId,
|
|
903
1030
|
status: 'succeeded',
|
|
904
1031
|
cost: { total: cost },
|
|
@@ -1125,6 +1252,53 @@ export function createMockHost(options = {}) {
|
|
|
1125
1252
|
});
|
|
1126
1253
|
return;
|
|
1127
1254
|
}
|
|
1255
|
+
// TRAINING ESTIMATE — KIND-FAITHFUL, the one exception to the
|
|
1256
|
+
// kind-agnostic rule above, because the real arm is not a priced
|
|
1257
|
+
// pass-through: it needs the spend scope, names a dataset the server
|
|
1258
|
+
// must hold, refuses a price above the per-run ceiling, and STORES a
|
|
1259
|
+
// quote the run is later checked against. Each refusal is the
|
|
1260
|
+
// server's message in the host's `failureSnapshot` shape.
|
|
1261
|
+
if (body.kind === 'training') {
|
|
1262
|
+
const refuse = (error) => dispatchToBlock({
|
|
1263
|
+
type: 'ESTIMATE_RESULT',
|
|
1264
|
+
payload: { requestId, snapshot: { workflowId: 'failed', status: 'failed', error } },
|
|
1265
|
+
});
|
|
1266
|
+
if (!consentGranted)
|
|
1267
|
+
return refuse(MOCK_TRAINING_SCOPE_ERROR);
|
|
1268
|
+
const imageCount = trainingDatasets.get(body.datasetId);
|
|
1269
|
+
if (imageCount === undefined)
|
|
1270
|
+
return refuse(MOCK_TRAINING_DATASET_GONE_ERROR);
|
|
1271
|
+
const total = trainingQuoteTotal;
|
|
1272
|
+
if (total > MOCK_TRAINING_BOUNDS.maxBuzzPerRun) {
|
|
1273
|
+
return refuse(`this training run costs ${total} Buzz, above the per-run limit of ${MOCK_TRAINING_BOUNDS.maxBuzzPerRun} for apps`);
|
|
1274
|
+
}
|
|
1275
|
+
const quoteId = trainingHandle('tq_');
|
|
1276
|
+
const expiresAtMs = Date.now() + TRAINING_QUOTE_TTL_MS;
|
|
1277
|
+
trainingQuotes.set(quoteId, {
|
|
1278
|
+
datasetId: body.datasetId,
|
|
1279
|
+
total,
|
|
1280
|
+
expiresAtMs,
|
|
1281
|
+
spent: false,
|
|
1282
|
+
});
|
|
1283
|
+
dispatchToBlock({
|
|
1284
|
+
type: 'ESTIMATE_RESULT',
|
|
1285
|
+
payload: {
|
|
1286
|
+
requestId,
|
|
1287
|
+
snapshot: {
|
|
1288
|
+
workflowId: 'wf_estimate',
|
|
1289
|
+
status: 'pending',
|
|
1290
|
+
cost: { total },
|
|
1291
|
+
trainingQuote: {
|
|
1292
|
+
quoteId,
|
|
1293
|
+
total,
|
|
1294
|
+
imageCount,
|
|
1295
|
+
expiresAt: new Date(expiresAtMs).toISOString(),
|
|
1296
|
+
},
|
|
1297
|
+
},
|
|
1298
|
+
},
|
|
1299
|
+
});
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1128
1302
|
dispatchToBlock({
|
|
1129
1303
|
type: 'ESTIMATE_RESULT',
|
|
1130
1304
|
payload: {
|
|
@@ -1148,6 +1322,26 @@ export function createMockHost(options = {}) {
|
|
|
1148
1322
|
// the largest-wallet stamp. No recipe-registry validation.
|
|
1149
1323
|
submitCount += 1;
|
|
1150
1324
|
const body = typed.payload?.body ?? {};
|
|
1325
|
+
// A `kind: 'training'` body is never started by SUBMIT_WORKFLOW: the
|
|
1326
|
+
// server charges a training run only against a quote a signed-in
|
|
1327
|
+
// session confirmed, which only RUN_TRAINING's dialog records. The
|
|
1328
|
+
// server's own refusal, in the host's `failureSnapshot` shape.
|
|
1329
|
+
// (`useBuzzWorkflow().submit()` refuses before sending; this covers a
|
|
1330
|
+
// raw transport send.)
|
|
1331
|
+
if (body.kind === 'training') {
|
|
1332
|
+
dispatchToBlock({
|
|
1333
|
+
type: 'WORKFLOW_SUBMITTED',
|
|
1334
|
+
payload: {
|
|
1335
|
+
requestId,
|
|
1336
|
+
snapshot: {
|
|
1337
|
+
workflowId: 'failed',
|
|
1338
|
+
status: 'failed',
|
|
1339
|
+
error: 'this training run has not been confirmed by the viewer in this session',
|
|
1340
|
+
},
|
|
1341
|
+
},
|
|
1342
|
+
});
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1151
1345
|
const cost = costFor(body);
|
|
1152
1346
|
// CAUGHT-SERVER-EXCEPTION simulation (`generation.failSubmitException`).
|
|
1153
1347
|
// Reproduces the host's `failureSnapshot(err)` byte-for-byte: the
|
|
@@ -1599,6 +1793,108 @@ export function createMockHost(options = {}) {
|
|
|
1599
1793
|
});
|
|
1600
1794
|
return;
|
|
1601
1795
|
}
|
|
1796
|
+
case 'PREPARE_TRAINING_DATASET': {
|
|
1797
|
+
// The host's gate order (`prepareTrainingDatasetGate.ts`): no
|
|
1798
|
+
// requestId → drop; signed-out → `sign in to train`; items outside
|
|
1799
|
+
// the server's schema → `invalid training dataset`, before any call.
|
|
1800
|
+
// Then the server: the spend scope, then the dataset.
|
|
1801
|
+
if (!isRoutableRequestId(requestId))
|
|
1802
|
+
return;
|
|
1803
|
+
const reply = (payload) => dispatchToBlock({ type: 'TRAINING_DATASET_RESULT', payload: { requestId, ...payload } });
|
|
1804
|
+
if (viewer === null)
|
|
1805
|
+
return reply({ error: 'sign in to train' });
|
|
1806
|
+
const items = typed.payload?.items;
|
|
1807
|
+
if (!isValidTrainingDatasetItems(items)) {
|
|
1808
|
+
return reply({ error: 'invalid training dataset' });
|
|
1809
|
+
}
|
|
1810
|
+
if (!consentGranted)
|
|
1811
|
+
return reply({ error: MOCK_TRAINING_SCOPE_ERROR });
|
|
1812
|
+
if (trainingDatasetError !== undefined)
|
|
1813
|
+
return reply({ error: trainingDatasetError });
|
|
1814
|
+
const named = new Set(items.map((it) => it.imageId));
|
|
1815
|
+
const rejected = trainingDatasetRejected
|
|
1816
|
+
.filter((r) => named.has(r.imageId))
|
|
1817
|
+
.map(({ imageId, reason }) => ({ imageId, reason }));
|
|
1818
|
+
const rejectedIds = new Set(rejected.map((r) => r.imageId));
|
|
1819
|
+
const count = [...named].filter((id) => !rejectedIds.has(id)).length;
|
|
1820
|
+
if (count === 0) {
|
|
1821
|
+
// The server THROWS here, eligibility before import: an `import-*`
|
|
1822
|
+
// rejection means that image PASSED eligibility, so the server
|
|
1823
|
+
// reached the import and refuses there.
|
|
1824
|
+
const anyImported = rejected.some((r) => r.reason.startsWith('import-'));
|
|
1825
|
+
return reply({
|
|
1826
|
+
error: anyImported
|
|
1827
|
+
? MOCK_TRAINING_NONE_IMPORTED_ERROR
|
|
1828
|
+
: MOCK_TRAINING_NONE_ELIGIBLE_ERROR,
|
|
1829
|
+
});
|
|
1830
|
+
}
|
|
1831
|
+
const datasetId = trainingHandle('tds_');
|
|
1832
|
+
trainingDatasets.set(datasetId, count);
|
|
1833
|
+
const result = { datasetId, count, rejected };
|
|
1834
|
+
return reply({ result });
|
|
1835
|
+
}
|
|
1836
|
+
case 'RUN_TRAINING': {
|
|
1837
|
+
// The host's gate (`runTrainingGate.ts`): no requestId → drop;
|
|
1838
|
+
// signed-out → `sign in to train`; not a `kind:'training'` body with
|
|
1839
|
+
// a `quoteId` → `invalid training request`. Then the server: the
|
|
1840
|
+
// spend scope, then the quote (unknown, expired or already spent).
|
|
1841
|
+
if (!isRoutableRequestId(requestId))
|
|
1842
|
+
return;
|
|
1843
|
+
const reply = (payload) => dispatchToBlock({ type: 'TRAINING_RESULT', payload: { requestId, ...payload } });
|
|
1844
|
+
if (viewer === null)
|
|
1845
|
+
return reply({ error: 'sign in to train' });
|
|
1846
|
+
const raw = typed.payload?.body;
|
|
1847
|
+
const b = raw && typeof raw === 'object' && !Array.isArray(raw)
|
|
1848
|
+
? raw
|
|
1849
|
+
: null;
|
|
1850
|
+
if (!b || b.kind !== 'training' || typeof b.quoteId !== 'string' || !b.quoteId) {
|
|
1851
|
+
return reply({ error: 'invalid training request' });
|
|
1852
|
+
}
|
|
1853
|
+
if (!consentGranted)
|
|
1854
|
+
return reply({ error: MOCK_TRAINING_SCOPE_ERROR });
|
|
1855
|
+
const quote = trainingQuotes.get(b.quoteId);
|
|
1856
|
+
if (!quote || quote.spent || Date.now() >= quote.expiresAtMs) {
|
|
1857
|
+
return reply({ error: MOCK_TRAINING_QUOTE_GONE_ERROR });
|
|
1858
|
+
}
|
|
1859
|
+
// Which outcomes consume the quote, modelled on the real flow: the
|
|
1860
|
+
// host's gate and consent dialog come first (quote kept); the
|
|
1861
|
+
// server's submit then claims the quote with a GETDEL
|
|
1862
|
+
// (`claimTrainingQuote`) BEFORE any of its own checks, so every
|
|
1863
|
+
// server outcome — a refusal, a body mismatch, a cap refusal,
|
|
1864
|
+
// `submission-unconfirmed`, a run — consumes it. The mock's check
|
|
1865
|
+
// ORDER below is its own; only the spent/kept split is the server's.
|
|
1866
|
+
if (runTrainingError !== undefined) {
|
|
1867
|
+
if (!PRE_CLAIM_RUN_TRAINING_ERRORS.has(runTrainingError))
|
|
1868
|
+
quote.spent = true;
|
|
1869
|
+
return reply({ error: runTrainingError });
|
|
1870
|
+
}
|
|
1871
|
+
quote.spent = true;
|
|
1872
|
+
if (b.datasetId !== quote.datasetId) {
|
|
1873
|
+
return reply({
|
|
1874
|
+
error: 'the training body differs from the one that was quoted — estimate again',
|
|
1875
|
+
});
|
|
1876
|
+
}
|
|
1877
|
+
// The server's cap / availability refusals RESOLVE (refunded, no run
|
|
1878
|
+
// started) — after the quote was consumed above.
|
|
1879
|
+
if (runTrainingCapRefusal !== undefined) {
|
|
1880
|
+
return reply({
|
|
1881
|
+
snapshot: {
|
|
1882
|
+
workflowId: 'failed',
|
|
1883
|
+
status: 'failed',
|
|
1884
|
+
cost: { total: quote.total },
|
|
1885
|
+
error: runTrainingCapRefusal,
|
|
1886
|
+
},
|
|
1887
|
+
});
|
|
1888
|
+
}
|
|
1889
|
+
submitCount += 1;
|
|
1890
|
+
const workflowId = `wf_${submitCount}_${Date.now()}`;
|
|
1891
|
+
workflows.set(workflowId, {
|
|
1892
|
+
polls: 0,
|
|
1893
|
+
cost: quote.total,
|
|
1894
|
+
body: raw,
|
|
1895
|
+
});
|
|
1896
|
+
return reply({ snapshot: { workflowId, status: 'pending' } });
|
|
1897
|
+
}
|
|
1602
1898
|
case 'PUBLISH_GENERATION_OUTPUTS': {
|
|
1603
1899
|
// Publish selected outputs of one of the app's OWN workflows as bare,
|
|
1604
1900
|
// real-scanned public Image rows. Drop a request with no requestId
|
|
@@ -2313,6 +2609,19 @@ export function createMockHost(options = {}) {
|
|
|
2313
2609
|
createPostResult = patch.createPostResult;
|
|
2314
2610
|
if (patch.createPostError !== undefined)
|
|
2315
2611
|
createPostError = patch.createPostError;
|
|
2612
|
+
if (patch.trainingDatasetRejected !== undefined) {
|
|
2613
|
+
trainingDatasetRejected = patch.trainingDatasetRejected;
|
|
2614
|
+
}
|
|
2615
|
+
// The training error knobs clear on an explicit `undefined` (key PRESENT in
|
|
2616
|
+
// the patch), so a harness can turn a refusal off without re-installing.
|
|
2617
|
+
if ('trainingDatasetError' in patch)
|
|
2618
|
+
trainingDatasetError = patch.trainingDatasetError;
|
|
2619
|
+
if (patch.trainingQuoteTotal !== undefined)
|
|
2620
|
+
trainingQuoteTotal = patch.trainingQuoteTotal;
|
|
2621
|
+
if ('runTrainingError' in patch)
|
|
2622
|
+
runTrainingError = patch.runTrainingError;
|
|
2623
|
+
if ('runTrainingCapRefusal' in patch)
|
|
2624
|
+
runTrainingCapRefusal = patch.runTrainingCapRefusal;
|
|
2316
2625
|
if (patch.appWorkflows !== undefined) {
|
|
2317
2626
|
appWorkflows = {
|
|
2318
2627
|
workflows: patch.appWorkflows.workflows,
|
|
@@ -28,6 +28,25 @@ export declare const DEFAULT_REQUEST_TIMEOUT_MS = 30000;
|
|
|
28
28
|
* no deep hook paths, so the rename touches no reachable API.
|
|
29
29
|
*/
|
|
30
30
|
export declare const HUMAN_INTERACTION_TIMEOUT_MS: number;
|
|
31
|
+
/**
|
|
32
|
+
* The bound for a `'protocol'` request whose SERVER WORK is itself slow — the
|
|
33
|
+
* host forwards it to the orchestrator and replies only once that answers. No
|
|
34
|
+
* person is in the loop, so it stays in the `'protocol'` bucket below; it just
|
|
35
|
+
* needs more than {@link DEFAULT_REQUEST_TIMEOUT_MS}.
|
|
36
|
+
*
|
|
37
|
+
* Callers: the workflow trio in `hooks/useBuzzWorkflow.ts` (a submit is a whatif
|
|
38
|
+
* preflight plus the real submit plus a prompt audit), and
|
|
39
|
+
* `PREPARE_TRAINING_DATASET` in `hooks/usePrepareTrainingDataset.ts`, whose
|
|
40
|
+
* server moderates the captions and imports up to 50 images into the
|
|
41
|
+
* orchestrator, the import alone under a 60s budget
|
|
42
|
+
* (`BLOCK_TRAINING_IMPORT_BUDGET_MS` in civitai/civitai's
|
|
43
|
+
* `block-training-dataset.service.ts`) — so the 30s default would reject a
|
|
44
|
+
* healthy prepare mid-import.
|
|
45
|
+
*
|
|
46
|
+
* Was a module-local constant in `useBuzzWorkflow.ts`; moved here so the second
|
|
47
|
+
* caller shares the number instead of re-typing it.
|
|
48
|
+
*/
|
|
49
|
+
export declare const WORKFLOW_REQUEST_TIMEOUT_MS = 120000;
|
|
31
50
|
/** Which bound a block→parent message is answered under. */
|
|
32
51
|
export type RequestTimeoutClass =
|
|
33
52
|
/** Reply waits on a PERSON — must pass {@link HUMAN_INTERACTION_TIMEOUT_MS}. */
|
|
@@ -27,6 +27,25 @@ export const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
|
|
|
27
27
|
* no deep hook paths, so the rename touches no reachable API.
|
|
28
28
|
*/
|
|
29
29
|
export const HUMAN_INTERACTION_TIMEOUT_MS = 10 * 60_000;
|
|
30
|
+
/**
|
|
31
|
+
* The bound for a `'protocol'` request whose SERVER WORK is itself slow — the
|
|
32
|
+
* host forwards it to the orchestrator and replies only once that answers. No
|
|
33
|
+
* person is in the loop, so it stays in the `'protocol'` bucket below; it just
|
|
34
|
+
* needs more than {@link DEFAULT_REQUEST_TIMEOUT_MS}.
|
|
35
|
+
*
|
|
36
|
+
* Callers: the workflow trio in `hooks/useBuzzWorkflow.ts` (a submit is a whatif
|
|
37
|
+
* preflight plus the real submit plus a prompt audit), and
|
|
38
|
+
* `PREPARE_TRAINING_DATASET` in `hooks/usePrepareTrainingDataset.ts`, whose
|
|
39
|
+
* server moderates the captions and imports up to 50 images into the
|
|
40
|
+
* orchestrator, the import alone under a 60s budget
|
|
41
|
+
* (`BLOCK_TRAINING_IMPORT_BUDGET_MS` in civitai/civitai's
|
|
42
|
+
* `block-training-dataset.service.ts`) — so the 30s default would reject a
|
|
43
|
+
* healthy prepare mid-import.
|
|
44
|
+
*
|
|
45
|
+
* Was a module-local constant in `useBuzzWorkflow.ts`; moved here so the second
|
|
46
|
+
* caller shares the number instead of re-typing it.
|
|
47
|
+
*/
|
|
48
|
+
export const WORKFLOW_REQUEST_TIMEOUT_MS = 120_000;
|
|
30
49
|
/**
|
|
31
50
|
* 🔴 THE TOTALITY GATE — every block→parent message type, bucketed.
|
|
32
51
|
*
|
|
@@ -69,6 +88,12 @@ const REQUEST_TIMEOUT_CLASS = {
|
|
|
69
88
|
// failure, with nothing to reconcile the two. Unlike the publish bridge there
|
|
70
89
|
// is no "outputs were lost" framing available here: the post exists.
|
|
71
90
|
CREATE_POST_FROM_APP: 'human',
|
|
91
|
+
// 🔴 `'human'`: the host opens a training CONSENT dialog (price, base model,
|
|
92
|
+
// length, dataset size) and replies only on the viewer's click or dismiss. At
|
|
93
|
+
// the 30s default the request would reject with the dialog still open, and a
|
|
94
|
+
// viewer who then clicked Train would start a CHARGED run the block had
|
|
95
|
+
// already reported as failed.
|
|
96
|
+
RUN_TRAINING: 'human',
|
|
72
97
|
// ── Fire-and-forget: no requestId, so no pending promise to time out ────
|
|
73
98
|
BLOCK_ERROR: 'no-reply',
|
|
74
99
|
BLOCK_HELLO: 'no-reply',
|
|
@@ -97,6 +122,11 @@ const REQUEST_TIMEOUT_CLASS = {
|
|
|
97
122
|
APP_STORAGE_SET: 'protocol',
|
|
98
123
|
CANCEL_APP_WORKFLOW: 'protocol',
|
|
99
124
|
CANCEL_WORKFLOW: 'protocol',
|
|
125
|
+
// No dialog — preparing a dataset charges nothing and the host replies when
|
|
126
|
+
// the server does. Server-bound, so `'protocol'`; but sent under
|
|
127
|
+
// `WORKFLOW_REQUEST_TIMEOUT_MS`, not the 30s default, because the server's
|
|
128
|
+
// image import alone may take 60s. Same reasoning as the workflow trio below.
|
|
129
|
+
PREPARE_TRAINING_DATASET: 'protocol',
|
|
100
130
|
// The workflow trio is server-bound, not person-bound. It carries its OWN
|
|
101
131
|
// longer bound (`WORKFLOW_REQUEST_TIMEOUT_MS` in `hooks/useBuzzWorkflow.ts`)
|
|
102
132
|
// for orchestrator latency — a different reason for a different number.
|
|
@@ -399,6 +399,33 @@ export declare function isValidCollectionFollowResult(p: unknown): boolean;
|
|
|
399
399
|
* so a drop at least times out visibly.
|
|
400
400
|
*/
|
|
401
401
|
export declare function isValidCreatePostResult(p: unknown): boolean;
|
|
402
|
+
/**
|
|
403
|
+
* Reply to `PREPARE_TRAINING_DATASET`. A well-formed reply carries EITHER a
|
|
404
|
+
* `result` (`{ datasetId: non-empty string, count: integer ≥ 0, rejected: [{
|
|
405
|
+
* imageId: number, reason: string }] }`) OR an `error`; one with neither is
|
|
406
|
+
* malformed and dropped.
|
|
407
|
+
*
|
|
408
|
+
* 🔴 `error` IS SHAPE-CHECKED ONLY — the same decision as
|
|
409
|
+
* {@link isValidCreatePostResult}: the host's closed codes share the field with
|
|
410
|
+
* any server message it forwards, and a membership check would drop those
|
|
411
|
+
* replies before correlation, leaving the request to its timer.
|
|
412
|
+
*
|
|
413
|
+
* `rejected[].reason` is checked as a STRING, not against today's six members —
|
|
414
|
+
* the host adding a reason must not make every prepare reply that carries it a
|
|
415
|
+
* dropped one (the forward-compatibility rule in {@link isValidWorkflowSnapshot}).
|
|
416
|
+
*/
|
|
417
|
+
export declare function isValidTrainingDatasetResult(p: unknown): boolean;
|
|
418
|
+
/**
|
|
419
|
+
* Reply to `RUN_TRAINING`. A well-formed reply carries EITHER a `snapshot` (a
|
|
420
|
+
* valid {@link isValidWorkflowSnapshot} — the block polls its `workflowId`) OR an
|
|
421
|
+
* `error`; one with neither is malformed and dropped.
|
|
422
|
+
*
|
|
423
|
+
* 🔴 `error` IS SHAPE-CHECKED ONLY, and here a membership check would cost the
|
|
424
|
+
* most: this request carries the TEN-MINUTE consent bound, and the host forwards
|
|
425
|
+
* server refusals (an expired quote, a changed body, an ineligible image) in this
|
|
426
|
+
* field. A dropped refusal would leave a Train button spinning for ten minutes.
|
|
427
|
+
*/
|
|
428
|
+
export declare function isValidTrainingResult(p: unknown): boolean;
|
|
402
429
|
/**
|
|
403
430
|
* Reply to `APP_STORAGE_GET`. `value` is arbitrary JSON (incl. `null` for an
|
|
404
431
|
* unset key / anon viewer), so there is nothing to type-check on it — the guard
|