@shop-os/modelkit 0.3.7 → 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/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" | "timeout" | "auth" | "network" | "rate_limited";
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" | "timeout" | "auth" | "network" | "rate_limited";
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
@@ -810,7 +810,9 @@ var OTHER_MODELS = [
810
810
  },
811
811
  {
812
812
  id: "bg-remove",
813
- aliases: [],
813
+ // Task ids the two emitters report on usage events (trigger-workflows,
814
+ // vibe-triggers); the usage price lookup resolves them to this row.
815
+ aliases: ["prodia-background-remover", "prodia-remove-background"],
814
816
  label: "Background Remover",
815
817
  type: "bg-remove",
816
818
  adapter: "prodia",
@@ -3283,6 +3285,36 @@ function describeThrown(cause) {
3283
3285
 
3284
3286
  // src/retry.ts
3285
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
+ }
3286
3318
  function classifyHttpError(status, body, provider, model) {
3287
3319
  const asText = textBody.safeParse(body);
3288
3320
  const text2 = asText.success ? asText.data : JSON.stringify(body);
@@ -3291,6 +3323,7 @@ function classifyHttpError(status, body, provider, model) {
3291
3323
  );
3292
3324
  let code = "provider_rejected";
3293
3325
  if (blocked) code = "content_blocked";
3326
+ else if (isBillingFailure(status, body, text2)) code = "provider_billing";
3294
3327
  else if (status === 401 || status === 403) code = "auth";
3295
3328
  else if (status === 429) code = "rate_limited";
3296
3329
  else if (status === 408 || status === 504) code = "timeout";
@@ -3353,12 +3386,11 @@ function sleep(ms, signal) {
3353
3386
  rejectAborted();
3354
3387
  return;
3355
3388
  }
3356
- let timer;
3357
3389
  const onAbort = () => {
3358
- if (timer !== void 0) clearTimeout(timer);
3390
+ clearTimeout(timer);
3359
3391
  rejectAborted();
3360
3392
  };
3361
- timer = setTimeout(() => {
3393
+ const timer = setTimeout(() => {
3362
3394
  signal?.removeEventListener("abort", onAbort);
3363
3395
  resolve();
3364
3396
  }, ms);
@@ -9148,6 +9180,13 @@ function createModelKit(config, adapters) {
9148
9180
  model: model.id
9149
9181
  });
9150
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
+ }
9151
9190
  async function emit(event) {
9152
9191
  try {
9153
9192
  await config.onUsage?.(event);
@@ -9231,17 +9270,21 @@ function createModelKit(config, adapters) {
9231
9270
  const abortSignal = combineSignals(req.abortSignal, call.signal);
9232
9271
  if (abortSignal) options.abortSignal = abortSignal;
9233
9272
  if (call.maxRetries !== void 0) options.maxRetries = call.maxRetries;
9273
+ const startedAt = Date.now();
9234
9274
  const result = await aiGenerateImage(options);
9235
9275
  const images = result.images.map(
9236
9276
  (file) => toGeneratedImage({ bytes: file.uint8Array, mime: file.mediaType })
9237
9277
  );
9238
- const usage2 = {
9239
- model: model.modelId,
9240
- provider: model.provider,
9241
- modality: "IMAGE",
9242
- unit: "images",
9243
- quantity: images.length
9244
- };
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
+ );
9245
9288
  await emit(usage2);
9246
9289
  return { image: images[0], images, usage: usage2, raw: result, warnings };
9247
9290
  }
@@ -9314,18 +9357,22 @@ function createModelKit(config, adapters) {
9314
9357
  if (providerOptions) options.providerOptions = providerOptions;
9315
9358
  if (abortSignal) options.abortSignal = abortSignal;
9316
9359
  logCoercions(logger, model.modelId, warnings);
9360
+ const startedAt = Date.now();
9317
9361
  const pending = aiGenerateVideo(options);
9318
9362
  let settled;
9319
9363
  let failed = false;
9320
9364
  const done = pending.then(async (result) => {
9321
- const usage2 = {
9322
- model: model.modelId,
9323
- provider: model.provider,
9324
- modality: "VIDEO",
9325
- unit: "seconds",
9326
- quantity: seconds2 ?? 0,
9327
- ...seconds2 !== void 0 && { durationSeconds: seconds2 }
9328
- };
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
+ );
9329
9376
  settled = {
9330
9377
  video: { bytes: result.video.uint8Array, mime: result.video.mediaType },
9331
9378
  usage: usage2,
@@ -9382,8 +9429,12 @@ function createModelKit(config, adapters) {
9382
9429
  message: `${adapter.id} cannot generate images`,
9383
9430
  model: model.id
9384
9431
  });
9432
+ const startedAt = Date.now();
9385
9433
  const result = await adapter.generateImage(request, ctx);
9386
- const usage2 = { ...result.usage, model: model.id, unit: model.usage.unit };
9434
+ const usage2 = timed(
9435
+ { ...result.usage, model: model.id, unit: model.usage.unit },
9436
+ startedAt
9437
+ );
9387
9438
  await emit(usage2);
9388
9439
  const images = result.images.map(toGeneratedImage);
9389
9440
  const first = images[0];
@@ -9426,6 +9477,7 @@ function createModelKit(config, adapters) {
9426
9477
  message: `${adapter.id} cannot generate video`,
9427
9478
  model: model.id
9428
9479
  });
9480
+ const startedAt = Date.now();
9429
9481
  const job = await adapter.generateVideo(request, ctx);
9430
9482
  const result = job.result.bind(job);
9431
9483
  let settled;
@@ -9435,11 +9487,14 @@ function createModelKit(config, adapters) {
9435
9487
  result(opts) {
9436
9488
  if (settled) return settled;
9437
9489
  const pending = result(opts).then(
9438
- (out) => emit({
9439
- ...out.usage,
9440
- model: model.id,
9441
- unit: model.usage.unit
9442
- }).then(() => out),
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
+ },
9443
9498
  (cause) => {
9444
9499
  settled = void 0;
9445
9500
  throw cause;
@@ -9476,9 +9531,14 @@ function createModelKit(config, adapters) {
9476
9531
  model: model.id
9477
9532
  });
9478
9533
  }
9534
+ const startedAt = Date.now();
9479
9535
  const result = await adapter.generateAudio(resolved, ctx);
9480
- await emit({ ...result.usage, model: model.id, unit: model.usage.unit });
9481
- return result;
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 };
9482
9542
  },
9483
9543
  async prepareMediaPart(modelId, ref, mediaType, call = {}) {
9484
9544
  mediaRetryOptions(call);
@@ -9507,11 +9567,13 @@ function createModelKit(config, adapters) {
9507
9567
  const { webhookUrl, ...rest } = call;
9508
9568
  const { adapter, ctx } = videoToolTarget(req.model, rest);
9509
9569
  if (!adapter.submitVideoTool) throw noVideoTools(adapter, ctx.model);
9510
- return adapter.submitVideoTool(
9570
+ const submittedAt = Date.now();
9571
+ const submission = await adapter.submitVideoTool(
9511
9572
  req,
9512
9573
  ctx,
9513
9574
  webhookUrl !== void 0 ? { webhookUrl } : {}
9514
9575
  );
9576
+ return { ...submission, submittedAt };
9515
9577
  },
9516
9578
  async videoToolStatus(sub, call = {}) {
9517
9579
  const { adapter, ctx } = videoToolTarget(sub.model, call);
@@ -9523,8 +9585,10 @@ function createModelKit(config, adapters) {
9523
9585
  const { adapter, ctx } = videoToolTarget(sub.model, rest);
9524
9586
  if (!adapter.videoToolResult) throw noVideoTools(adapter, ctx.model);
9525
9587
  const result = await adapter.videoToolResult(sub, ctx, webhook);
9526
- await emit({ ...result.usage, model: ctx.model.id, unit: ctx.model.usage.unit });
9527
- return result;
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 };
9528
9592
  },
9529
9593
  async cancelVideoTool(sub, call = {}) {
9530
9594
  const { adapter, ctx } = videoToolTarget(sub.model, call);
@@ -9546,6 +9610,7 @@ function createModelKit(config, adapters) {
9546
9610
  (model) => textTarget(model, call),
9547
9611
  options.providerOptions
9548
9612
  );
9613
+ const startedAt = Date.now();
9549
9614
  const result = await aiGenerateText2({
9550
9615
  ...options,
9551
9616
  ...textAbortOptions(options.abortSignal, call.signal),
@@ -9555,7 +9620,8 @@ function createModelKit(config, adapters) {
9555
9620
  options.prepareStep ?? options.experimental_prepareStep
9556
9621
  )
9557
9622
  });
9558
- for (const event of steps.usage(result.steps)) await emit(withRowUnit(event));
9623
+ for (const event of steps.usage(result.steps))
9624
+ await emit(textTimed(withRowUnit(event), result.steps.length, startedAt));
9559
9625
  return result;
9560
9626
  },
9561
9627
  streamText(options, call = {}) {
@@ -9566,6 +9632,7 @@ function createModelKit(config, adapters) {
9566
9632
  options.providerOptions
9567
9633
  );
9568
9634
  const callerOnFinish = options.onFinish;
9635
+ const startedAt = Date.now();
9569
9636
  return aiStreamText2({
9570
9637
  ...options,
9571
9638
  ...textAbortOptions(options.abortSignal, call.signal),
@@ -9575,7 +9642,8 @@ function createModelKit(config, adapters) {
9575
9642
  options.prepareStep ?? options.experimental_prepareStep
9576
9643
  ),
9577
9644
  onFinish: async (event) => {
9578
- for (const usage2 of steps.usage(event.steps)) await emit(withRowUnit(usage2));
9645
+ for (const usage2 of steps.usage(event.steps))
9646
+ await emit(textTimed(withRowUnit(usage2), event.steps.length, startedAt));
9579
9647
  await callerOnFinish?.(event);
9580
9648
  }
9581
9649
  });
@@ -9588,15 +9656,22 @@ function createModelKit(config, adapters) {
9588
9656
  model: languageModel2,
9589
9657
  providerOptions: mergeTextProviderOptions(entry, options.providerOptions)
9590
9658
  };
9659
+ const startedAt = Date.now();
9591
9660
  const result = await aiGenerateObject2(sdkOptions);
9592
9661
  await emit(
9593
- withRowUnit(textUsageEvent(entry, provider, result.usage, result.providerMetadata))
9662
+ timed(
9663
+ withRowUnit(
9664
+ textUsageEvent(entry, provider, result.usage, result.providerMetadata)
9665
+ ),
9666
+ startedAt
9667
+ )
9594
9668
  );
9595
9669
  return result;
9596
9670
  },
9597
9671
  streamObject(options, call = {}) {
9598
9672
  const { entry, languageModel: languageModel2, provider } = textTarget(options.model, call);
9599
9673
  const callerOnFinish = options.onFinish;
9674
+ const startedAt = Date.now();
9600
9675
  const sdkOptions = {
9601
9676
  ...options,
9602
9677
  ...textAbortOptions(options.abortSignal, call.signal),
@@ -9604,8 +9679,16 @@ function createModelKit(config, adapters) {
9604
9679
  providerOptions: mergeTextProviderOptions(entry, options.providerOptions),
9605
9680
  onFinish: async (event) => {
9606
9681
  await emit(
9607
- withRowUnit(
9608
- textUsageEvent(entry, provider, event.usage, event.providerMetadata)
9682
+ timed(
9683
+ withRowUnit(
9684
+ textUsageEvent(
9685
+ entry,
9686
+ provider,
9687
+ event.usage,
9688
+ event.providerMetadata
9689
+ )
9690
+ ),
9691
+ startedAt
9609
9692
  )
9610
9693
  );
9611
9694
  await callerOnFinish?.(event);