@shop-os/modelkit 0.3.8 → 0.4.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/CHANGELOG.md +15 -0
- package/README.md +2 -2
- package/dist/catalog.cjs.map +1 -1
- package/dist/catalog.d.cts +12 -1
- package/dist/catalog.d.ts +12 -1
- package/dist/catalog.js.map +1 -1
- package/dist/index.cjs +115 -34
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -2
- package/dist/index.d.ts +13 -2
- package/dist/index.js +115 -34
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -184,6 +184,8 @@ interface VideoToolSubmission {
|
|
|
184
184
|
responseUrl: string;
|
|
185
185
|
cancelUrl: string;
|
|
186
186
|
output: VideoToolOutputSpec;
|
|
187
|
+
/** Epoch ms when the kit submitted the job; the result's usage `latencyMs` is measured from it. */
|
|
188
|
+
submittedAt?: number;
|
|
187
189
|
}
|
|
188
190
|
type VideoToolStatus = "queued" | "running" | "completed";
|
|
189
191
|
interface VideoToolResult {
|
|
@@ -523,6 +525,13 @@ interface UsageEvent {
|
|
|
523
525
|
inputTokens?: number;
|
|
524
526
|
outputTokens?: number;
|
|
525
527
|
reportedCostUsd?: number;
|
|
528
|
+
/**
|
|
529
|
+
* Wall time of the kit call that produced this event, in ms: from dispatch to the
|
|
530
|
+
* provider's final answer (a video's polling included; a multi-step text call's tool
|
|
531
|
+
* steps included). Absent only where the kit never saw the start (a video tool whose
|
|
532
|
+
* submission carries no `submittedAt`).
|
|
533
|
+
*/
|
|
534
|
+
latencyMs?: number;
|
|
526
535
|
raw?: unknown;
|
|
527
536
|
}
|
|
528
537
|
type ProviderKeyName = "openai" | "anthropic" | "google" | "xai" | "perplexity" | "deepseek" | "moonshot" | "minimax" | "ark" | "fal" | "replicate" | "prodia" | "higgsfield" | "cloudflare";
|
|
@@ -605,7 +614,9 @@ interface CallContext {
|
|
|
605
614
|
|
|
606
615
|
/** Provider body or the lifted detail (Gemini blockReason, fal body.detail). */
|
|
607
616
|
type ProviderErrorBody = string | JsonValue;
|
|
608
|
-
type ModelKitErrorCode = "unknown_model" | "unsupported_option" | "missing_input" | "content_blocked" | "provider_rejected"
|
|
617
|
+
type ModelKitErrorCode = "unknown_model" | "unsupported_option" | "missing_input" | "content_blocked" | "provider_rejected"
|
|
618
|
+
/** The provider account is out of credit or quota (HTTP 402 and its body spellings). Never retried. */
|
|
619
|
+
| "provider_billing" | "timeout" | "auth" | "network" | "rate_limited";
|
|
609
620
|
interface ModelKitErrorInit {
|
|
610
621
|
code: ModelKitErrorCode;
|
|
611
622
|
message: string;
|
|
@@ -1230,7 +1241,7 @@ declare function keysFromEnv(env: NodeJS.ProcessEnv, prefix?: string): ProviderK
|
|
|
1230
1241
|
|
|
1231
1242
|
/**
|
|
1232
1243
|
* One classifier for every adapter. Never retry safety blocks,
|
|
1233
|
-
* content_policy_violation, or 4xx other than 429. Retry 429, 5xx, timeouts
|
|
1244
|
+
* content_policy_violation, billing failures, or 4xx other than 429. Retry 429, 5xx, timeouts
|
|
1234
1245
|
* and network failures. Adapters must not stack their own retry loop on top.
|
|
1235
1246
|
*/
|
|
1236
1247
|
declare function classifyHttpError(status: number | undefined, body: ProviderErrorBody, provider: string, model?: string): ModelKitError;
|
package/dist/index.d.ts
CHANGED
|
@@ -184,6 +184,8 @@ interface VideoToolSubmission {
|
|
|
184
184
|
responseUrl: string;
|
|
185
185
|
cancelUrl: string;
|
|
186
186
|
output: VideoToolOutputSpec;
|
|
187
|
+
/** Epoch ms when the kit submitted the job; the result's usage `latencyMs` is measured from it. */
|
|
188
|
+
submittedAt?: number;
|
|
187
189
|
}
|
|
188
190
|
type VideoToolStatus = "queued" | "running" | "completed";
|
|
189
191
|
interface VideoToolResult {
|
|
@@ -523,6 +525,13 @@ interface UsageEvent {
|
|
|
523
525
|
inputTokens?: number;
|
|
524
526
|
outputTokens?: number;
|
|
525
527
|
reportedCostUsd?: number;
|
|
528
|
+
/**
|
|
529
|
+
* Wall time of the kit call that produced this event, in ms: from dispatch to the
|
|
530
|
+
* provider's final answer (a video's polling included; a multi-step text call's tool
|
|
531
|
+
* steps included). Absent only where the kit never saw the start (a video tool whose
|
|
532
|
+
* submission carries no `submittedAt`).
|
|
533
|
+
*/
|
|
534
|
+
latencyMs?: number;
|
|
526
535
|
raw?: unknown;
|
|
527
536
|
}
|
|
528
537
|
type ProviderKeyName = "openai" | "anthropic" | "google" | "xai" | "perplexity" | "deepseek" | "moonshot" | "minimax" | "ark" | "fal" | "replicate" | "prodia" | "higgsfield" | "cloudflare";
|
|
@@ -605,7 +614,9 @@ interface CallContext {
|
|
|
605
614
|
|
|
606
615
|
/** Provider body or the lifted detail (Gemini blockReason, fal body.detail). */
|
|
607
616
|
type ProviderErrorBody = string | JsonValue;
|
|
608
|
-
type ModelKitErrorCode = "unknown_model" | "unsupported_option" | "missing_input" | "content_blocked" | "provider_rejected"
|
|
617
|
+
type ModelKitErrorCode = "unknown_model" | "unsupported_option" | "missing_input" | "content_blocked" | "provider_rejected"
|
|
618
|
+
/** The provider account is out of credit or quota (HTTP 402 and its body spellings). Never retried. */
|
|
619
|
+
| "provider_billing" | "timeout" | "auth" | "network" | "rate_limited";
|
|
609
620
|
interface ModelKitErrorInit {
|
|
610
621
|
code: ModelKitErrorCode;
|
|
611
622
|
message: string;
|
|
@@ -1230,7 +1241,7 @@ declare function keysFromEnv(env: NodeJS.ProcessEnv, prefix?: string): ProviderK
|
|
|
1230
1241
|
|
|
1231
1242
|
/**
|
|
1232
1243
|
* One classifier for every adapter. Never retry safety blocks,
|
|
1233
|
-
* content_policy_violation, or 4xx other than 429. Retry 429, 5xx, timeouts
|
|
1244
|
+
* content_policy_violation, billing failures, or 4xx other than 429. Retry 429, 5xx, timeouts
|
|
1234
1245
|
* and network failures. Adapters must not stack their own retry loop on top.
|
|
1235
1246
|
*/
|
|
1236
1247
|
declare function classifyHttpError(status: number | undefined, body: ProviderErrorBody, provider: string, model?: string): ModelKitError;
|
package/dist/index.js
CHANGED
|
@@ -3285,6 +3285,36 @@ function describeThrown(cause) {
|
|
|
3285
3285
|
|
|
3286
3286
|
// src/retry.ts
|
|
3287
3287
|
var textBody = z6.string();
|
|
3288
|
+
var BILLING_CODES = /* @__PURE__ */ new Set([
|
|
3289
|
+
"insufficient_quota",
|
|
3290
|
+
"billing_hard_limit_reached",
|
|
3291
|
+
"billing_not_active",
|
|
3292
|
+
"payment_required",
|
|
3293
|
+
"insufficient_balance",
|
|
3294
|
+
"insufficient_credits",
|
|
3295
|
+
"insufficient_credit"
|
|
3296
|
+
]);
|
|
3297
|
+
var errorCodes = z6.object({
|
|
3298
|
+
code: z6.string().optional(),
|
|
3299
|
+
type: z6.string().optional(),
|
|
3300
|
+
error: z6.object({ code: z6.string().optional(), type: z6.string().optional() }).optional()
|
|
3301
|
+
});
|
|
3302
|
+
var BILLING_TEXT = /insufficient_quota|payment[ _]required|exhausted balance|insufficient (?:credits?|balance|funds)|billing_hard_limit/i;
|
|
3303
|
+
var BILLING_TEXT_STATUSES = /* @__PURE__ */ new Set([403, 429]);
|
|
3304
|
+
function billingCode(body) {
|
|
3305
|
+
const parsed = errorCodes.safeParse(body);
|
|
3306
|
+
if (!parsed.success) return false;
|
|
3307
|
+
const { code, type, error } = parsed.data;
|
|
3308
|
+
return [code, type, error?.code, error?.type].some(
|
|
3309
|
+
(value) => value !== void 0 && BILLING_CODES.has(value.toLowerCase())
|
|
3310
|
+
);
|
|
3311
|
+
}
|
|
3312
|
+
function isBillingFailure(status, body, text2) {
|
|
3313
|
+
if (status === 402) return true;
|
|
3314
|
+
if (status === void 0 || status < 400 || status >= 500) return false;
|
|
3315
|
+
if (billingCode(body)) return true;
|
|
3316
|
+
return BILLING_TEXT_STATUSES.has(status) && BILLING_TEXT.test(text2);
|
|
3317
|
+
}
|
|
3288
3318
|
function classifyHttpError(status, body, provider, model) {
|
|
3289
3319
|
const asText = textBody.safeParse(body);
|
|
3290
3320
|
const text2 = asText.success ? asText.data : JSON.stringify(body);
|
|
@@ -3293,6 +3323,7 @@ function classifyHttpError(status, body, provider, model) {
|
|
|
3293
3323
|
);
|
|
3294
3324
|
let code = "provider_rejected";
|
|
3295
3325
|
if (blocked) code = "content_blocked";
|
|
3326
|
+
else if (isBillingFailure(status, body, text2)) code = "provider_billing";
|
|
3296
3327
|
else if (status === 401 || status === 403) code = "auth";
|
|
3297
3328
|
else if (status === 429) code = "rate_limited";
|
|
3298
3329
|
else if (status === 408 || status === 504) code = "timeout";
|
|
@@ -3355,12 +3386,11 @@ function sleep(ms, signal) {
|
|
|
3355
3386
|
rejectAborted();
|
|
3356
3387
|
return;
|
|
3357
3388
|
}
|
|
3358
|
-
let timer;
|
|
3359
3389
|
const onAbort = () => {
|
|
3360
|
-
|
|
3390
|
+
clearTimeout(timer);
|
|
3361
3391
|
rejectAborted();
|
|
3362
3392
|
};
|
|
3363
|
-
timer = setTimeout(() => {
|
|
3393
|
+
const timer = setTimeout(() => {
|
|
3364
3394
|
signal?.removeEventListener("abort", onAbort);
|
|
3365
3395
|
resolve();
|
|
3366
3396
|
}, ms);
|
|
@@ -9150,6 +9180,13 @@ function createModelKit(config, adapters) {
|
|
|
9150
9180
|
model: model.id
|
|
9151
9181
|
});
|
|
9152
9182
|
}
|
|
9183
|
+
function timed(event, startedAt) {
|
|
9184
|
+
if (event.latencyMs !== void 0) return event;
|
|
9185
|
+
return { ...event, latencyMs: Math.max(0, Date.now() - startedAt) };
|
|
9186
|
+
}
|
|
9187
|
+
function textTimed(event, stepCount, startedAt) {
|
|
9188
|
+
return stepCount === 1 ? timed(event, startedAt) : event;
|
|
9189
|
+
}
|
|
9153
9190
|
async function emit(event) {
|
|
9154
9191
|
try {
|
|
9155
9192
|
await config.onUsage?.(event);
|
|
@@ -9233,17 +9270,21 @@ function createModelKit(config, adapters) {
|
|
|
9233
9270
|
const abortSignal = combineSignals(req.abortSignal, call.signal);
|
|
9234
9271
|
if (abortSignal) options.abortSignal = abortSignal;
|
|
9235
9272
|
if (call.maxRetries !== void 0) options.maxRetries = call.maxRetries;
|
|
9273
|
+
const startedAt = Date.now();
|
|
9236
9274
|
const result = await aiGenerateImage(options);
|
|
9237
9275
|
const images = result.images.map(
|
|
9238
9276
|
(file) => toGeneratedImage({ bytes: file.uint8Array, mime: file.mediaType })
|
|
9239
9277
|
);
|
|
9240
|
-
const usage2 =
|
|
9241
|
-
|
|
9242
|
-
|
|
9243
|
-
|
|
9244
|
-
|
|
9245
|
-
|
|
9246
|
-
|
|
9278
|
+
const usage2 = timed(
|
|
9279
|
+
{
|
|
9280
|
+
model: model.modelId,
|
|
9281
|
+
provider: model.provider,
|
|
9282
|
+
modality: "IMAGE",
|
|
9283
|
+
unit: "images",
|
|
9284
|
+
quantity: images.length
|
|
9285
|
+
},
|
|
9286
|
+
startedAt
|
|
9287
|
+
);
|
|
9247
9288
|
await emit(usage2);
|
|
9248
9289
|
return { image: images[0], images, usage: usage2, raw: result, warnings };
|
|
9249
9290
|
}
|
|
@@ -9316,18 +9357,22 @@ function createModelKit(config, adapters) {
|
|
|
9316
9357
|
if (providerOptions) options.providerOptions = providerOptions;
|
|
9317
9358
|
if (abortSignal) options.abortSignal = abortSignal;
|
|
9318
9359
|
logCoercions(logger, model.modelId, warnings);
|
|
9360
|
+
const startedAt = Date.now();
|
|
9319
9361
|
const pending = aiGenerateVideo(options);
|
|
9320
9362
|
let settled;
|
|
9321
9363
|
let failed = false;
|
|
9322
9364
|
const done = pending.then(async (result) => {
|
|
9323
|
-
const usage2 =
|
|
9324
|
-
|
|
9325
|
-
|
|
9326
|
-
|
|
9327
|
-
|
|
9328
|
-
|
|
9329
|
-
|
|
9330
|
-
|
|
9365
|
+
const usage2 = timed(
|
|
9366
|
+
{
|
|
9367
|
+
model: model.modelId,
|
|
9368
|
+
provider: model.provider,
|
|
9369
|
+
modality: "VIDEO",
|
|
9370
|
+
unit: "seconds",
|
|
9371
|
+
quantity: seconds2 ?? 0,
|
|
9372
|
+
...seconds2 !== void 0 && { durationSeconds: seconds2 }
|
|
9373
|
+
},
|
|
9374
|
+
startedAt
|
|
9375
|
+
);
|
|
9331
9376
|
settled = {
|
|
9332
9377
|
video: { bytes: result.video.uint8Array, mime: result.video.mediaType },
|
|
9333
9378
|
usage: usage2,
|
|
@@ -9384,8 +9429,12 @@ function createModelKit(config, adapters) {
|
|
|
9384
9429
|
message: `${adapter.id} cannot generate images`,
|
|
9385
9430
|
model: model.id
|
|
9386
9431
|
});
|
|
9432
|
+
const startedAt = Date.now();
|
|
9387
9433
|
const result = await adapter.generateImage(request, ctx);
|
|
9388
|
-
const usage2 =
|
|
9434
|
+
const usage2 = timed(
|
|
9435
|
+
{ ...result.usage, model: model.id, unit: model.usage.unit },
|
|
9436
|
+
startedAt
|
|
9437
|
+
);
|
|
9389
9438
|
await emit(usage2);
|
|
9390
9439
|
const images = result.images.map(toGeneratedImage);
|
|
9391
9440
|
const first = images[0];
|
|
@@ -9428,6 +9477,7 @@ function createModelKit(config, adapters) {
|
|
|
9428
9477
|
message: `${adapter.id} cannot generate video`,
|
|
9429
9478
|
model: model.id
|
|
9430
9479
|
});
|
|
9480
|
+
const startedAt = Date.now();
|
|
9431
9481
|
const job = await adapter.generateVideo(request, ctx);
|
|
9432
9482
|
const result = job.result.bind(job);
|
|
9433
9483
|
let settled;
|
|
@@ -9437,11 +9487,14 @@ function createModelKit(config, adapters) {
|
|
|
9437
9487
|
result(opts) {
|
|
9438
9488
|
if (settled) return settled;
|
|
9439
9489
|
const pending = result(opts).then(
|
|
9440
|
-
(out) =>
|
|
9441
|
-
|
|
9442
|
-
|
|
9443
|
-
|
|
9444
|
-
|
|
9490
|
+
async (out) => {
|
|
9491
|
+
const usage2 = timed(
|
|
9492
|
+
{ ...out.usage, model: model.id, unit: model.usage.unit },
|
|
9493
|
+
startedAt
|
|
9494
|
+
);
|
|
9495
|
+
await emit(usage2);
|
|
9496
|
+
return { ...out, usage: usage2 };
|
|
9497
|
+
},
|
|
9445
9498
|
(cause) => {
|
|
9446
9499
|
settled = void 0;
|
|
9447
9500
|
throw cause;
|
|
@@ -9478,9 +9531,14 @@ function createModelKit(config, adapters) {
|
|
|
9478
9531
|
model: model.id
|
|
9479
9532
|
});
|
|
9480
9533
|
}
|
|
9534
|
+
const startedAt = Date.now();
|
|
9481
9535
|
const result = await adapter.generateAudio(resolved, ctx);
|
|
9482
|
-
|
|
9483
|
-
|
|
9536
|
+
const usage2 = timed(
|
|
9537
|
+
{ ...result.usage, model: model.id, unit: model.usage.unit },
|
|
9538
|
+
startedAt
|
|
9539
|
+
);
|
|
9540
|
+
await emit(usage2);
|
|
9541
|
+
return { ...result, usage: usage2 };
|
|
9484
9542
|
},
|
|
9485
9543
|
async prepareMediaPart(modelId, ref, mediaType, call = {}) {
|
|
9486
9544
|
mediaRetryOptions(call);
|
|
@@ -9509,11 +9567,13 @@ function createModelKit(config, adapters) {
|
|
|
9509
9567
|
const { webhookUrl, ...rest } = call;
|
|
9510
9568
|
const { adapter, ctx } = videoToolTarget(req.model, rest);
|
|
9511
9569
|
if (!adapter.submitVideoTool) throw noVideoTools(adapter, ctx.model);
|
|
9512
|
-
|
|
9570
|
+
const submittedAt = Date.now();
|
|
9571
|
+
const submission = await adapter.submitVideoTool(
|
|
9513
9572
|
req,
|
|
9514
9573
|
ctx,
|
|
9515
9574
|
webhookUrl !== void 0 ? { webhookUrl } : {}
|
|
9516
9575
|
);
|
|
9576
|
+
return { ...submission, submittedAt };
|
|
9517
9577
|
},
|
|
9518
9578
|
async videoToolStatus(sub, call = {}) {
|
|
9519
9579
|
const { adapter, ctx } = videoToolTarget(sub.model, call);
|
|
@@ -9525,8 +9585,10 @@ function createModelKit(config, adapters) {
|
|
|
9525
9585
|
const { adapter, ctx } = videoToolTarget(sub.model, rest);
|
|
9526
9586
|
if (!adapter.videoToolResult) throw noVideoTools(adapter, ctx.model);
|
|
9527
9587
|
const result = await adapter.videoToolResult(sub, ctx, webhook);
|
|
9528
|
-
|
|
9529
|
-
|
|
9588
|
+
const base = { ...result.usage, model: ctx.model.id, unit: ctx.model.usage.unit };
|
|
9589
|
+
const usage2 = sub.submittedAt === void 0 ? base : timed(base, sub.submittedAt);
|
|
9590
|
+
await emit(usage2);
|
|
9591
|
+
return { ...result, usage: usage2 };
|
|
9530
9592
|
},
|
|
9531
9593
|
async cancelVideoTool(sub, call = {}) {
|
|
9532
9594
|
const { adapter, ctx } = videoToolTarget(sub.model, call);
|
|
@@ -9548,6 +9610,7 @@ function createModelKit(config, adapters) {
|
|
|
9548
9610
|
(model) => textTarget(model, call),
|
|
9549
9611
|
options.providerOptions
|
|
9550
9612
|
);
|
|
9613
|
+
const startedAt = Date.now();
|
|
9551
9614
|
const result = await aiGenerateText2({
|
|
9552
9615
|
...options,
|
|
9553
9616
|
...textAbortOptions(options.abortSignal, call.signal),
|
|
@@ -9557,7 +9620,8 @@ function createModelKit(config, adapters) {
|
|
|
9557
9620
|
options.prepareStep ?? options.experimental_prepareStep
|
|
9558
9621
|
)
|
|
9559
9622
|
});
|
|
9560
|
-
for (const event of steps.usage(result.steps))
|
|
9623
|
+
for (const event of steps.usage(result.steps))
|
|
9624
|
+
await emit(textTimed(withRowUnit(event), result.steps.length, startedAt));
|
|
9561
9625
|
return result;
|
|
9562
9626
|
},
|
|
9563
9627
|
streamText(options, call = {}) {
|
|
@@ -9568,6 +9632,7 @@ function createModelKit(config, adapters) {
|
|
|
9568
9632
|
options.providerOptions
|
|
9569
9633
|
);
|
|
9570
9634
|
const callerOnFinish = options.onFinish;
|
|
9635
|
+
const startedAt = Date.now();
|
|
9571
9636
|
return aiStreamText2({
|
|
9572
9637
|
...options,
|
|
9573
9638
|
...textAbortOptions(options.abortSignal, call.signal),
|
|
@@ -9577,7 +9642,8 @@ function createModelKit(config, adapters) {
|
|
|
9577
9642
|
options.prepareStep ?? options.experimental_prepareStep
|
|
9578
9643
|
),
|
|
9579
9644
|
onFinish: async (event) => {
|
|
9580
|
-
for (const usage2 of steps.usage(event.steps))
|
|
9645
|
+
for (const usage2 of steps.usage(event.steps))
|
|
9646
|
+
await emit(textTimed(withRowUnit(usage2), event.steps.length, startedAt));
|
|
9581
9647
|
await callerOnFinish?.(event);
|
|
9582
9648
|
}
|
|
9583
9649
|
});
|
|
@@ -9590,15 +9656,22 @@ function createModelKit(config, adapters) {
|
|
|
9590
9656
|
model: languageModel2,
|
|
9591
9657
|
providerOptions: mergeTextProviderOptions(entry, options.providerOptions)
|
|
9592
9658
|
};
|
|
9659
|
+
const startedAt = Date.now();
|
|
9593
9660
|
const result = await aiGenerateObject2(sdkOptions);
|
|
9594
9661
|
await emit(
|
|
9595
|
-
|
|
9662
|
+
timed(
|
|
9663
|
+
withRowUnit(
|
|
9664
|
+
textUsageEvent(entry, provider, result.usage, result.providerMetadata)
|
|
9665
|
+
),
|
|
9666
|
+
startedAt
|
|
9667
|
+
)
|
|
9596
9668
|
);
|
|
9597
9669
|
return result;
|
|
9598
9670
|
},
|
|
9599
9671
|
streamObject(options, call = {}) {
|
|
9600
9672
|
const { entry, languageModel: languageModel2, provider } = textTarget(options.model, call);
|
|
9601
9673
|
const callerOnFinish = options.onFinish;
|
|
9674
|
+
const startedAt = Date.now();
|
|
9602
9675
|
const sdkOptions = {
|
|
9603
9676
|
...options,
|
|
9604
9677
|
...textAbortOptions(options.abortSignal, call.signal),
|
|
@@ -9606,8 +9679,16 @@ function createModelKit(config, adapters) {
|
|
|
9606
9679
|
providerOptions: mergeTextProviderOptions(entry, options.providerOptions),
|
|
9607
9680
|
onFinish: async (event) => {
|
|
9608
9681
|
await emit(
|
|
9609
|
-
|
|
9610
|
-
|
|
9682
|
+
timed(
|
|
9683
|
+
withRowUnit(
|
|
9684
|
+
textUsageEvent(
|
|
9685
|
+
entry,
|
|
9686
|
+
provider,
|
|
9687
|
+
event.usage,
|
|
9688
|
+
event.providerMetadata
|
|
9689
|
+
)
|
|
9690
|
+
),
|
|
9691
|
+
startedAt
|
|
9611
9692
|
)
|
|
9612
9693
|
);
|
|
9613
9694
|
await callerOnFinish?.(event);
|