@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/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
@@ -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
- if (timer !== void 0) clearTimeout(timer);
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
- model: model.modelId,
9242
- provider: model.provider,
9243
- modality: "IMAGE",
9244
- unit: "images",
9245
- quantity: images.length
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
- model: model.modelId,
9325
- provider: model.provider,
9326
- modality: "VIDEO",
9327
- unit: "seconds",
9328
- quantity: seconds2 ?? 0,
9329
- ...seconds2 !== void 0 && { durationSeconds: seconds2 }
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 = { ...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
+ );
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) => emit({
9441
- ...out.usage,
9442
- model: model.id,
9443
- unit: model.usage.unit
9444
- }).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
+ },
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
- await emit({ ...result.usage, model: model.id, unit: model.usage.unit });
9483
- 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 };
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
- return adapter.submitVideoTool(
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
- await emit({ ...result.usage, model: ctx.model.id, unit: ctx.model.usage.unit });
9529
- 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 };
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)) await emit(withRowUnit(event));
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)) await emit(withRowUnit(usage2));
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
- withRowUnit(textUsageEvent(entry, provider, result.usage, result.providerMetadata))
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
- withRowUnit(
9610
- 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
9611
9692
  )
9612
9693
  );
9613
9694
  await callerOnFinish?.(event);